Repository: PyFilesystem/pyfilesystem2
Pull request #100 (2017-10-28 17:51:01)

Seek files and extract permissions from `ZipFS`
## About #98

The objects returned by `ZipFile.open` do not support seeking, but since they use an internal buffer we can use that buffer to our advantage. So here's what I did:

* `_ZipExtFile.seek` will try to use the internal zip buffer as much as possible, by manually changing the position of the internal file object in the buffer. If it's not possible, then: 
    - if we're seeking backward, we will have to open the file one more time (like `FTPFile` does)
    - if we're seeking forward, we read and discard until we reach the desired position
* `_ZipExtFile.read` and `_ZipExtFile.read1` were patched to prevent the internal file object from discarding the read buffer on EOF. Without this fudge, we would have to reopen the file each time the EOF is reached, even for a `_ZipExtFile.seek(-1, Seek.end)`.
* `_ZipExtFile.tell` will return the position of the file.

While I was at it, I implemented #60 as well :smile: : 

## About #60 

* When reading a zip file, permissions are extracted (but nothing else, since Zip files do not store owner / group of the archive), **providing that the zip file was created on a Unix system**, and that there are some permissions to extract.
* When writing a zip file, permissions from the `"access"` namespace are written (as well as the directory bit to indicate whether the zip entry is a file or a directory). If there are no permissions to write, `zip_info.external_attr` is left as default (`0`).

I also reorganized `ReadZipFs.getinfo` to only return the required namespaces, meaning the call to `ZipFile.getinfo` will be skipped if possible, and then did the same thing for `ReadTarFs` for the archives filesystems to be more consistent.

## People this may interest
* @willmcgugan 😄 
* @rkhwaja  
* @mashabow 


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

Coverage decreased (-0.9%) to 99.034% when pulling **65041170c42a66544cafd918f4721c31a9bd9473 on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage decreased (-0.1%) to 99.827% when pulling **2bc3ef99dfe1e4054c9e19758932b358be381fe5 on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage increased (+0.0007%) to 99.951% when pulling **cb2391201611b008a6be508387448f6e44eef3ed on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage decreased (-0.1%) to 99.852% when pulling **205e83214e0e975c7b09217c3ce60e32791d110a on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage decreased (-0.2%) to 99.779% when pulling **744fa4a3500005423d34de47d318dec3b4977317 on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage increased (+0.001%) to 99.951% when pulling **d8bf1c4bb36d4b9ac5502a8fde4b637f0ba688ec on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

willmcgugan: Excellent. :) I'll have a quick scan.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13944238/badge)](https://coveralls.io/builds/13944238)

Coverage increased (+0.001%) to 99.951% when pulling **05145ef789c00323b5e1fb760e2cfda0ec0eab8d on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

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

Coverage increased (+0.001%) to 99.951% when pulling **dc4f76bfecb4a14b5f18605e3003262837005cc2 on althonos:feat-zipfile** into **b8cc82be593b7c9dd203c96a08d99053668ed844 on PyFilesystem:master**.

althonos: @willmcgugan : clear to merge.
willmcgugan: Thanks. Will do a release soon.

Patch:
diff --git a/fs/compress.py b/fs/compress.py
index c958581..93fd619 100644
--- a/fs/compress.py
+++ b/fs/compress.py
@@ -18,7 +18,7 @@ import six
 from .enums import ResourceType
 from .path import relpath
 from .time import datetime_to_epoch
-from .errors import NoSysPath
+from .errors import NoSysPath, MissingInfoNamespace
 from .walk import Walker
 
 
@@ -51,7 +51,8 @@ def write_zip(src_fs,
     )
     walker = walker or Walker()
     with _zip:
-        gen_walk = walker.info(src_fs, namespaces=["details", "stat"])
+        gen_walk = walker.info(
+            src_fs, namespaces=["details", "stat", "access"])
         for path, info in gen_walk:
             # Zip names must be relative, directory names must end
             # with a slash.
@@ -76,7 +77,13 @@ def write_zip(src_fs,
                 )
             zip_info = zipfile.ZipInfo(zip_name, zip_time)
 
+            try:
+                zip_info.external_attr = info.permissions.mode << 16
+            except MissingInfoNamespace:
+                pass
+
             if info.is_dir:
+                zip_info.external_attr |= 0x10
                 # This is how to record directories with zipfile
                 _zip.writestr(zip_info, b'')
             else:
diff --git a/fs/tarfs.py b/fs/tarfs.py
index 95b1b42..24f85a7 100644
--- a/fs/tarfs.py
+++ b/fs/tarfs.py
@@ -18,6 +18,16 @@ from .path import dirname, normpath, relpath, basename
 from .wrapfs import WrapFS
 from .permissions import Permissions
 
+__all__ = ['TarFS', 'WriteTarFS', 'ReadTarFS']
+
+
+if six.PY2:
+    def _get_member_info(member, encoding):
+        return member.get_info(encoding, None)
+else:
+    def _get_member_info(member, encoding):
+        return member.get_info()
+
 
 class TarFS(WrapFS):
     """Read and write tar files.
@@ -204,59 +214,53 @@ class ReadTarFS(FS):
         return "<TarFS '{}'>".format(self._file)
 
     def getinfo(self, path, namespaces=None):
-        self.check()
+        _path = relpath(self.validatepath(path))
         namespaces = namespaces or ()
-        path = relpath(normpath(path))
-        if not path:
-            raw_info = {
-                "basic":
-                {
-                    "name": "",
-                    "is_dir": True,
-                },
-                "details":
-                {
+        raw_info = {}
+
+        if not _path:
+            raw_info['basic'] = {
+                "name": "",
+                "is_dir": True,
+            }
+            if 'details' in namespaces:
+                raw_info['details'] = {
                     "type": int(ResourceType.directory)
                 }
-            }
+
         else:
             try:
-                member = self._tar.getmember(path)
+                member = self._tar.getmember(_path)
             except KeyError:
                 raise errors.ResourceNotFound(path)
 
+            raw_info["basic"] = {
+                "name": basename(member.name),
+                "is_dir": member.isdir(),
+            }
 
-            raw_tar_info = member.get_info(*(
-                [self.encoding, None] if six.PY2 else []
-            ))
-
-            raw_tar_info.update({
-                k.replace('is', 'is_'):getattr(member, k)()
-                for k in dir(member)
-                if k.startswith('is')
-            })
-            raw_info = {
-                "basic":
-                {
-                    "name": basename(member.name),
-                    "is_dir": member.isdir(),
-                },
-                "details":
-                {
+            if "details" in namespaces:
+                raw_info["details"] = {
                     "size": member.size,
                     "type": int(self.type_map[member.type]),
                     "modified": member.mtime,
-                },
-                "access":
-                {
+                }
+            if "access" in namespaces:
+                raw_info["access"] = {
                     "gid": member.gid,
                     "group": member.gname,
                     "permissions": Permissions(mode=member.mode).dump(),
                     "uid": member.uid,
                     "user": member.uname,
-                },
-                "tar": raw_tar_info,
-            }
+                }
+            if "tar" in namespaces:
+                raw_info["tar"] = _get_member_info(member, self.encoding)
+                raw_info["tar"].update({
+                    k.replace('is', 'is_'):getattr(member, k)()
+                    for k in dir(member)
+                    if k.startswith('is')
+                })
+
         return Info(raw_info)
 
     def setinfo(self, path, info):
diff --git a/fs/zipfs.py b/fs/zipfs.py
index b1d122d..b77e43f 100644
--- a/fs/zipfs.py
+++ b/fs/zipfs.py
@@ -4,17 +4,20 @@
 from __future__ import print_function
 from __future__ import unicode_literals
 
-from datetime import datetime
 import zipfile
+import stat
+
+from datetime import datetime
 
 import six
 
 from . import errors
 from .base import FS
 from .compress import write_zip
-from .enums import ResourceType
+from .enums import ResourceType, Seek
 from .info import Info
 from .iotools import RawWrapper
+from .permissions import Permissions
 from .memoryfs import MemoryFS
 from .opener import open_fs
 from .path import dirname, normpath, relpath
@@ -22,6 +25,115 @@ from .time import datetime_to_epoch
 from .wrapfs import WrapFS
 
 
+class _ZipExtFile(RawWrapper):
+
+    def __init__(self, fs, name):
+        self._zip = _zip = fs._zip
+        self._end = _zip.getinfo(name).file_size
+        self._pos = 0
+        super(_ZipExtFile, self).__init__(_zip.open(name), 'r', name)
+
+    def read(self, size=-1):
+        if self._pos >= self._end:
+            return b''
+        elif size is None or size < 0:
+            size = self._end - self._pos
+            # NB(@althonos): do NOT replace by self._f.read() !
+            buf = self._f.read(size-1) + self._f._readbuffer[-1:]
+            self._f._offset += 1
+        elif self._f._offset + size <= len(self._f._readbuffer):
+            buf = self._f._readbuffer[self._f._offset:size+self._f._offset]
+            self._f._offset += size
+        else:
+            buf = self._f.read(size)
+        self._pos += len(buf)
+        return buf
+
+    def read1(self, size=-1):
+        if self._pos >= self._end:
+            return b''
+        if size is None or size < 0:
+            size = self._end - self._pos
+            # NB(@althonos): do NOT replace by self._f.read1() !
+            buf = self._f.read1(size-1) + self._f._readbuffer[-1:]
+            self._f._offset += 1
+        elif self._f._offset + size <= len(self._f._readbuffer):
+            buf = self._f._readbuffer[self._f._offset:size+self._f._offset]
+            self._f._offset += size
+        else:
+            buf = self._f.read1(size)
+        self._pos += len(buf)
+        return buf
+
+    def seek(self, offset, whence=Seek.set):
+        """Change stream position.
+
+        Change the stream position to the given byte offset. The
+        offset is interpreted relative to the position indicated by
+        ``whence``.
+
+        Arguments:
+            offset (int): the offset to the new position, in bytes.
+            whence (int): the position reference. Possible values are:
+                * `Seek.set`: start of stream (the default).
+                * `Seek.current`: current position; offset may be negative.
+                * `Seek.end`: end of stream; offset must be negative.
+
+        Returns:
+            int: the new absolute position.
+
+        Raises:
+            ValueError: when ``whence`` is not known, or ``offset``
+                is invalid.
+
+        Note:
+            Zip compression does not support seeking, so the seeking
+            is emulated. The internal decompression buffer will be used
+            as much as possible, but sometimes it way be necessary to:
+                * reopen the file and restart decompression
+                * read and discard data to advance in the file
+
+            The size of the zip buffer can be changed by setting the
+            `zipfile.ZipExtFile.MIN_READ_SIZE` attribute.
+
+        """
+        if whence == Seek.set:
+            if offset < 0:
+                raise ValueError("Negative seek position {}".format(offset))
+            elif offset >= self._pos:
+                self.seek(offset - self._pos, Seek.current)
+            else:
+                self._f = self._zip.open(self.name)
+                self._pos = 0
+                self.seek(offset, Seek.set)
+        elif whence == Seek.current:
+            if offset > 0:
+                if self._f._offset + offset < len(self._f._readbuffer):
+                    self._f._offset += offset
+                else:
+                    self._f.read(offset)
+                self._pos += offset
+            elif self._f._offset + offset >= 0:
+                self._f._offset += offset
+                self._pos += offset
+            else:
+                self.seek(self._pos + offset, Seek.set)
+        elif whence == Seek.end:
+            if offset > 0:
+                raise ValueError("Positive seek position {}".format(offset))
+            self.seek(self._end + offset, Seek.set)
+        else:
+            raise ValueError(
+                "Invalid whence ({}, should be {}, {} or {})".format(
+                    whence, Seek.set, Seek.current, Seek.end
+                )
+            )
+        return self._pos
+
+    def tell(self):
+        return self._pos
+
+
 class ZipFS(WrapFS):
     """Read and write zip files.
 
@@ -207,63 +319,62 @@ class ReadZipFS(FS):
             return self._directory_fs
 
     def getinfo(self, path, namespaces=None):
-        self.check()
+        _path = self.validatepath(path)
         namespaces = namespaces or ()
-        _path = normpath(path)
+        raw_info = {}
+
         if _path == '/':
-            raw_info = {
-                "basic":
-                {
-                    "name": "",
-                    "is_dir": True,
-                },
-                "details":
-                {
+            raw_info["basic"] = {
+                "name": "",
+                "is_dir": True,
+            }
+            if "details" in namespaces:
+                raw_info["details"] = {
                     "type": int(ResourceType.directory)
                 }
-            }
+
         else:
             basic_info = self._directory.getinfo(_path)
-            zip_name = self._path_to_zip_name(path)
-            try:
-                zip_info = self._zip.getinfo(zip_name)
-            except KeyError:
-                # Can occur if there is an implied directory in the zip
-                raw_info = {
-                    "basic":
-                    {
-                        "name": basic_info.name,
-                        "is_dir": basic_info.is_dir
-                    }
-                }
-            else:
-                modified_epoch = datetime_to_epoch(
-                    datetime(*zip_info.date_time)
-                )
-                raw_zip_info = {
-                    k: getattr(zip_info, k)
-                    for k in dir(zip_info)
-                    if (not k.startswith('_') and
-                        not callable(getattr(zip_info, k)))
-                }
-                raw_info = {
-                    "basic":
-                    {
-                        "name": basic_info.name,
-                        "is_dir": basic_info.is_dir,
-                    },
-                    "details":
-                    {
-                        "size": zip_info.file_size,
-                        "type": int(
-                            ResourceType.directory
-                            if basic_info.is_dir else
-                            ResourceType.file
-                        ),
-                        "modified": modified_epoch
-                    },
-                    "zip": raw_zip_info
-                }
+            raw_info["basic"] = {
+                "name": basic_info.name,
+                "is_dir": basic_info.is_dir,
+            }
+
+            if not {"details", "access", "zip"}.isdisjoint(namespaces):
+                zip_name = self._path_to_zip_name(path)
+                try:
+                    zip_info = self._zip.getinfo(zip_name)
+                except KeyError:
+                    # Can occur if there is an implied directory in the zip
+                    pass
+                else:
+                    if "details" in namespaces:
+                        raw_info["details"] = {
+                            "size": zip_info.file_size,
+                            "type": int(
+                                ResourceType.directory
+                                if basic_info.is_dir else
+                                ResourceType.file
+                            ),
+                            "modified": datetime_to_epoch(
+                                datetime(*zip_info.date_time)
+                            )
+                        }
+                    if "zip" in namespaces:
+                        raw_info["zip"] = {
+                            k: getattr(zip_info, k)
+                            for k in zip_info.__slots__
+                            if not k.startswith('_')
+                        }
+                    if "access" in namespaces:
+                        # check the zip was created on UNIX to get permissions
+                        if zip_info.external_attr \
+                                and zip_info.create_system == 3:
+                            raw_info["access"] = {
+                                "permissions": Permissions(
+                                    mode=zip_info.external_attr >> 16 & 0xFFF
+                                ).dump(),
+                            }
 
         return Info(raw_info)
 
@@ -290,8 +401,7 @@ class ReadZipFS(FS):
             raise errors.FileExpected(path)
 
         zip_name = self._path_to_zip_name(path)
-        bin_file = self._zip.open(zip_name, 'r')
-        return RawWrapper(bin_file)
+        return _ZipExtFile(self, zip_name)
 
     def remove(self, path):
         self.check()


Test patch:
diff --git a/tests/test_archives.py b/tests/test_archives.py
index 2c5b1b2..3b85b72 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -3,8 +3,10 @@ from __future__ import unicode_literals
 from six import text_type
 
 from fs.opener import open_fs
+from fs.enums import ResourceType
 from fs import walk
 from fs import errors
+from fs.memoryfs import MemoryFS
 from fs.test import UNICODE_TEXT
 
 
@@ -61,13 +63,22 @@ class ArchiveTestCases(object):
             self.fs.setinfo('foo.txt', {})
 
     def test_getinfo(self):
-        root = self.fs.getinfo('/')
+        root = self.fs.getinfo('/', ["details"])
         self.assertEqual(root.name, '')
         self.assertTrue(root.is_dir)
+        self.assertEqual(root.get('details', 'type'), ResourceType.directory)
 
-        top = self.fs.getinfo('top.txt', 'details')
+        bar = self.fs.getinfo('foo/bar', ['details'])
+        self.assertEqual(bar.name, 'bar')
+        self.assertTrue(bar.is_dir)
+        self.assertEqual(bar.get('details', 'type'), ResourceType.directory)
+
+        top = self.fs.getinfo('top.txt', ['details', 'access'])
         self.assertEqual(top.size, 12)
         self.assertFalse(top.is_dir)
+        if not isinstance(self.source_fs, MemoryFS):
+            self.assertEqual(top.permissions.mode, 0o644)
+        self.assertEqual(top.get('details', 'type'), ResourceType.file)
 
     def test_listdir(self):
         self.assertEqual(
@@ -130,4 +141,3 @@ class ArchiveTestCases(object):
     def test_implied_dir(self):
         self.fs.getinfo('foo/bar')
         self.fs.getinfo('foo')
-
diff --git a/tests/test_tarfs.py b/tests/test_tarfs.py
index 3860d44..bd763e5 100644
--- a/tests/test_tarfs.py
+++ b/tests/test_tarfs.py
@@ -4,6 +4,7 @@ import os
 import six
 import gzip
 import tarfile
+import getpass
 import tempfile
 import unittest
 
@@ -153,7 +154,13 @@ class TestReadTarFS(ArchiveTestCases, unittest.TestCase):
         try:
             tarfs.TarFS(self._temp_path)
         except:
-            self.fail("Couldn't open tarfs from fileobject")
+            self.fail("Couldn't open tarfs from filename")
+
+    def test_getinfo(self):
+        super(TestReadTarFS, self).test_getinfo()
+        top = self.fs.getinfo('top.txt', ['tar'])
+        self.assertTrue(top.get('tar', 'is_file'))
+
 
 class TestReadTarFSMem(TestReadTarFS):
 
diff --git a/tests/test_zipfs.py b/tests/test_zipfs.py
index 9d3c69c..5e627b2 100644
--- a/tests/test_zipfs.py
+++ b/tests/test_zipfs.py
@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 import os
+import sys
 import tempfile
 import unittest
 import zipfile
@@ -10,6 +11,7 @@ from fs.compress import write_zip
 from fs.opener import open_fs
 from fs.opener.errors import NotWriteable
 from fs.test import FSTestCases
+from fs.enums import Seek, ResourceType
 
 from .test_archives import ArchiveTestCases
 
@@ -49,6 +51,82 @@ class TestReadZipFS(ArchiveTestCases, unittest.TestCase):
     def remove_archive(self):
         os.remove(self._temp_path)
 
+    def test_getinfo(self):
+        super(TestReadZipFS, self).test_getinfo()
+        top = self.fs.getinfo('top.txt', ['zip'])
+        if sys.platform in ('linux', 'darwin'):
+            self.assertEqual(top.get('zip', 'create_system'), 3)
+
+    def test_openbin(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.name, 'top.txt')
+        with self.fs.openbin('top.txt') as f:
+            self.assertRaises(ValueError, f.seek, -2, Seek.set)
+        with self.fs.openbin('top.txt') as f:
+            self.assertRaises(ValueError, f.seek, 2, Seek.end)
+        with self.fs.openbin('top.txt') as f:
+            self.assertRaises(ValueError, f.seek, 0, 5)
+
+    def test_read(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read(), b'Hello, World')
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read(5), b'Hello')
+            self.assertEqual(f.read(7), b', World')
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read(12), b'Hello, World')
+
+    def test_read1(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read1(), b'Hello, World')
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read1(5), b'Hello')
+            self.assertEqual(f.read1(7), b', World')
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.read1(12), b'Hello, World')
+
+    def test_seek_set(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.tell(), 0)
+            self.assertEqual(f.read(), b'Hello, World')
+            self.assertEqual(f.tell(), 12)
+            self.assertEqual(f.read(), b'')
+            self.assertEqual(f.tell(), 12)
+            self.assertEqual(f.seek(0), 0)
+            self.assertEqual(f.tell(), 0)
+            self.assertEqual(f.read1(), b'Hello, World')
+            self.assertEqual(f.tell(), 12)
+            self.assertEqual(f.seek(1), 1)
+            self.assertEqual(f.tell(), 1)
+            self.assertEqual(f.read(), b'ello, World')
+            self.assertEqual(f.tell(), 12)
+            self.assertEqual(f.seek(7), 7)
+            self.assertEqual(f.tell(), 7)
+            self.assertEqual(f.read(), b'World')
+            self.assertEqual(f.tell(), 12)
+
+    def test_seek_current(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.tell(), 0)
+            self.assertEqual(f.read(5), b'Hello')
+            self.assertEqual(f.tell(), 5)
+            self.assertEqual(f.seek(2, Seek.current), 7)
+            self.assertEqual(f.read1(), b'World')
+            self.assertEqual(f.tell(), 12)
+            self.assertEqual(f.seek(-1, Seek.current), 11)
+            self.assertEqual(f.read(), b'd')
+        with self.fs.openbin('top.txt') as f:
+            self.assertRaises(ValueError, f.seek, -1, Seek.current)
+
+    def test_seek_end(self):
+        with self.fs.openbin('top.txt') as f:
+            self.assertEqual(f.tell(), 0)
+            self.assertEqual(f.seek(-12, Seek.end), 0)
+            self.assertEqual(f.read1(5), b'Hello')
+            self.assertEqual(f.seek(-7, Seek.end), 5)
+            self.assertEqual(f.seek(-5, Seek.end), 7)
+            self.assertEqual(f.read(), b'World')
+
 
 class TestReadZipFSMem(TestReadZipFS):
 
@@ -63,15 +141,14 @@ class TestDirsZipFS(unittest.TestCase):
         fh, path = tempfile.mkstemp('testzip.zip')
         try:
             os.close(fh)
-            _zip_file = zipfile.ZipFile(path, mode='w')
-            _zip_file.writestr('foo/bar/baz/egg', b'hello')
-            _zip_file.close()
-            zip_fs = zipfs.ZipFS(path)
-            zip_fs.getinfo('foo')
-            zip_fs.getinfo('foo/bar')
-            zip_fs.getinfo('foo/bar/baz')
-            self.assertTrue(zip_fs.isdir('foo/bar/baz'))
-            self.assertTrue(zip_fs.isfile('foo/bar/baz/egg'))
+            with zipfile.ZipFile(path, mode='w') as z:
+                z.writestr('foo/bar/baz/egg', b'hello')
+            with zipfs.ReadZipFS(path) as zip_fs:
+                foo = zip_fs.getinfo('foo', ['details'])
+                bar = zip_fs.getinfo('foo/bar')
+                baz = zip_fs.getinfo('foo/bar/baz')
+                self.assertTrue(foo.is_dir)
+                self.assertTrue(zip_fs.isfile('foo/bar/baz/egg'))
         finally:
             os.remove(path)
 
@@ -81,4 +158,3 @@ class TestOpener(unittest.TestCase):
     def test_not_writeable(self):
         with self.assertRaises(NotWriteable):
             open_fs('zip://foo.zip', writeable=True)
-