-- ************************************************************************
-- Script: ZIP_CLASS
-- Version: 0.6
-- Date: 2009-08-08
-- Author: Valentin Schmidt
--
-- implements ZIP compression according to PKWARE's ZIP File Format Specification (http://www.pkware.com/documents/casestudies/APPNOTE.TXT)
-- data is e.g. compatible with WinZip, WinRAR and many other tools.
--
-- Notice: unfortunately the current implementation of the compress/uncompress functions of director's bytearray class use
-- ZLIB (i.e. gzcompress) instead of raw DEFLATE compression. this is no limitation for compressing data (as
-- DEFLATE/GZIP/ZIP files) since the 6 additional ZLIB bytes (2 bytes at the beginning, 4 adler32 checksum bytes at the end)
-- just need to be removed to create valid DEFLATE compressed data (that can then be used to create GZIP/ZIP files). but
-- unfortunately the bytearray's uncompress() function fails if the 4 adler32 checksum bytes at the end of the bytearray are
-- missing or incorrect, and therefor INFLATE, GZIP and ZIP decompression is only possible if the adler32 checksums of the
-- original file(s) are known.
-- one way to lessen this strong limitation a little bit, so at least GZIP and ZIP files created with those scripts can be
-- decompressed without requiring any additional info, is to store the adler32 checksums as comments inside the GZP/ZIP file.
-- such files can be decompresssed with any 3rd party tool (which just ignore the comments) as well as with code based on
-- byterrays's uncompress function.
--
-- Requirements:
-- * fileIO xtra
-- * fileIO wrapper functions file_get_bytes()/file_put_bytes()
-- * str_replace() function
-- * crc32_bytearray() function (or alternatively an xtra for faster CRC32 calculation, e.g. crypto xtra)
--
-- ************************************************************************
-- PACK
property pDataSec
property pCtrlDir
property pOffset
-- UNPACK
property pZipFile
property pFileList
----------------------------------------
--
----------------------------------------
on new me
me.pDataSec = []
me.pCtrlDir = []
me.pOffset = 0
me.pFileList = []
return me
end
--**************************************
-- PUBLIC
--**************************************
----------------------------------------
--
----------------------------------------
on mAddFile (me, tFile, tFilename, tComment, tDate)
if voidP(tFilename) then tFilename=tFile
baData = file_get_bytes(tFile)
me.mAddFileData(baData, tFilename, tComment, tDate)
end
----------------------------------------
-- NOTICE:
-- passed bytearray tData is changed (bytearray is compressed)!!!
-- if this is not wanted, either create a copy or use tData.uncompress() to restore original state
----------------------------------------
on mAddFileData (me, tData, tFilename, tComment, tDate)
if the platform contains "win" then
tFilename = str_replace("\", "/", tFilename)
else
tFilename = str_replace(":", "/", tFilename)
end if
-- "local file header" segment
unc_len = tData.length
crc32 = crc32_bytearray(tData)
tData.compress()
if tComment=#adler32 then
tData.position = tData.length-3
tData.endian=#bigEndian
tComment = string(tData.readInt32())
else
tComment = string(tComment)
end if
c_len = tData.length + 6 -- fix
if voidP(tDate) then tDate = the systemdate
dosTime = dateToDosTime(tDate)
fr = bytearray()
fr.writeInt32(80 + 75*256 + 3*65536 + 4*16777216) -- \x50\x4b\x03\x04
fr.writeInt16(20) -- \x14\x00 = ver needed to extract
fr.writeInt16(0) -- \x00\x00 = gen purpose bit flag
fr.writeInt16(8) -- \x08\x00 = compression method
fr.writeInt32(dosTime)
fr.writeInt32(crc32) -- crc32
fr.writeInt32(c_len) -- compressed filesize
fr.writeInt32(unc_len) -- uncompressed filesize
fr.writeInt16(tFilename.length) -- length of filename
fr.writeInt16(0) -- extra field length
fr.writeRawString(tFilename, tFilename.length)
-- "file data" segment
fr.writeByteArray(tData, 3, tData.length-6) -- remove header and adler32 checksum
-- add this entry to array
me.pDataSec.add(fr)
-- now add to central directory record
cdrec = bytearray()
cdrec.writeInt32(80 + 75*256 + 1*65536 + 2*16777216) -- \x50\x4b\x01\x02
cdrec.writeInt16(0) -- \x00\x00 = version made by
cdrec.writeInt16(20) -- \x14\x00 = version needed to extract
cdrec.writeInt16(0) -- \x00\x00 = gen purpose bit flag
cdrec.writeInt16(8) -- \x08\x00 = compression method
cdrec.writeInt32(dosTime) -- last mod time & date
cdrec.writeInt32(crc32) -- crc32
cdrec.writeInt32(c_len) -- compressed filesize
cdrec.writeInt32(unc_len) -- uncompressed filesize
cdrec.writeInt16(tFilename.length) -- length of filename
cdrec.writeInt16(0) -- extra field length
cdrec.writeInt16(tComment.length) -- file comment length
cdrec.writeInt16(0) -- disk number start
cdrec.writeInt16(0) -- internal file attributes
cdrec.writeInt32(32) -- external file attributes - "archive" bit set
cdrec.writeInt32(me.pOffset) -- relative offset of local header
me.pOffset = me.pOffset + fr.length
cdrec.writeRawString(tFilename, tFilename.length)
-- optional extra field
if tComment.length>0 then cdrec.writeRawString(tComment, tComment.length)
-- save to central directory
me.pCtrlDir.add(cdrec)
end
----------------------------------------
--
----------------------------------------
on mSaveZip(me, tFilename, tZipComment)
tZipComment = string(tZipComment)
-- DATA SECTION
baZip = bytearray()
repeat with ba in me.pDataSec
baZip.writeByteArray(ba)
end repeat
dataSize = baZip.length
-- CENTRAL DIR
repeat with ba in me.pCtrlDir
baZip.writeByteArray(ba)
end repeat
ctrldirSize = baZip.length - dataSize
-- EOF CENTRAL DIR = \x50\x4b\x05\x06\x00\x00\x00\x00
baZip.writeInt32(80 + 75*256 + 5*65536 + 6*16777216)
baZip.writeInt32(0)
baZip.writeInt16(me.pCtrlDir.count) -- total # of entries "on this disk"
baZip.writeInt16(me.pCtrlDir.count) -- total # of entries overall
baZip.writeInt32(ctrldirSize) -- size of central dir
baZip.writeInt32(dataSize) -- offset to start of central dir
baZip.writeInt16(tZipComment.length) -- zip file comment length
if tZipComment.length>0 then
baZip.writeRawString(tZipComment, tZipComment.length)
end if
if string(tFilename)<>"" then
file_put_bytes(tFilename, baZip)
else
return baZip
end if
end
----------------------------------------
--
----------------------------------------
on mLoadZip(me, tZipFile)
me.pZipFile = tZipFile
me.pFileList = []
fp = xtra("fileIO").new()
fp.openFile(tZipFile, 1)
-- PARSE DATA SECTION
repeat while true
magic = fp.readByteArray(4)
if not(magic[1]=80 AND magic[2]=75 AND magic[3]=3 AND magic[4]=4) then -- magic: 'P' 'K' 3 4
fp.setPosition(fp.getPosition() - 4)
exit repeat
end if
tFile = [:]
ba = fp.readByteArray(6+16+2+2)
ba.position = 7
dtime = ba.readInt32()
crc32 = ba.readInt32() -- crc32
c_len = ba.readInt32() -- compressed filesize
unc_len = ba.readInt32() -- uncompressed filesize
filenameLen = ba.readInt16() -- length of filename
extraFieldLen = ba.readInt16() -- extra field length
ba = fp.readByteArray(filenameLen)
ba.position = 1
tFile["filename"] = ba.readRawString(ba.length)
if extraFieldLen>0 then
ba = fp.readByteArray(extraFieldLen)
end if
tFile["date"] = dosTimeToDate(dtime)
tFile["crc32"] = crc32
tFile["uncompressedSize"] = unc_len
tFile["compressedSize"] = c_len
tFile["offset"] = fp.getPosition()
me.pFileList.add(tFile)
fp.setPosition(fp.getPosition() + c_len -12)
end repeat
-- PARSE CENTRAL DIR
cnt = me.pFileList.count
repeat with i = 1 to cnt
tFile = me.pFileList[i]
magic = fp.readByteArray(4)
if not(magic[1]=80 AND magic[2]=75 AND magic[3]=1 AND magic[4]=2) then exit repeat -- magic: 'P' 'K' 1 2
fp.setPosition(fp.getPosition() + 8)
cdrec = fp.readByteArray(16+6)
cdrec.position = 17
filenameLen = cdrec.readInt16()
cdrec.readInt16()
commentLen = cdrec.readInt16()
fp.setPosition(fp.getPosition() + 12)
tmp = fp.readByteArray(filenameLen)
if commentLen>0 then
tmp = fp.readByteArray(commentLen)
tmp.position = 1
tFile["comment"] = tmp.readRawString(tmp.length)
end if
end repeat
fp.closeFile()
fp = 0
return me.pFileList
end
----------------------------------------
--
----------------------------------------
on mUnpacktFile (me, tFileIndex, tDestDirectory, tAdler32)
if voidP(tDestDirectory) then tDestDirectory=_movie.path
else if the last char of tDestDirectory<>the last char of _movie.path then
put the last char of _movie.path after tDestDirectory
end if
if voidP(tAdler32) then tAdler32=#comment -- ???
tFile = me.pFileList[tFileIndex]
if tAdler32=#comment then tAdler32=integer(tFile["comment"])
fp = xtra("fileIO").new()
fp.openFile(pZipFile, 1)
fp.setPosition(tFile["offset"])
ba = fp.readByteArray(tFile["compressedSize"]-12)
fp.closeFile()
fp = 0
-- INFLATE BYTEARRAY
ret = bytearray(ba.length+6)
-- copy deflated data
ret[1] = 120
ret[2] = 218
ret.position = 3
ret.writeByteArray(ba)
ret.endian = #bigEndian
ret.writeInt32(tAdler32)
ret.uncompress()
fn = tDestDirectory & tFile["filename"]
file_put_bytes(fn, ret)
return fn
end
--**************************************
-- UTILITIES
--**************************************
----------------------------------------
--
----------------------------------------
on dateToDosTime (tDate)
if (tDate.year < 1980) then
tDate = date(1980)
end if
tYear = tDate.year-1980
tMonth = tDate.month
tDay = tDate.day
tHour = tDate.seconds/3600
tMin = (tDate.seconds mod 3600)/60
tSec = tDate.seconds mod 60
d = tYear * 33554432
d = bitOr(d, tMonth * 2097152)
d = bitOr(d, tDay * 65536)
d = bitOr(d, tHour * 2048)
d = bitOr(d, tMin * 32)
d = bitOr(d, tSec / 2)
return d
end
----------------------------------------
--
----------------------------------------
on dosTimeToDate(dosTime)
tSec = 2 * bitAnd(dosTime, 31)
tMin = bitAnd((dosTime / 32), 63)
tHour = bitAnd((dosTime / 2048), 31)
tDay = bitAnd((dosTime / 65536), 31)
tMonth = bitAnd((dosTime / 2097152), 15)
tYear = bitAnd((dosTime / 33554432), 127) + 1980
tDate = date(tYear, tMonth, tDay)
tDate.seconds = tHour*3600 + tMin*60 + tSec
return tDate
end