diff options
| author | Ruijie Yu | 2023-03-06 11:03:32 +0800 |
|---|---|---|
| committer | Eli Zaretskii | 2023-04-20 12:26:05 +0300 |
| commit | fd4c9246fc8daea4965b868e80e0f2d9d544dc22 (patch) | |
| tree | 1ae2a962dad71de1ed46e283fa9b19b21aa5c722 | |
| parent | e0c8e4f12fb18695f309b1fd5ff26513ac5611e5 (diff) | |
| download | emacs-fd4c9246fc8daea4965b868e80e0f2d9d544dc22.tar.gz emacs-fd4c9246fc8daea4965b868e80e0f2d9d544dc22.zip | |
Handle modifications in extensionless zip files (bug#61326)
* lisp/arc-mode.el (archive-*-write-file-member)
(archive-*-expunge): Refactor to correctly modify
extensionless zip archives.
(archive-expunge): Move implementation to a separate helper
function to facilitate testing.
(archive--act-files): New helper function to wrap around
`call-process' calls.
(archive--need-rename-p): New helper function to check whether
a temporary rename is necessary.
(archive--ensure-extension) (archive--maybe-rename): New helper
functions to rename archive if the caller deems it necessary.
(archive--with-ensure-extension): New helper function to handle
writing an archive while ensuring extensionless archives work
correctly by temporarily renaming them.
* test/lisp/arc-mode-tests.el (arc-mode-test-zip-ensure-ext):
New regression test for bug#61326.
| -rw-r--r-- | lisp/arc-mode.el | 76 | ||||
| -rw-r--r-- | test/lisp/arc-mode-tests.el | 67 |
2 files changed, 123 insertions, 20 deletions
diff --git a/lisp/arc-mode.el b/lisp/arc-mode.el index 5e696c091b2..0a971799746 100644 --- a/lisp/arc-mode.el +++ b/lisp/arc-mode.el | |||
| @@ -645,6 +645,49 @@ Does not signal an error if optional argument NOERROR is non-nil." | |||
| 645 | (if (not noerror) | 645 | (if (not noerror) |
| 646 | (error "Line does not describe a member of the archive"))))) | 646 | (error "Line does not describe a member of the archive"))))) |
| 647 | ;; ------------------------------------------------------------------------- | 647 | ;; ------------------------------------------------------------------------- |
| 648 | ;;; Section: Helper functions for requiring filename extensions | ||
| 649 | |||
| 650 | (defun archive--act-files (command files) | ||
| 651 | (lambda (archive) | ||
| 652 | (apply #'call-process (car command) | ||
| 653 | nil nil nil (append (cdr command) (cons archive files))))) | ||
| 654 | |||
| 655 | (defun archive--need-rename-p (&optional archive) | ||
| 656 | (let ((archive | ||
| 657 | (file-name-nondirectory (or archive buffer-file-name)))) | ||
| 658 | (cl-case archive-subtype | ||
| 659 | ((zip) (not (seq-contains-p archive ?. #'eq)))))) | ||
| 660 | |||
| 661 | (defun archive--ensure-extension (archive ensure-extension) | ||
| 662 | (if ensure-extension | ||
| 663 | (make-temp-name (expand-file-name (concat archive "_tmp."))) | ||
| 664 | archive)) | ||
| 665 | |||
| 666 | (defun archive--maybe-rename (newname need-rename-p) | ||
| 667 | ;; Operating with archive as current buffer, and protect | ||
| 668 | ;; `default-directory' from being modified in `rename-visited-file'. | ||
| 669 | (when need-rename-p | ||
| 670 | (let ((default-directory default-directory)) | ||
| 671 | (rename-visited-file newname)))) | ||
| 672 | |||
| 673 | (defun archive--with-ensure-extension (archive proc-fn) | ||
| 674 | (let ((saved default-directory)) | ||
| 675 | (with-current-buffer (find-buffer-visiting archive) | ||
| 676 | (let ((ensure-extension (archive--need-rename-p)) | ||
| 677 | (default-directory saved)) | ||
| 678 | (unwind-protect | ||
| 679 | ;; Some archive programs (like zip) expect filenames to | ||
| 680 | ;; have an extension, so if necessary, temporarily rename | ||
| 681 | ;; an extensionless file for write accesses. | ||
| 682 | (let ((archive (archive--ensure-extension | ||
| 683 | archive ensure-extension))) | ||
| 684 | (archive--maybe-rename archive ensure-extension) | ||
| 685 | (let ((exitcode (funcall proc-fn archive))) | ||
| 686 | (or (zerop exitcode) | ||
| 687 | (error "Updating was unsuccessful (%S)" exitcode)))) | ||
| 688 | (progn (archive--maybe-rename archive ensure-extension) | ||
| 689 | (revert-buffer nil t))))))) | ||
| 690 | ;; ------------------------------------------------------------------------- | ||
| 648 | ;;; Section: the mode definition | 691 | ;;; Section: the mode definition |
| 649 | 692 | ||
| 650 | ;;;###autoload | 693 | ;;;###autoload |
| @@ -1378,16 +1421,9 @@ NEW-NAME." | |||
| 1378 | (setq ename | 1421 | (setq ename |
| 1379 | (encode-coding-string ename archive-file-name-coding-system)) | 1422 | (encode-coding-string ename archive-file-name-coding-system)) |
| 1380 | (let* ((coding-system-for-write 'no-conversion) | 1423 | (let* ((coding-system-for-write 'no-conversion) |
| 1381 | (default-directory (file-name-as-directory archive-tmpdir)) | 1424 | (default-directory (file-name-as-directory archive-tmpdir))) |
| 1382 | (exitcode (apply #'call-process | 1425 | (archive--with-ensure-extension |
| 1383 | (car command) | 1426 | archive (archive--act-files command (list ename))))) |
| 1384 | nil | ||
| 1385 | nil | ||
| 1386 | nil | ||
| 1387 | (append (cdr command) | ||
| 1388 | (list archive ename))))) | ||
| 1389 | (or (zerop exitcode) | ||
| 1390 | (error "Updating was unsuccessful (%S)" exitcode)))) | ||
| 1391 | (archive-delete-local tmpfile)))) | 1427 | (archive-delete-local tmpfile)))) |
| 1392 | 1428 | ||
| 1393 | (defun archive-write-file (&optional file) | 1429 | (defun archive-write-file (&optional file) |
| @@ -1510,9 +1546,7 @@ as a relative change like \"g+rw\" as for chmod(2)." | |||
| 1510 | (archive-resummarize)) | 1546 | (archive-resummarize)) |
| 1511 | (error "Setting group is not supported for this archive type")))) | 1547 | (error "Setting group is not supported for this archive type")))) |
| 1512 | 1548 | ||
| 1513 | (defun archive-expunge () | 1549 | (defun archive--expunge-maybe-force (force) |
| 1514 | "Do the flagged deletions." | ||
| 1515 | (interactive) | ||
| 1516 | (let (files) | 1550 | (let (files) |
| 1517 | (save-excursion | 1551 | (save-excursion |
| 1518 | (goto-char archive-file-list-start) | 1552 | (goto-char archive-file-list-start) |
| @@ -1526,7 +1560,8 @@ as a relative change like \"g+rw\" as for chmod(2)." | |||
| 1526 | (and files | 1560 | (and files |
| 1527 | (or (not archive-read-only) | 1561 | (or (not archive-read-only) |
| 1528 | (error "Archive is read-only")) | 1562 | (error "Archive is read-only")) |
| 1529 | (or (yes-or-no-p (format "Really delete %d member%s? " | 1563 | (or force |
| 1564 | (yes-or-no-p (format "Really delete %d member%s? " | ||
| 1530 | (length files) | 1565 | (length files) |
| 1531 | (if (null (cdr files)) "" "s"))) | 1566 | (if (null (cdr files)) "" "s"))) |
| 1532 | (error "Operation aborted")) | 1567 | (error "Operation aborted")) |
| @@ -1540,13 +1575,14 @@ as a relative change like \"g+rw\" as for chmod(2)." | |||
| 1540 | (archive-resummarize) | 1575 | (archive-resummarize) |
| 1541 | (revert-buffer)))))) | 1576 | (revert-buffer)))))) |
| 1542 | 1577 | ||
| 1578 | (defun archive-expunge () | ||
| 1579 | "Do the flagged deletions." | ||
| 1580 | (interactive) | ||
| 1581 | (archive--expunge-maybe-force nil)) | ||
| 1582 | |||
| 1543 | (defun archive-*-expunge (archive files command) | 1583 | (defun archive-*-expunge (archive files command) |
| 1544 | (apply #'call-process | 1584 | (archive--with-ensure-extension |
| 1545 | (car command) | 1585 | archive (archive--act-files command files))) |
| 1546 | nil | ||
| 1547 | nil | ||
| 1548 | nil | ||
| 1549 | (append (cdr command) (cons archive files)))) | ||
| 1550 | 1586 | ||
| 1551 | (defun archive-rename-entry (newname) | 1587 | (defun archive-rename-entry (newname) |
| 1552 | "Change the name associated with this entry in the archive file." | 1588 | "Change the name associated with this entry in the archive file." |
diff --git a/test/lisp/arc-mode-tests.el b/test/lisp/arc-mode-tests.el index 32bce1b71bd..b6e06a563fe 100644 --- a/test/lisp/arc-mode-tests.el +++ b/test/lisp/arc-mode-tests.el | |||
| @@ -46,6 +46,73 @@ | |||
| 46 | (when (buffer-live-p zip-buffer) (kill-buffer zip-buffer)) | 46 | (when (buffer-live-p zip-buffer) (kill-buffer zip-buffer)) |
| 47 | (when (buffer-live-p gz-buffer) (kill-buffer gz-buffer))))) | 47 | (when (buffer-live-p gz-buffer) (kill-buffer gz-buffer))))) |
| 48 | 48 | ||
| 49 | (ert-deftest arc-mode-test-zip-ensure-ext () | ||
| 50 | "Regression test for bug#61326." | ||
| 51 | (skip-unless (executable-find "zip")) | ||
| 52 | (let* ((default-directory arc-mode-tests-data-directory) | ||
| 53 | (base-zip-1 "base-1.zip") | ||
| 54 | (base-zip-2 "base-2.zip") | ||
| 55 | (content-1 '("1" "2")) | ||
| 56 | (content-2 '("3" "4")) | ||
| 57 | (make-file (lambda (name) | ||
| 58 | (with-temp-buffer | ||
| 59 | (insert name) | ||
| 60 | (write-file name)))) | ||
| 61 | (make-zip | ||
| 62 | (lambda (zip files) | ||
| 63 | (delete-file zip nil) | ||
| 64 | (funcall (archive--act-files '("zip") files) zip))) | ||
| 65 | (update-fn | ||
| 66 | (lambda (zip-nonempty) | ||
| 67 | (with-current-buffer (find-file-noselect zip-nonempty) | ||
| 68 | (save-excursion | ||
| 69 | (goto-char archive-file-list-start) | ||
| 70 | (save-current-buffer | ||
| 71 | (archive-extract) | ||
| 72 | (save-excursion | ||
| 73 | (goto-char (point-max)) | ||
| 74 | (insert ?a) | ||
| 75 | (save-buffer)) | ||
| 76 | (kill-buffer (current-buffer))) | ||
| 77 | (archive-extract) | ||
| 78 | ;; [2] must be ?a; [3] must be (eobp) | ||
| 79 | (should (eq (char-after 2) ?a)) | ||
| 80 | (should (eq (point-max) 3)))))) | ||
| 81 | (delete-fn | ||
| 82 | (lambda (zip-nonempty) | ||
| 83 | (with-current-buffer (find-file-noselect zip-nonempty) | ||
| 84 | ;; mark delete and expunge first entry | ||
| 85 | (save-excursion | ||
| 86 | (goto-char archive-file-list-start) | ||
| 87 | (should (length= archive-files 2)) | ||
| 88 | (archive-flag-deleted 1) | ||
| 89 | (archive--expunge-maybe-force t) | ||
| 90 | (should (length= archive-files 1)))))) | ||
| 91 | (test-modify | ||
| 92 | (lambda (zip mod-fn) | ||
| 93 | (let ((zip-base (concat zip ".zip")) | ||
| 94 | (tag (gensym))) | ||
| 95 | (copy-file base-zip-1 zip t) | ||
| 96 | (copy-file base-zip-2 zip-base t) | ||
| 97 | (file-has-changed-p zip tag) | ||
| 98 | (file-has-changed-p zip-base tag) | ||
| 99 | (funcall mod-fn zip) | ||
| 100 | (should-not (file-has-changed-p zip-base tag)) | ||
| 101 | (should (file-has-changed-p zip tag)))))) | ||
| 102 | ;; setup: make two zip files with different contents | ||
| 103 | (mapc make-file (append content-1 content-2)) | ||
| 104 | (mapc (lambda (args) (apply make-zip args)) | ||
| 105 | (list (list base-zip-1 content-1) | ||
| 106 | (list base-zip-2 content-2))) | ||
| 107 | ;; test 1: with "test-update" and "test-update.zip", update | ||
| 108 | ;; "test-update": (1) ensure only "test-update" is modified, (2) | ||
| 109 | ;; ensure the contents of the new member is expected. | ||
| 110 | (funcall test-modify "test-update" update-fn) | ||
| 111 | ;; test 2: with "test-delete" and "test-delete.zip", delete entry | ||
| 112 | ;; from "test-delete": (1) ensure only "test-delete" is modified, | ||
| 113 | ;; (2) ensure the file list is reduced as expected. | ||
| 114 | (funcall test-modify "test-delete" delete-fn))) | ||
| 115 | |||
| 49 | (provide 'arc-mode-tests) | 116 | (provide 'arc-mode-tests) |
| 50 | 117 | ||
| 51 | ;;; arc-mode-tests.el ends here | 118 | ;;; arc-mode-tests.el ends here |