Repository: PyFilesystem/pyfilesystem2
Pull request #291 (2019-06-02 17:29:28)

Tar pit
## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [x] Tests
- [x] Other

## Checklist

- [x] I've run the latest [black](https://github.com/ambv/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Fixes https://github.com/PyFilesystem/pyfilesystem2/issues/284

Also dropped Python3.3 support to solve Github security warning. We support Python3.4 - Python3.7 and 3.8 is around the corner, so I think its time. Hope there is no pushback to that.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/23745455/badge)](https://coveralls.io/builds/23745455)

Coverage decreased (-0.04%) to 99.571% when pulling **307b1bb20af206218cbf9560e19af918d32c0e80 on tar-pit** into **4e6031fe578adf165b9dda837cbaecfe9ee879ea on master**.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 77cde8f..ad36cf4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,9 +20,6 @@ matrix:
     - python: "3.4"
       env:
         - SETUPTOOLS=setuptools PIP=pip
-    - python: "3.3"
-      env:
-        - SETUPTOOLS=setuptools~=39.2 PIP="pip<=18.0"
 
 before_install:
   - pip install $SETUPTOOLS $PIP -U
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93254f6..122587f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
-## [2.5.6] - Unreleased
+## [2.5.6] - 2019-06-08
 
 ### Added
 
@@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Fixed
 
 - Fixed FTP test suite when time is not UTC-0 @mrg0029
+- Fixed issues with paths in tarfs https://github.com/PyFilesystem/pyfilesystem2/issues/284
+
+### Changed
+
+- Dropped Python3.3 support
 
 ## [2.4.5] - 2019-05-05
 
diff --git a/fs/_version.py b/fs/_version.py
index 8ac8d39..35824d0 100644
--- a/fs/_version.py
+++ b/fs/_version.py
@@ -1,3 +1,3 @@
 """Version, used in module and setup.py.
 """
-__version__ = "2.4.6a0"
+__version__ = "2.4.6"
diff --git a/fs/tarfs.py b/fs/tarfs.py
index fcd7ceb..ce2109c 100644
--- a/fs/tarfs.py
+++ b/fs/tarfs.py
@@ -17,6 +17,7 @@ from . import errors
 from .base import FS
 from .compress import write_tar
 from .enums import ResourceType
+from .errors import IllegalBackReference
 from .info import Info
 from .iotools import RawWrapper
 from .opener import open_fs
@@ -275,14 +276,26 @@ class ReadTarFS(FS):
         self._directory_cache = None
 
     @property
-    def _directory(self):
+    def _directory_entries(self):
         """Lazy directory cache."""
         if self._directory_cache is None:
             _decode = self._decode
-            _directory = ((_decode(info.name).strip("/"), info) for info in self._tar)
-            self._directory_cache = OrderedDict(
-                (name, info) for name, info in _directory if normpath(name)
+            _directory_entries = (
+                (_decode(info.name).strip("/"), info) for info in self._tar
             )
+
+            def _list_tar():
+                for name, info in _directory_entries:
+                    try:
+                        _name = normpath(name)
+                    except IllegalBackReference:
+                        # Back references outside root, must be up to no good.
+                        pass
+                    else:
+                        if _name:
+                            yield _name, info
+
+            self._directory_cache = OrderedDict(_list_tar())
         return self._directory_cache
 
     def __repr__(self):
@@ -327,7 +340,7 @@ class ReadTarFS(FS):
         else:
             try:
                 implicit = False
-                member = self._tar.getmember(self._encode(_path))
+                member = self._directory_entries[_path]
             except KeyError:
                 if not self.isdir(_path):
                     raise errors.ResourceNotFound(path)
@@ -370,14 +383,14 @@ class ReadTarFS(FS):
     def isdir(self, path):
         _path = relpath(self.validatepath(path))
         try:
-            return self._directory[_path].isdir()
+            return self._directory_entries[_path].isdir()
         except KeyError:
-            return any(isbase(_path, name) for name in self._directory)
+            return any(isbase(_path, name) for name in self._directory_entries)
 
     def isfile(self, path):
         _path = relpath(self.validatepath(path))
         try:
-            return self._directory[_path].isfile()
+            return self._directory_entries[_path].isfile()
         except KeyError:
             return False
 
@@ -393,7 +406,9 @@ class ReadTarFS(FS):
         if not self.gettype(path) is ResourceType.directory:
             raise errors.DirectoryExpected(path)
 
-        children = (frombase(_path, n) for n in self._directory if isbase(_path, n))
+        children = (
+            frombase(_path, n) for n in self._directory_entries if isbase(_path, n)
+        )
         content = (parts(child)[1] for child in children if relpath(child))
         return list(OrderedDict.fromkeys(content))
 
@@ -415,7 +430,7 @@ class ReadTarFS(FS):
             raise errors.ResourceReadOnly(path)
 
         try:
-            member = self._tar.getmember(self._encode(_path))
+            member = self._directory_entries[_path]
         except KeyError:
             six.raise_from(errors.ResourceNotFound(path), None)
 
diff --git a/setup.py b/setup.py
index 13a7161..c1bcec3 100644
--- a/setup.py
+++ b/setup.py
@@ -2,30 +2,24 @@
 
 from setuptools import setup, find_packages
 
-with open('fs/_version.py') as f:
+with open("fs/_version.py") as f:
     exec(f.read())
 
 CLASSIFIERS = [
-    'Development Status :: 5 - Production/Stable',
-    'Intended Audience :: Developers',
-    'License :: OSI Approved :: MIT License',
-    'Operating System :: OS Independent',
-    'Programming Language :: Python',
-    'Programming Language :: Python :: 2.7',
-    'Programming Language :: Python :: 3.3',
-    'Programming Language :: Python :: 3.4',
-    'Programming Language :: Python :: 3.5',
-    'Programming Language :: Python :: 3.6',
-    'Programming Language :: Python :: 3.7',
-    'Topic :: System :: Filesystems',
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 2.7",
+    "Programming Language :: Python :: 3.4",
+    "Programming Language :: Python :: 3.5",
+    "Programming Language :: Python :: 3.6",
+    "Programming Language :: Python :: 3.7",
+    "Topic :: System :: Filesystems",
 ]
 
-REQUIREMENTS = [
-    "appdirs~=1.4.3",
-    "pytz",
-    "setuptools",
-    "six~=1.10",
-]
+REQUIREMENTS = ["appdirs~=1.4.3", "pytz", "setuptools", "six~=1.10"]
 
 setup(
     author="Will McGugan",
@@ -34,21 +28,19 @@ setup(
     description="Python's filesystem abstraction layer",
     install_requires=REQUIREMENTS,
     extras_require={
-        "scandir :python_version < '3.5'": ['scandir~=1.5'],
-        ":python_version < '3.4'": ['enum34~=1.1.6'],
-        ":python_version < '3.6'": ['typing~=3.6'],
-        ":python_version < '3.0'": ['backports.os~=0.1']
+        "scandir :python_version < '3.5'": ["scandir~=1.5"],
+        ":python_version < '3.4'": ["enum34~=1.1.6"],
+        ":python_version < '3.6'": ["typing~=3.6"],
+        ":python_version < '3.0'": ["backports.os~=0.1"],
     },
     license="MIT",
-    name='fs',
+    name="fs",
     packages=find_packages(exclude=("tests",)),
-    package_data = {
-        'fs': ['py.typed'],
-    },
+    package_data={"fs": ["py.typed"]},
     zip_safe=False,
-    platforms=['any'],
+    platforms=["any"],
     test_suite="nose.collector",
-    tests_require=['appdirs', 'mock', 'pytz', 'pyftpdlib'],
+    tests_require=["appdirs", "mock", "pytz", "pyftpdlib"],
     url="https://github.com/PyFilesystem/pyfilesystem2",
     version=__version__,
 )


Test patch:
diff --git a/testrequirements.txt b/testrequirements.txt
index 8ac33c1..e465d4c 100644
--- a/testrequirements.txt
+++ b/testrequirements.txt
@@ -2,7 +2,6 @@ appdirs~=1.4.0
 coverage
 mock
 pyftpdlib==1.5.2
-PyYAML<4.1          ;   python_version == '3.3'
 python-coveralls
 pytz==2016.7
 nose
diff --git a/tests/test_tarfs.py b/tests/test_tarfs.py
index 2a1dc82..610c7a3 100644
--- a/tests/test_tarfs.py
+++ b/tests/test_tarfs.py
@@ -188,7 +188,7 @@ class TestReadTarFS(ArchiveTestCases, unittest.TestCase):
         self.assertTrue(top.get("tar", "is_file"))
 
 
-class TestBrokenDir(unittest.TestCase):
+class TestBrokenPaths(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         cls.tmpfs = open_fs("temp://tarfstest")
@@ -200,7 +200,8 @@ class TestBrokenDir(unittest.TestCase):
     def setUp(self):
         self.tempfile = self.tmpfs.open("test.tar", "wb+")
         with tarfile.open(mode="w", fileobj=self.tempfile) as tf:
-            tf.addfile(tarfile.TarInfo("."), io.StringIO)
+            tf.addfile(tarfile.TarInfo("."), io.StringIO())
+            tf.addfile(tarfile.TarInfo("../foo.txt"), io.StringIO())
         self.tempfile.seek(0)
         self.fs = tarfs.TarFS(self.tempfile)
 
@@ -228,8 +229,8 @@ class TestImplicitDirectories(unittest.TestCase):
         self.tempfile = self.tmpfs.open("test.tar", "wb+")
         with tarfile.open(mode="w", fileobj=self.tempfile) as tf:
             tf.addfile(tarfile.TarInfo("foo/bar/baz/spam.txt"), io.StringIO())
-            tf.addfile(tarfile.TarInfo("foo/eggs.bin"), io.StringIO())
-            tf.addfile(tarfile.TarInfo("foo/yolk/beans.txt"), io.StringIO())
+            tf.addfile(tarfile.TarInfo("./foo/eggs.bin"), io.StringIO())
+            tf.addfile(tarfile.TarInfo("./foo/yolk/beans.txt"), io.StringIO())
             info = tarfile.TarInfo("foo/yolk")
             info.type = tarfile.DIRTYPE
             tf.addfile(info, io.BytesIO())