Subject: bug#47058: 28.0.50; Dired Z: insert-directory: Reading directory: No such file or directory, CrossLine_linux_x86

From: "Michalis V." <[email-masked]>
Date: Tue, 21 Sep 2021 01:12:10 +0300

Marco Centurion - URI <[email-masked]> writes:

> Arthur Miller <[email-masked]> writes:
>
>> Marco Centurion <[email-masked]> writes:
>>
>>> I can confirm that this bug is present and pretty easy to reproduce.
>>> Steps to reproduce:
>>>
>>> ----
>>> $ touch test1
>>> $ tar czf test.tar.gz test1
>>> $ rm test1
>>>
>>> In dired, press Z (`dired-do-compress`) when the point is on test.tar.gz
>>> ----
>> I just did all those steps in emacs -Q, and I can not confirm any errors. I
>> named files exactly as you show there, and decompressed file is correctly named
>> 'test1'.
>>
>> I tested in two different directories, you three shell commands from terminal
>> (st in my case), and Z from dired created correctly test1.
>>
>
> Yes, the file is correctly decompressed.  The original report is about
> an error message that shows up in the minibuffer "Reading directory: No
> such file or directory, test".  And that's what I was able to reproduce
> without having to download the file linked in the report, something that
> I guess a lot of people wouldn't want to do.
>
>> Press 'g'.
>
> I do that, I just thought that it's a bit of a weird behaviour that
> sometimes after pressing Z the dired buffer is consistent with what's
> actually in the directory and sometimes it's not.  That is all.
> Personally I wouldn't be opposed to reverting the buffer after every
> operation that modifies files, but I'm sure most would and with good
> reason.

hi,

i've had a look into this some weeks ago and only just now managed to
assemble something presentable. As Marco correctly noted, the
problem is not with just a single file - any .tar.[gz|xz|zst|bz2] that
contains just files (no directories) will give the reported error.
The culprit is what 'dired-compress' expects: it assumes that the
uncompression result will produce just a single file or directory.

For example a test-file.gz will produce test-file, emacs-27.2.tar.gz
will give emacs-27.2/ etc. A special case is with formats that support
output directory like zip: boing.zip will be extracted to boing/
no-questions-asked. But for tars like this:

for i in a b c;do touch $i;done
tar cvzf abc.tar.gz a b c

doing Z on abc.tar.gz causes 'dired-compress' to expect abc but the
result is 3 new files a b c (and thus the error is shown). I tried to
think of some way to fix this and i've ended up with the attached
patch. What it does is to basically ask the user where to extract the
contents of the archive (even for zip files, so that the Z behavior is
somehow similar). To make this work for tar files i've added the -C
parameter in 'dired-compress-file-suffixes/. I've also added a missing
.tar.bz2 suffix (until now .tar.bz2 would just be decompressed & not
extracted).

The main work is done on a new function 'dired-uncompress-file' which
contains part of the code of 'dired-compress-file' (which handles both
compress/uncompress actions).
I thought it would be cleaner to have separate functions for these two
(and perhaps the latter function should be renamed to something better)
i've also added some new tests for .tar.gz and .zip formats.

the big downside of this patch is that it adds another prompt when
pressing Z: User must now enter the extraction directory (for file
/tmp/test.tar.gz the suggested default will be /tmp/test/). And that
behavior change might step on some people's toes so i'm a bit reserved
if this is the correct approach to solving the particular problem.

finally there's a corner case that is not solved: in the above scenario
with abc.tar.gz when you uncompress you still need to hit 'g' to refresh
the dired buffer (but there's no error anymore so at least that's
something). A fix for this would require some refactoring on what
'dired-compress' expects, perhaps make it expect a list of
files/directories and not just a single one, plus some more thinking into
the 'dired-compress-file' compression part.

thanks,
Michalis

diff --git a/lisp/dired-aux.el b/lisp/dired-aux.el
index c728642917..2d4269daed 100644
--- a/lisp/dired-aux.el
+++ b/lisp/dired-aux.el
@@ -1134,9 +1134,10 @@ dired-compress-file-suffixes
     ;; "tar -zxf" isn't used because it's not available on the
     ;; Solaris 10 version of tar (obsolete in 2024?).
     ;; Same thing on AIX 7.1 (obsolete 2023?) and 7.2 (obsolete 2022?).
-    ("\\.tar\\.gz\\'" "" "gzip -dc %i | tar -xf -")
-    ("\\.tar\\.xz\\'" "" "xz -dc %i | tar -xf -")
-    ("\\.tgz\\'" "" "gzip -dc %i | tar -xf -")
+    ("\\.tar\\.gz\\'" "" "gzip -dc %i | tar -xf - -C %c")
+    ("\\.tar\\.xz\\'" "" "xz -dc %i | tar -xf - -C %c")
+    ("\\.tgz\\'" "" "gzip -dc %i | tar -xf - -C %c")
+    ("\\.tar\\.bz2\\'" "" "bunzip2 -c %i | tar -xf - -C %c")
     ("\\.gz\\'" "" "gzip -d")
     ("\\.lz\\'" "" "lzip -d")
     ("\\.Z\\'" "" "uncompress")
@@ -1148,8 +1149,8 @@ dired-compress-file-suffixes
     ("\\.bz2\\'" "" "bunzip2")
     ("\\.xz\\'" "" "unxz")
     ("\\.zip\\'" "" "unzip -o -d %o %i")
-    ("\\.tar\\.zst\\'" "" "unzstd -c %i | tar -xf -")
-    ("\\.tzst\\'" "" "unzstd -c %i | tar -xf -")
+    ("\\.tar\\.zst\\'" "" "unzstd -c %i | tar -xf - -C %c")
+    ("\\.tzst\\'" "" "unzstd -c %i | tar -xf - -C %c")
     ("\\.zst\\'" "" "unzstd --rm")
     ("\\.7z\\'" "" "7z x -aoa -o%o %i")
     ;; This item controls naming for compression.
@@ -1253,6 +1254,42 @@ dired-do-compress-to
                       (length in-files)
                       (file-name-nondirectory out-file)))))))
 
+;;;###autoload
+(defun dired-uncompress-file (file dirname command)
+  "Uncompress FILE using COMMAND. If file is a tar archive or some other
+format that supports output directory in its parameters, ask user the
+target directory to extract it (defaults to DIRNAME). Returns the
+directory or filename produced after the uncompress operation."
+  (if (string-match "%[ioc]" command)
+      (let ((extractdir (expand-file-name
+                         (read-file-name
+                          (format "Extract file to (default %s): " dirname)
+                          dirname))))
+        (prog1 (file-name-as-directory extractdir)
+          (when (not (file-directory-p extractdir))
+            (dired-create-directory extractdir))
+          (dired-shell-command
+           (replace-regexp-in-string
+            "%[oc]" (shell-quote-argument extractdir)
+            (replace-regexp-in-string
+             "%i" (shell-quote-argument file)
+             command
+             nil t)
+            nil t))))
+    ;; We found an uncompression rule without output dir argument
+    (let ((match (string-search " " command))
+          (msg (concat "Uncompressing " file)))
+      (unless (if match
+                  (dired-check-process
+                   msg
+                   (substring command 0 match)
+                   (substring command (1+ match))
+                   file)
+                (dired-check-process msg
+                                     command
+                                     file))
+        dirname))))
+
 ;;;###autoload
 (defun dired-compress-file (file)
   "Compress or uncompress FILE.
@@ -1277,28 +1314,7 @@ dired-compress-file
           ((file-symlink-p file)
            nil)
           ((and suffix (setq command (nth 2 suffix)))
-           (if (string-match "%[io]" command)
-               (prog1 (setq newname (file-name-as-directory newname))
-                 (dired-shell-command
-                  (replace-regexp-in-string
-                   "%o" (shell-quote-argument newname)
-                   (replace-regexp-in-string
-                    "%i" (shell-quote-argument file)
-                    command
-                    nil t)
-                   nil t)))
-             ;; We found an uncompression rule.
-             (let ((match (string-search " " command))
-                   (msg (concat "Uncompressing " file)))
-               (unless (if match
-                           (dired-check-process msg
-                                                (substring command 0 match)
-                                                (substring command (1+ match))
-                                                file)
-                         (dired-check-process msg
-                                              command
-                                              file))
-                 newname))))
+           (dired-uncompress-file file newname command))
           (t
            ;; We don't recognize the file as compressed, so compress it.
            ;; Try gzip; if we don't have that, use compress.
diff --git a/test/lisp/dired-aux-tests.el b/test/lisp/dired-aux-tests.el
index 7f1743f88d..5888f4cd99 100644
--- a/test/lisp/dired-aux-tests.el
+++ b/test/lisp/dired-aux-tests.el
@@ -158,5 +158,59 @@ dired-test-highlight-metachar
     (should (string-match (regexp-quote command) (nth 0 lines)))
     (dired-test--check-highlighting (nth 0 lines) '(8))))
 
+(ert-deftest dired-test-bug47058-tar ()
+  "test for https://debbugs.gnu.org/47058 ."
+  (dired-test-bug47058-fn "tar -cf - %i | gzip -c9 > %o"
+                           "gzip -dc %i | tar -xf - -C %c"
+                           ".tar.gz"))
+
+(ert-deftest dired-test-bug47058-zip ()
+  "test for https://debbugs.gnu.org/47058 ."
+  (dired-test-bug47058-fn "zip %o -r --filesync %i"
+                           "unzip -o -d %o %i"
+                           ".zip"))
+
+(defun dired-test-bug47058-fn (compress-cmd uncompress-cmd extension)
+  "helper fn for testing https://debbugs.gnu.org/47058 ."
+  (let* ((base-file (make-temp-file "dired-test-47058-"))
+         (archive-file (concat base-file extension))
+         (file1 (make-temp-file "a"))
+         (file2 (make-temp-file "b"))
+         (file3 (make-temp-file "c"))
+         (filelist (list file1 file2 file3))
+         (comprcmd (replace-regexp-in-string
+                  "%c" (shell-quote-argument temporary-file-directory)
+                  (replace-regexp-in-string
+                   "%i" (mapconcat 'identity filelist " ")
+                   (replace-regexp-in-string
+                    "%o" (shell-quote-argument archive-file)
+                    compress-cmd)))))
+    (cl-letf (((symbol-function 'read-file-name)
+               (lambda (&rest _) base-file)))
+      (dired-delete-file base-file)
+      (should-not (file-exists-p base-file))
+      (should-not (file-exists-p archive-file))
+      (dired-shell-command comprcmd)
+      (should (file-exists-p archive-file))
+      (mapcar (lambda (f) (should (file-exists-p f)))
+              filelist)
+      (mapcar (lambda (f) (delete-file f))
+              filelist)
+      (mapcar (lambda (f) (should-not (file-exists-p f)))
+              filelist)
+      (should (string-equal
+               (dired-uncompress-file archive-file
+                                      base-file
+                                      uncompress-cmd)
+               (file-name-as-directory base-file)))
+      (mapcar (lambda (f)
+                (should (file-exists-p
+                         (concat (file-name-as-directory base-file) f))))
+              filelist)
+      (dired-delete-file base-file 'always' nil)
+      (dired-delete-file archive-file 'always' nil)
+      (should-not (file-exists-p base-file))
+      (should-not (file-exists-p archive-file)))))
+
 (provide 'dired-aux-tests)
 ;; dired-aux-tests.el ends here