from __future__ import print_function
import os, sys, time
import zipfile as zipfilemodule
from .zipmodtimeutc import addModtimeUTC
RunningOnWindows = sys.platform.startswith('win')
"""
ABOUT THE "MAGIC" BITMASK
Magic = type + permission + DOS is-dir flag?
>>> code = 0xA1ED0000
>>> code
2716663808
>>> bin(code)
'0b10100001111011010000000000000000'
Type = symlink (0o12/0xA=symlink 0o10/0x8=file, 0o04/0x4=dir) [=stat() bits]
>>> bin(code & 0xF0000000)
'0b10100000000000000000000000000000'
>>> bin(code >> 28)
'0b1010'
>>> hex(code >> 28)
'0xa'
>>> oct(code >> 28)
'0o12'
Permission = 0o755 [rwx + r-x + r-x]
>>> bin((code & 0b00000001111111110000000000000000) >> 16)
'0b111101101'
>>> bin((code >> 16) & 0o777)
'0b111101101'
DOS (Windows) is-dir bit
>>> code |= 0x10
>>> bin(code)
'0b10100001111011010000000000010000'
>>> code & 0x10
16
>>> code = 0xA1ED0000
>>> code & 0x10
0
Full format:
TTTTsstrwxrwxrwx0000000000ADVSHR
^^^^____________________________ file type, per sys/stat.h (BSD)
^^^_________________________ setuid, setgid, sticky
^^^^^^^^^________________ permissions, per unix style
^^^^^^^^________ Unused (apparently)
^^^^^^^^ DOS attribute bits: bit 0x10 = is-dir
Discussion:
http://unix.stackexchange.com/questions/14705/
the-zip-formats-external-file-attribute
http://stackoverflow.com/questions/434641/
how-do-i-set-permissions-attributes-
on-a-file-in-a-zip-file-using-pythons-zip/6297838#6297838
"""
SYMLINK_TYPE = 0xA
SYMLINK_PERM = 0o755
SYMLINK_ISDIR = 0x10
SYMLINK_MAGIC = (SYMLINK_TYPE << 28) | (SYMLINK_PERM << 16)
assert SYMLINK_MAGIC == 0xA1ED0000, 'Bit math is askew'
SYMLINK_MAGIC &= 0b11111110000000001111111111111111
def addSymlink(filepath, zippath, zipfile, trace=print):
assert os.path.islink(filepath)
if hasattr(os, 'readlink'):
try:
linkpath = os.readlink(filepath)
except:
trace('--Symlink not supported')
linkpath = 'symlink-not-supported'
else:
trace('--Symlink not supported')
linkpath = 'symlink-not-supported'
createsystem = 0 if RunningOnWindows else 3
linkstat = os.lstat(filepath)
origtime = linkstat.st_mtime
ziptime = time.localtime(origtime)[0:6]
allseps = os.sep + (os.altsep or '')
if not zippath:
zippath = filepath
zippath = os.path.splitdrive(zippath)[1]
zippath = os.path.normpath(zippath)
zippath = zippath.lstrip(allseps)
zippath = zippath.replace(os.sep, '/')
newinfo = zipfilemodule.ZipInfo()
newinfo.filename = zippath
newinfo.date_time = ziptime
newinfo.create_system = createsystem
newinfo.compress_type = zipfile.compression
newinfo.external_attr = SYMLINK_MAGIC
if os.path.isdir(filepath):
newinfo.external_attr |= SYMLINK_ISDIR
linkperms = (linkstat[0] & 0xFFFF) << 16
newinfo.external_attr |= linkperms
zipfile.writestr(newinfo, linkpath)
addModtimeUTC(zipfile, utcmodtime=origtime)
def isSymlink(zipinfo):
"""
----------------------------------------------------------------------------
Extract: check the entry's type bits for symlink code.
This is the upper 4 bits, and matches os.stat() codes.
----------------------------------------------------------------------------
"""
return (zipinfo.external_attr >> 28) == SYMLINK_TYPE
def symlinkStubFile(destpath, linkpath, trace):
"""
----------------------------------------------------------------------------
Extract: simulate an unsupported symlink with a dummy file [1.1].
The is subpar, but it's better than killing the rest of the unzip.
No stub is made for symlink filenames with non-portable characters.
Must encode, else non-ASCII Unicode bombs on py2.X for 'w' text
files, and Unicode text-file interfaces differ in py2.X and 3.X.
Update: zips don't stop on errors anymore, but stubs still useful.
----------------------------------------------------------------------------
"""
try:
linkpath = linkpath.encode('utf8')
bogus = open(destpath, 'wb')
bogus.write(linkpath)
bogus.close()
except:
trace('--Could not make stub file for', destpath)
def extractSymlink(zipinfo, pathto, zipfile,
nofixlinks=False, trace=print, origname=None):
assert zipinfo.external_attr >> 28 == SYMLINK_TYPE
zippath = zipinfo.filename
linkpath = zipfile.read(origname or zippath)
try:
linkpath = linkpath.decode('utf8')
except UnicodeDecodeError:
trace('--Symlink not decodable: link forged')
linkpath = u'symlink-not-decodable'
zippath = zippath.replace('/', os.sep)
zippath = os.path.splitdrive(zippath)[1]
zippath = zippath.lstrip(os.sep)
allparts = zippath.split(os.sep)
okparts = [p for p in allparts if p not in ('.', '..')]
zippath = os.sep.join(okparts)
destpath = os.path.join(pathto, zippath)
destpath = os.path.normpath(destpath)
upperdirs = os.path.dirname(destpath)
if upperdirs and not os.path.exists(upperdirs):
os.makedirs(upperdirs)
if not origname:
open(destpath + '.ziptools_probe', 'w').close()
os.remove(destpath + '.ziptools_probe')
if not nofixlinks:
linkpath = linkpath.replace(u'/', os.sep).replace(u'\\', os.sep)
if os.path.lexists(destpath):
os.remove(destpath)
isdir = zipinfo.external_attr & SYMLINK_ISDIR
if (isdir and
RunningOnWindows and
int(sys.version[0]) >= 3):
dirarg = dict(target_is_directory=True)
else:
dirarg ={}
if hasattr(os, 'symlink'):
try:
os.symlink(linkpath, destpath, **dirarg)
except:
trace('--Symlink not supported: stub file made')
symlinkStubFile(destpath, linkpath, trace)
else:
trace('--Symlink not supported: stub file made')
symlinkStubFile(destpath, linkpath, trace)
return destpath