diff options
Diffstat (limited to 'gptel-tools')
| -rw-r--r-- | gptel-tools/git_diff.el | 8 | ||||
| -rw-r--r-- | gptel-tools/git_log.el | 8 | ||||
| -rw-r--r-- | gptel-tools/git_status.el | 8 | ||||
| -rw-r--r-- | gptel-tools/move_to_trash.el | 23 | ||||
| -rw-r--r-- | gptel-tools/read_buffer.el | 4 | ||||
| -rw-r--r-- | gptel-tools/read_text_file.el | 20 | ||||
| -rw-r--r-- | gptel-tools/update_text_file.el | 30 | ||||
| -rw-r--r-- | gptel-tools/web_fetch.el | 2 | ||||
| -rw-r--r-- | gptel-tools/write_text_file.el | 15 |
9 files changed, 82 insertions, 36 deletions
diff --git a/gptel-tools/git_diff.el b/gptel-tools/git_diff.el index daccdc20..47db8dae 100644 --- a/gptel-tools/git_diff.el +++ b/gptel-tools/git_diff.el @@ -23,11 +23,17 @@ "Validate PATH for a git diff call. Return the expanded path on success. Same contract as the other git_* validators: under HOME, a directory, inside a git working tree." - (let ((full (expand-file-name (or path "~") "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full (expand-file-name (or path "~") "~"))) (unless (string-prefix-p (expand-file-name "~") full) (error "Path must be within home directory: %s" path)) (unless (file-directory-p full) (error "Not a directory: %s" full)) + (let ((resolved (file-truename full))) + (unless (or (string= resolved (directory-file-name home)) + (string-prefix-p home resolved)) + (error "Resolved path must be within home directory: %s" path)) + (setq full resolved)) (let ((default-directory full)) (unless (zerop (process-file "git" nil nil nil "rev-parse" "--is-inside-work-tree")) diff --git a/gptel-tools/git_log.el b/gptel-tools/git_log.el index 9cfae263..324435dc 100644 --- a/gptel-tools/git_log.el +++ b/gptel-tools/git_log.el @@ -25,11 +25,17 @@ "Validate PATH for a git log call. Return the expanded path on success. Same contract as the git_status validator: must be under HOME, must be a directory, must be inside a git working tree." - (let ((full (expand-file-name (or path "~") "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full (expand-file-name (or path "~") "~"))) (unless (string-prefix-p (expand-file-name "~") full) (error "Path must be within home directory: %s" path)) (unless (file-directory-p full) (error "Not a directory: %s" full)) + (let ((resolved (file-truename full))) + (unless (or (string= resolved (directory-file-name home)) + (string-prefix-p home resolved)) + (error "Resolved path must be within home directory: %s" path)) + (setq full resolved)) (let ((default-directory full)) (unless (zerop (process-file "git" nil nil nil "rev-parse" "--is-inside-work-tree")) diff --git a/gptel-tools/git_status.el b/gptel-tools/git_status.el index 300d5da5..de76a985 100644 --- a/gptel-tools/git_status.el +++ b/gptel-tools/git_status.el @@ -23,11 +23,17 @@ PATH must resolve under the user's home directory, must be an existing directory, and must be inside a git working tree. Returns the expanded path string on success; signals `error' otherwise." - (let ((full (expand-file-name (or path "~") "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full (expand-file-name (or path "~") "~"))) (unless (string-prefix-p (expand-file-name "~") full) (error "Path must be within home directory: %s" path)) (unless (file-directory-p full) (error "Not a directory: %s" full)) + (let ((resolved (file-truename full))) + (unless (or (string= resolved (directory-file-name home)) + (string-prefix-p home resolved)) + (error "Resolved path must be within home directory: %s" path)) + (setq full resolved)) (let ((default-directory full)) (unless (zerop (process-file "git" nil nil nil "rev-parse" "--is-inside-work-tree")) diff --git a/gptel-tools/move_to_trash.el b/gptel-tools/move_to_trash.el index 6ea97995..923da790 100644 --- a/gptel-tools/move_to_trash.el +++ b/gptel-tools/move_to_trash.el @@ -41,7 +41,7 @@ YYYY-MM-DD-HH-MM-SS." (let* ((extension (file-name-extension base-name t)) (name-sans-ext (file-name-sans-extension base-name)) (timestamp (format-time-string "%Y-%m-%d-%H-%M-%S")) - (new-name (if extension + (new-name (if (and extension (not (string= extension ""))) (concat name-sans-ext "-" timestamp extension) (concat base-name "-" timestamp)))) (expand-file-name new-name trash-dir))))) @@ -51,15 +51,18 @@ YYYY-MM-DD-HH-MM-SS." Returns the expanded path if valid, signals an error otherwise. Ensures path is within home directory or /tmp, and prevents trashing of critical system directories." - (let ((expanded-path (expand-file-name path)) - (home-dir (expand-file-name "~")) - (critical-dirs (list (expand-file-name "~") - (expand-file-name "~/.emacs.d") - (expand-file-name "~/.config") - "/tmp"))) + (let* ((expanded-path (expand-file-name path)) + (resolved-path (and (file-exists-p expanded-path) + (file-truename expanded-path))) + (home-dir (file-name-as-directory (file-truename (expand-file-name "~")))) + (tmp-dir (file-name-as-directory (file-truename "/tmp"))) + (critical-dirs (list (directory-file-name home-dir) + (file-truename (expand-file-name "~/.emacs.d")) + (file-truename (expand-file-name "~/.config")) + (directory-file-name tmp-dir)))) ;; Security check: must be within allowed directories (unless (or (string-prefix-p home-dir expanded-path) - (string-prefix-p "/tmp" expanded-path)) + (string-prefix-p tmp-dir expanded-path)) (error "Path must be within home directory or /tmp: %s" path)) ;; Prevent trashing critical directories @@ -70,6 +73,10 @@ trashing of critical system directories." (unless (file-exists-p expanded-path) (error "File or directory does not exist: %s" path)) + (unless (or (string-prefix-p home-dir resolved-path) + (string-prefix-p tmp-dir resolved-path)) + (error "Resolved path must be within home directory or /tmp: %s" path)) + expanded-path)) (defun gptel--move-to-trash-perform (expanded-path trash-dir) diff --git a/gptel-tools/read_buffer.el b/gptel-tools/read_buffer.el index 1b4fc904..c9136e3c 100644 --- a/gptel-tools/read_buffer.el +++ b/gptel-tools/read_buffer.el @@ -14,7 +14,9 @@ error when no live buffer matches." (unless (buffer-live-p (get-buffer buffer)) (error "Buffer %s is not live" buffer)) (with-current-buffer buffer - (buffer-substring-no-properties (point-min) (point-max)))) + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max))))) (gptel-make-tool :name "read_buffer" diff --git a/gptel-tools/read_text_file.el b/gptel-tools/read_text_file.el index 8e0433a9..f35c9494 100644 --- a/gptel-tools/read_text_file.el +++ b/gptel-tools/read_text_file.el @@ -25,19 +25,21 @@ ;; Helper functions for read_text_file tool (defun cj/validate-file-path (path) "Validate PATH is within home directory and exists." - (let ((full-path (expand-file-name path "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full-path (expand-file-name path "~"))) (unless (string-prefix-p (expand-file-name "~") full-path) (error "Path must be within home directory")) (unless (file-exists-p full-path) (error "File not found: %s" full-path)) - (when (file-directory-p full-path) - (error "Path is a directory, not a file: %s" full-path)) - (unless (file-readable-p full-path) - (error "No read permission for file: %s" full-path)) - ;; Follow symlinks - (if (file-symlink-p full-path) - (file-truename full-path) - full-path))) + (let ((resolved (file-truename full-path))) + (unless (or (string= resolved (directory-file-name home)) + (string-prefix-p home resolved)) + (error "Resolved path must be within home directory: %s" path)) + (when (file-directory-p resolved) + (error "Path is a directory, not a file: %s" resolved)) + (unless (file-readable-p resolved) + (error "No read permission for file: %s" resolved)) + resolved))) (defun cj/get-file-metadata (path) "Return formatted metadata string for file at PATH." diff --git a/gptel-tools/update_text_file.el b/gptel-tools/update_text_file.el index 492ed554..f8b58025 100644 --- a/gptel-tools/update_text_file.el +++ b/gptel-tools/update_text_file.el @@ -40,20 +40,23 @@ PATH must resolve inside the user's home directory, must exist, must be a regular file, and must be readable and writable." - (let ((full (expand-file-name path "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full (expand-file-name path "~"))) (unless (string-prefix-p (expand-file-name "~") full) (error "Path must be within home directory: %s" path)) (unless (file-exists-p full) (error "File not found: %s" full)) - (when (file-directory-p full) - (error "Path is a directory, not a file: %s" full)) - (unless (file-readable-p full) - (error "No read permission for file: %s" full)) - (unless (file-writable-p full) - (error "No write permission for file: %s" full)) - (if (file-symlink-p full) - (file-truename full) - full))) + (let ((resolved (file-truename full))) + (unless (or (string= resolved (directory-file-name home)) + (string-prefix-p home resolved)) + (error "Resolved path must be within home directory: %s" path)) + (when (file-directory-p resolved) + (error "Path is a directory, not a file: %s" resolved)) + (unless (file-readable-p resolved) + (error "No read permission for file: %s" resolved)) + (unless (file-writable-p resolved) + (error "No write permission for file: %s" resolved)) + resolved))) (defun cj/update-text-file--backup-name (path) "Return a backup filename for PATH timestamped to the current second." @@ -113,9 +116,10 @@ on out-of-range LINE-NUM or empty TEXT." ;; extra empty element at the end. Trim it so the line count ;; matches what a human would say. (trailing-newline (string-suffix-p "\n" content)) - (line-count (if trailing-newline - (1- (length lines)) - (length lines)))) + (line-count (cond + ((string-empty-p content) 0) + (trailing-newline (1- (length lines))) + (t (length lines))))) (when (> line-num (1+ line-count)) (error "Line %d out of range (file has %d lines)" line-num line-count)) (let* ((to-insert (if (string-suffix-p "\n" text) diff --git a/gptel-tools/web_fetch.el b/gptel-tools/web_fetch.el index 1f950a31..b2f80c5f 100644 --- a/gptel-tools/web_fetch.el +++ b/gptel-tools/web_fetch.el @@ -62,7 +62,7 @@ from the response status line, or nil when the line is unrecognized." (let* ((status (when (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" (point-max) t) (string-to-number (match-string 1)))) - (body-start (when (re-search-forward "\n\n" nil t) + (body-start (when (re-search-forward "\r?\n\r?\n" nil t) (point)))) (cons status (if body-start diff --git a/gptel-tools/write_text_file.el b/gptel-tools/write_text_file.el index 40482c66..1bda5446 100644 --- a/gptel-tools/write_text_file.el +++ b/gptel-tools/write_text_file.el @@ -22,9 +22,22 @@ (defun cj/write-text-file--validate-path (path) "Validate PATH for write. Return the expanded path on success. PATH must resolve inside the user's home directory." - (let ((full (expand-file-name path "~"))) + (let* ((home (file-name-as-directory (file-truename (expand-file-name "~")))) + (full (expand-file-name path "~")) + (existing (and (file-exists-p full) (file-truename full))) + (parent (file-name-directory full)) + (resolved-parent (and parent + (file-exists-p parent) + (file-truename parent)))) (unless (string-prefix-p (expand-file-name "~") full) (error "Path must be within home directory: %s" path)) + (when (and existing + (not (string-prefix-p home existing))) + (error "Resolved path must be within home directory: %s" path)) + (when (and resolved-parent + (not (or (string= resolved-parent (directory-file-name home)) + (string-prefix-p home resolved-parent)))) + (error "Resolved parent must be within home directory: %s" path)) full)) (defun cj/write-text-file--backup-name (path) |
