1"""
2A helper module that can work with paths
3that can refer to data inside a zipfile
4
5XXX: Need to determine if isdir("zipfile.zip")
6should return True or False. Currently returns
7True, but that might do the wrong thing with
8data-files that are zipfiles.
9"""
10import os as _os
11import zipfile as _zipfile
12import errno as _errno
13import time as _time
14import sys as _sys
15import stat as _stat
16
17_DFLT_DIR_MODE = (
18    _stat.S_IXOTH
19    | _stat.S_IXGRP
20    | _stat.S_IXUSR
21    | _stat.S_IROTH
22    | _stat.S_IRGRP
23    | _stat.S_IRUSR)
24
25_DFLT_FILE_MODE = (
26    _stat.S_IROTH
27    | _stat.S_IRGRP
28    | _stat.S_IRUSR)
29
30
31if _sys.version_info[0] == 2:
32    from StringIO import StringIO as _BaseStringIO
33    from StringIO import StringIO as _BaseBytesIO
34
35    class _StringIO (_BaseStringIO):
36        def __enter__(self):
37            return self
38
39        def __exit__(self, exc_type, exc_value, traceback):
40            self.close()
41            return False
42
43    class _BytesIO (_BaseBytesIO):
44        def __enter__(self):
45            return self
46
47        def __exit__(self, exc_type, exc_value, traceback):
48            self.close()
49            return False
50
51else:
52    from io import StringIO as _StringIO
53    from io import BytesIO as _BytesIO
54
55
56def _locate(path):
57    full_path = path
58    if _os.path.exists(path):
59        return path, None
60
61    else:
62        rest = []
63        root = _os.path.splitdrive(path)
64        while path and path != root:
65            path, bn = _os.path.split(path)
66            rest.append(bn)
67            if _os.path.exists(path):
68                break
69
70        if path == root:
71            raise IOError(
72                _errno.ENOENT, full_path,
73                "No such file or directory")
74
75        if not _os.path.isfile(path):
76            raise IOError(
77                _errno.ENOENT, full_path,
78                "No such file or directory")
79
80        rest.reverse()
81        return path, '/'.join(rest).strip('/')
82
83
84_open = open
85
86
87def open(path, mode='r'):
88    if 'w' in mode or 'a' in mode:
89        raise IOError(
90            _errno.EINVAL, path, "Write access not supported")
91    elif 'r+' in mode:
92        raise IOError(
93            _errno.EINVAL, path, "Write access not supported")
94
95    full_path = path
96    path, rest = _locate(path)
97    if not rest:
98        return _open(path, mode)
99
100    else:
101        try:
102            zf = _zipfile.ZipFile(path, 'r')
103
104        except _zipfile.error:
105            raise IOError(
106                _errno.ENOENT, full_path,
107                "No such file or directory")
108
109        try:
110            data = zf.read(rest)
111        except (_zipfile.error, KeyError):
112            zf.close()
113            raise IOError(
114                _errno.ENOENT, full_path,
115                "No such file or directory")
116        zf.close()
117
118        if mode == 'rb':
119            return _BytesIO(data)
120
121        else:
122            if _sys.version_info[0] == 3:
123                data = data.decode('ascii')
124
125            return _StringIO(data)
126
127
128def listdir(path):
129    full_path = path
130    path, rest = _locate(path)
131    if not rest and not _os.path.isfile(path):
132        return _os.listdir(path)
133
134    else:
135        try:
136            zf = _zipfile.ZipFile(path, 'r')
137
138        except _zipfile.error:
139            raise IOError(
140                _errno.ENOENT, full_path,
141                "No such file or directory")
142
143        result = set()
144        seen = False
145        try:
146            for nm in zf.namelist():
147                if rest is None:
148                    seen = True
149                    value = nm.split('/')[0]
150                    if value:
151                        result.add(value)
152
153                elif nm.startswith(rest):
154                    if nm == rest:
155                        seen = True
156                        value = ''
157                        pass
158                    elif nm[len(rest)] == '/':
159                        seen = True
160                        value = nm[len(rest)+1:].split('/')[0]
161                    else:
162                        value = None
163
164                    if value:
165                        result.add(value)
166        except _zipfile.error:
167            zf.close()
168            raise IOError(
169                _errno.ENOENT, full_path,
170                "No such file or directory")
171
172        zf.close()
173
174        if not seen:
175            raise IOError(
176                _errno.ENOENT, full_path,
177                "No such file or directory")
178
179        return list(result)
180
181
182def isfile(path):
183    full_path = path
184    path, rest = _locate(path)
185    if not rest:
186        ok = _os.path.isfile(path)
187        if ok:
188            try:
189                zf = _zipfile.ZipFile(path, 'r')
190                return False
191            except (_zipfile.error, IOError):
192                return True
193        return False
194
195    zf = None
196    try:
197        zf = _zipfile.ZipFile(path, 'r')
198        zf.getinfo(rest)
199        zf.close()
200        return True
201    except (KeyError, _zipfile.error):
202        if zf is not None:
203            zf.close()
204
205        # Check if this is a directory
206        try:
207            zf.getinfo(rest + '/')
208        except KeyError:
209            pass
210        else:
211            return False
212
213        rest = rest + '/'
214        for nm in zf.namelist():
215            if nm.startswith(rest):
216                # Directory
217                return False
218
219        # No trace in zipfile
220        raise IOError(
221            _errno.ENOENT, full_path,
222            "No such file or directory")
223
224
225def isdir(path):
226    full_path = path
227    path, rest = _locate(path)
228    if not rest:
229        ok = _os.path.isdir(path)
230        if not ok:
231            try:
232                zf = _zipfile.ZipFile(path, 'r')
233            except (_zipfile.error, IOError):
234                return False
235            return True
236        return True
237
238    zf = None
239    try:
240        try:
241            zf = _zipfile.ZipFile(path)
242        except _zipfile.error:
243            raise IOError(
244                _errno.ENOENT, full_path,
245                "No such file or directory")
246
247        try:
248            zf.getinfo(rest)
249        except KeyError:
250            pass
251        else:
252            # File found
253            return False
254
255        rest = rest + '/'
256        try:
257            zf.getinfo(rest)
258        except KeyError:
259            pass
260        else:
261            # Directory entry found
262            return True
263
264        for nm in zf.namelist():
265            if nm.startswith(rest):
266                return True
267
268        raise IOError(
269            _errno.ENOENT, full_path,
270            "No such file or directory")
271    finally:
272        if zf is not None:
273            zf.close()
274
275
276def islink(path):
277    full_path = path
278    path, rest = _locate(path)
279    if not rest:
280        return _os.path.islink(path)
281
282    try:
283        zf = _zipfile.ZipFile(path)
284    except _zipfile.error:
285        raise IOError(
286            _errno.ENOENT, full_path,
287            "No such file or directory")
288    try:
289        try:
290            zf.getinfo(rest)
291        except KeyError:
292            pass
293        else:
294            # File
295            return False
296
297        rest += '/'
298        try:
299            zf.getinfo(rest)
300        except KeyError:
301            pass
302        else:
303            # Directory
304            return False
305
306        for nm in zf.namelist():
307            if nm.startswith(rest):
308                # Directory without listing
309                return False
310
311        raise IOError(
312            _errno.ENOENT, full_path,
313            "No such file or directory")
314
315    finally:
316        zf.close()
317
318
319def readlink(path):
320    full_path = path
321    path, rest = _locate(path)
322    if rest:
323        # No symlinks inside zipfiles
324        raise OSError(
325            _errno.ENOENT, full_path,
326            "No such file or directory")
327
328    return _os.readlink(path)
329
330
331def getmode(path):
332    full_path = path
333    path, rest = _locate(path)
334    if not rest:
335        return _stat.S_IMODE(_os.stat(path).st_mode)
336
337    zf = None
338    try:
339        zf = _zipfile.ZipFile(path)
340        info = None
341
342        try:
343            info = zf.getinfo(rest)
344        except KeyError:
345            pass
346
347        if info is None:
348            try:
349                info = zf.getinfo(rest + '/')
350            except KeyError:
351                pass
352
353        if info is None:
354            rest = rest + '/'
355            for nm in zf.namelist():
356                if nm.startswith(rest):
357                    break
358            else:
359                raise IOError(
360                    _errno.ENOENT, full_path,
361                    "No such file or directory")
362
363            # Directory exists, but has no entry of its own.
364            return _DFLT_DIR_MODE
365
366        # The mode is stored without file-type in external_attr.
367        if (info.external_attr >> 16) != 0:
368            return _stat.S_IMODE(info.external_attr >> 16)
369        else:
370            return _DFLT_FILE_MODE
371
372    finally:
373        if zf is not None:
374            zf.close()
375
376
377def getmtime(path):
378    full_path = path
379    path, rest = _locate(path)
380    if not rest:
381        return _os.path.getmtime(path)
382
383    zf = None
384    try:
385        zf = _zipfile.ZipFile(path)
386        info = None
387
388        try:
389            info = zf.getinfo(rest)
390        except KeyError:
391            pass
392
393        if info is None:
394            try:
395                info = zf.getinfo(rest + '/')
396            except KeyError:
397                pass
398
399        if info is None:
400            rest = rest + '/'
401            for nm in zf.namelist():
402                if nm.startswith(rest):
403                    break
404            else:
405                raise IOError(
406                    _errno.ENOENT, full_path,
407                    "No such file or directory")
408
409            # Directory exists, but has no entry of its
410            # own, fake mtime by using the timestamp of
411            # the zipfile itself.
412            return _os.path.getmtime(path)
413
414        return _time.mktime(info.date_time + (0, 0, -1))
415
416    finally:
417        if zf is not None:
418            zf.close()
419