Repository: conan-io/conan

Issue:
strip_root behaviour with pattern differs between tools.unzip and tools.untargz
### Environment details

Conan version 1.57.0

### Steps to reproduce

Consider an archive with a structure like this

- root_dir/a.txt
- root_dir/b.txt

This is compressed once as .tar.gz and once as .zip. I would expect this command:

`conans.tools.unzip("archive.XXX", pattern="a.txt", strip_root=True)`

to extract a.txt. But this only happens for the .tar.gz archive. For the zip, I need to specify root_dir in the pattern.

Both behaviours can be argued for (though I'd say if I pass strip_root, I have expressed I do not care about the root directory, and do not want to pass its name in my pattern -- that's half the point of the option) but it should certainly be consistent.

### Logs

_No response_


Discussion:
Hi @scummos 

Thanks for your comments.

It seems you are using the legacy ``from conans`` tools. These are no longer recommended and will not get any changes anymore. Please update to the new tools ``from conan.tools.files import xxxx``. In that new scope the ``untargz`` is not exposed anymore, it is internal only, so ``unzip()`` should always be used.
Hi, ok, but is the behaviour actually fixed there? The problem is not with the `untargz` function signature, it is with the logic itself. Calling `unzip` for both archives has the same problem.
> Hi, ok, but is the behaviour actually fixed there?

I don't know, it is possible that the issue is still happening there. But also new tools have fixed tons of issues, so it might be possible that it is fixed too. As legacy tools will not get the fixes anyway, the recommendation is to update to the new tools and report against those. Having a quick look to the new tools, it seems they are using the same ``fnmatch()`` expression for the pattern, both for unzip and untargz, so apparently this should behave the same, and this would need to be further investigated, once confirmed it keeps happening with the new tools. Many thanks!



Patch:
diff --git a/conan/tools/files/files.py b/conan/tools/files/files.py
index b3a05026b57..2828e116826 100644
--- a/conan/tools/files/files.py
+++ b/conan/tools/files/files.py
@@ -224,7 +224,8 @@ def rename(conanfile, src, dst):
     :param dst: Path to be renamed to.
     """
 
-    # FIXME: This function has been copied from legacy. Needs to fix: which() call and wrap subprocess call.
+    # FIXME: This function has been copied from legacy. Needs to fix: which()
+    # call and wrap subprocess call.
     if os.path.exists(dst):
         raise ConanException("rename {} to {} failed, dst exists.".format(src, dst))
 
@@ -323,7 +324,7 @@ def print_progress(_, __):
         if pattern:
             zip_info = [zi for zi in zip_info if fnmatch(zi.filename, pattern)]
         if strip_root:
-            names = [n.replace("\\", "/") for n in z.namelist()]
+            names = [zi.filename.replace("\\", "/") for zi in zip_info]
             common_folder = os.path.commonprefix(names).split("/", 1)[0]
             if not common_folder and len(names) > 1:
                 raise ConanException("The zip file contains more than 1 folder in the root")
@@ -379,8 +380,11 @@ def untargz(filename, destination=".", pattern=None, strip_root=False, extract_f
         else:
             members = tarredgzippedFile.getmembers()
 
+            if pattern:
+                members = list(filter(lambda m: fnmatch(m.name, pattern),
+                                      tarredgzippedFile.getmembers()))
             if strip_root:
-                names = [n.replace("\\", "/") for n in tarredgzippedFile.getnames()]
+                names = [member.name.replace("\\", "/") for member in members]
                 common_folder = os.path.commonprefix(names).split("/", 1)[0]
                 if not common_folder and len(names) > 1:
                     raise ConanException("The tgz file contains more than 1 folder in the root")
@@ -397,9 +401,6 @@ def untargz(filename, destination=".", pattern=None, strip_root=False, extract_f
                         linkpath = member.linkpath.replace("\\", "/")
                         member.linkpath = linkpath.split("/", 1)[1]
                         member.linkname = member.linkpath
-            if pattern:
-                members = list(filter(lambda m: fnmatch(m.name, pattern),
-                                      tarredgzippedFile.getmembers()))
             tarredgzippedFile.extractall(destination, members=members)
 
 


Test patch:
diff --git a/test/unittests/tools/files/test_zipping.py b/test/unittests/tools/files/test_zipping.py
index 42e89bcee20..0c41a494d33 100644
--- a/test/unittests/tools/files/test_zipping.py
+++ b/test/unittests/tools/files/test_zipping.py
@@ -1,6 +1,6 @@
-import os
 import zipfile
-from os.path import basename
+import tarfile
+from os.path import join, exists
 
 import pytest
 
@@ -8,6 +8,7 @@
 from conan.test.utils.mocks import ConanFileMock
 from conan.test.utils.test_files import temp_folder
 from conans.util.files import save
+from conan.errors import ConanException
 
 
 def test_impossible_to_import_untargz():
@@ -15,17 +16,136 @@ def test_impossible_to_import_untargz():
         from conan.tools.files import untargz
 
 
-def test_unzip():
+def create_example_zip(root_file=True, subfolder=False):
     tmp_dir = temp_folder()
-    file_path = os.path.join(tmp_dir, "foo.txt")
-    save(os.path.join(tmp_dir, "foo.txt"), "bar")
-    zf = zipfile.ZipFile(os.path.join(tmp_dir, 'zipfile.zip'), mode='w')
-    zf.write(file_path, basename(file_path))
+    archive = join(tmp_dir, "zipfile.zip")
+    zf = zipfile.ZipFile(archive, mode="w")
+    if root_file:
+        foo_txt = join(tmp_dir, "foo.txt")
+        save(foo_txt, "foo-content")
+        zf.write(foo_txt, "foo.txt")
+    if subfolder:
+        src_bar_txt = join(tmp_dir, "src", "bar.txt")
+        save(src_bar_txt, "bar-content")
+        zf.write(src_bar_txt, join("src", "bar.txt"))
     zf.close()
+    return archive
+
+
+def test_unzip():
+    archive = create_example_zip(subfolder=True)
+    conanfile = ConanFileMock({})
+
+    # Unzip and check permissions are kept
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir)
+    assert exists(join(dest_dir, "foo.txt"))
+    assert exists(join(dest_dir, "src", "bar.txt"))
+
+
+def test_unzip_with_pattern():
+    archive = create_example_zip(subfolder=True)
+    conanfile = ConanFileMock({})
+
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir, pattern="foo.txt")
+    assert exists(join(dest_dir, "foo.txt"))
+    assert not exists(join(dest_dir, "src", "bar.txt"))
+
+
+def test_unzip_with_strip_root():
+    archive = create_example_zip(root_file=False, subfolder=True)
+    conanfile = ConanFileMock({})
+
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir, strip_root=True)
+    assert exists(join(dest_dir, "bar.txt"))
+
+
+def test_unzip_with_strip_root_fails():
+    archive = create_example_zip(root_file=True, subfolder=True)
+    conanfile = ConanFileMock({})
+
+    dest_dir = temp_folder()
+    with pytest.raises(ConanException) as error:
+        unzip(conanfile, archive, dest_dir, strip_root=True)
+    assert "The zip file contains more than 1 folder in the root" in str(error.value)
+
+
+def test_unzip_with_strip_root_and_pattern():
+    archive = create_example_zip(root_file=True, subfolder=True)
+    conanfile = ConanFileMock({})
+
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir, pattern="src/*", strip_root=True)
+    assert exists(join(dest_dir, "bar.txt"))
+    assert not exists(join(dest_dir, "foo.txt"))
+
+
+def create_example_tar(root_file=True, subfolder=False):
+    tmp_dir = temp_folder()
+    tar_path = join(tmp_dir, "file.tgz")
+    tar = tarfile.open(tar_path, "w:gz")
+    if root_file:
+        foo_txt = join(tmp_dir, "foo.txt")
+        save(foo_txt, "foo-content")
+        tar.add(foo_txt, "foo.txt")
+    if subfolder:
+        src_bar_txt = join(tmp_dir, "src", "bar.txt")
+        save(src_bar_txt, "bar-content")
+        tar.add(src_bar_txt, join("src", "bar.txt"))
+    tar.close()
+    return tar_path
+
+
+def test_untargz():
+    archive = create_example_tar(subfolder=True)
+    conanfile = ConanFileMock({})
+
+    # Unzip and check permissions are kept
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir)
+    assert exists(join(dest_dir, "foo.txt"))
+    assert exists(join(dest_dir, "src", "bar.txt"))
+
+
+def test_untargz_with_pattern():
+    archive = create_example_tar(subfolder=True)
+    conanfile = ConanFileMock({})
+
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir, pattern="foo.txt")
+    assert exists(join(dest_dir, "foo.txt"))
+    assert not exists(join(dest_dir, "src", "bar.txt"))
+
+
+def test_untargz_with_strip_root():
+    archive = create_example_tar(root_file=False, subfolder=True)
+    conanfile = ConanFileMock({})
+
+    # Unzip and check permissions are kept
+    dest_dir = temp_folder()
+    unzip(conanfile, archive, dest_dir, strip_root=True)
+    assert exists(join(dest_dir, "bar.txt"))
+
+
+def test_untargz_with_strip_root_fails():
+    archive = create_example_tar(root_file=True, subfolder=True)
+    conanfile = ConanFileMock({})
+
+    # Unzip and check permissions are kept
+    dest_dir = temp_folder()
+    with pytest.raises(ConanException) as error:
+        unzip(conanfile, archive, dest_dir, strip_root=True)
+    assert "The tgz file contains more than 1 folder in the root" in str(error.value)
+
 
+def test_untargz_with_strip_root_and_pattern():
+    archive = create_example_tar(root_file=True, subfolder=True)
     conanfile = ConanFileMock({})
 
     # Unzip and check permissions are kept
     dest_dir = temp_folder()
-    unzip(conanfile, os.path.join(tmp_dir, 'zipfile.zip'), dest_dir)
-    assert os.path.exists(os.path.join(dest_dir, "foo.txt"))
+    unzip(conanfile, archive, dest_dir, pattern="src/*", strip_root=True)
+    assert exists(join(dest_dir, "bar.txt"))
+    assert not exists(join(dest_dir, "foo.txt"))