ZIP Archive List: Source Code
This is the Macromedia Director Lingo source code for a parent script which returns the internal directory list of a .ZIP archive.
General notes:
- This is a Parent script. This is NOT a behaviour, NOT a movie script.
- The code will work in Macromedia Director 7.0 or later.
- Both the BinaryIO and ConvertData xtras are required. You should perform the BinaryIO registration Lingo before calling this parent script.
- This code is solely meant to extract the directory contents - it does not add to or extract data from a ZIP archive.
Copy and paste the source code below into a parent script.
-- Zip Archive Parent
-- Copyright (c) 2002 Kendall Anderson. All rights reserved.
-- http://invisiblethreads.com
-- This script was originally located at:
-- http://invisiblethreads.com/lingo/zipparent/index.html
-- OVERVIEW:
-- This parent script will read the directory contents of a .ZIP archive.
-- This process is quite fast - far faster than extracting the zip contents
-- to a text file and then parsing the text file.
-- This is a tool script, and there is no need to make an instance of the script.
-- Rather, execute the script as:
-- lData = script("Zip Archive Parent").mGetArchiveContents(sFilename)
-- You should run your BinaryIO registration code BEFORE calling this script.
-- Current Limitations:
-- We do not deal with .zip files which are parts of a set
-- (ie: zip files spanned across many volumes).
-- Not all elements of a file's directory entry are returned, but this code
-- could very easily be modified such that other elements (compressed size,
-- version needed to extract, etc) would also be returned.
-- Requirements:
-- Director 7.0+ (dot syntax)
-- BinaryIO Xtra (www.updatestage.com/xtras/binaryio.html)
-- ConvertData Xtra (free with BinaryIO Xtra - www.updatestage.com/xtras/convertdata.html)
-- Projector (BinaryIO Xtra does not work in ShockWave)
-- History:
-- Feb 14 2002, Kendall Anderson: initial design and coding
-- May 31 2002, Kendall Anderson: revised to use BinaryIO and read .zip archive directly
-- Jun 07 2002, Kendall Anderson: cleaned up code, added error-messaging
-- Public Methods:
-- mGetArchiveContents object me, string sFilename
-- returns a list of property lists in the format:
-- [ #arc_filesize: <integer>, -- ie: 147968
-- #arc_filedate: <string>, -- ie: "25-05-2002" (dd-mm-yyyy)
-- #arc_filetime: <string>, -- ie: "14:35:05" (hh:mm:ss)
-- #arc_folder: <string>, -- ie: "folder1/folder2/"
-- #arc_filename: <string> ] -- ie: "myfilename.ext"
-- There will be 1 property list for each file in the archive.
-- mInterface object me
-- mGetErrorMsg object me, integer iErrorNum
-- Error Handling:
-- If a non-zero integer is returned, it is an error-number.
-- Use obj.mGetErrorMsg(iErrorNumber) for the corresponding error message.
-- Example:
-- lDirectory = script("Zip Archive Parent").mGetArchiveContents("c:\myzipfile.zip")
-- if (lDirectory.ilk = #list) then
-- repeat with i = 1 to lDirectory.count
-- put i && lDirectory[i][#arc_filename]
-- end repeat
-- else
-- put "Did not successfully read archive contents."
-- end if
-------------------
-- Public Methods
-- given a .zip filename, read the directory list contents and
-- return as a list of property lists.
on mGetArchiveContents me, sArchiveFilename
if NOT (me._IsXtraPresent("binaryio")) then -- if BinaryIO Xtra not installed,
return -998 -- return with error
end if
if NOT (me._IsXtraPresent("convertdata")) then -- if ConvData Xtra not installed,
return -997 -- return with error
end if
lDirectory = me._ReadZipDirectory(sArchiveFilename)
if (lDirectory.ilk <> #list) then
put "mGetArchiveContents:didn't read contents from (" & sArchiveFilename & "):" & lDirectory
end if
return lDirectory
end
-----------------------------------------
-- Interface Usage and Error Handling methods
-----------------------------------------
-- show the interface for this object
on mInterface me
sMsg = RETURN & "Zip Archive Parent"
sMsg = sMsg & RETURN & "-------------------"
sMsg = sMsg & RETURN & "This object reads the directory contents of a .ZIP archive."
sMsg = sMsg & RETURN & "The object requires the BinaryIO and ConvertData xtras."
sMsg = sMsg & RETURN
sMsg = sMsg & RETURN & "Public Methods:"
sMsg = sMsg & RETURN & "---------------"
sMsg = sMsg & RETURN & "mGetArchiveContents object me, string sFilename"
sMsg = sMsg & RETURN
sMsg = sMsg & RETURN & "mInterface object me"
sMsg = sMsg & RETURN & "mGetErrorMsg object me, integer iErrorNum"
sMsg = sMsg & RETURN
put sMsg
return 0
end
-- given an error number, return the corresponding message
on mGetErrorMsg me, iErrorNum
return (me._GetErrorMessage(iErrorNum))
end
-------------------
-- Private Methods
-- given a .zip filename, read the directory structure and return as property list
-- do this by directly reading the file through BinaryIO
on _ReadZipDirectory me, sFilename
oFile = new(xtra "binaryio") -- get xtra reference
if NOT (objectP(oFile)) then -- if we didn't get a valid object
put "_ReadZipDirectory:error getting xtra reference"
return -999 -- return with error
end if
sError = oFile.OpenFile(1, sFilename) -- open the zip file
if (sError.word[1] = "Error") then -- if didn't open successfully
put "_ReadZipDirectory:error opening file (" & sFilename & "):" & sError
oFile.CloseFile() -- close the file
oFile = void -- clear the binaryio reference
return -1 -- return with error
end if
-- try to read the End of Central Directory record
lEndOfCentralDir = me._ReadEndOfCentralDirectory(oFile) -- read the end of central dir data
if (lEndOfCentralDir.ilk <> #proplist) then -- if we didn't read it successfully
put "_ReadZipDirectory:error reading end of central dir (" & sFilename & "):" & lEndOfCentralDir
oFile.CloseFile() -- close the file
oFile = void -- clear the binaryio reference
return lEndOfCentralDir -- return the error
end if
-- try to read the Central Directory Structure
lDirectory = me._ReadCentralDirectory(oFile, lEndOfCentralDir)
if (lDirectory.ilk <> #list) then -- if we didn't read central dir successfully
put "_ReadZipDirectory:error reading central dir (" & sFilename & "):" & lDirectory
oFile.CloseFile() -- close the file
oFile = void -- clear the binaryio reference
return lDirectory -- return the error
end if
oFile.CloseFile() -- after reading data, close the zip file
oFile = void -- and clear the binaryio reference
return lDirectory -- return the directory data
end
-- scan the .zip file and try to find/read the End of Central Directory record.
-- if we successfully read it, we return a property list of the values
-- if NOT, we return an integer error number.
on _ReadEndOfCentralDirectory me, oFile
iFileSize = oFile.GetFileSize() -- get the file length
if (iFileSize.ilk <> #integer) then -- if we didn't read it successfully,
put "_ReadCentralDirectory:error getting filesize:" & iFileSize
return -2 -- return error
end if
-- we have now opened the file
-- we want to find the end of central directory signature
iOffset = 4 -- offset to where we look for signature
bFound = FALSE
sSignature = me._GetZipSignature(#endofcentraldir) -- get signature for 'end of central directory'
repeat while NOT (bFound) -- starting at end of file
sError = oFile.SetFilePosition(iFileSize - iOffset) -- and working backwards
if (sError.word[1] = "Error") then
put "_ReadCentralDirectory:error setting file position:" & sError
return -3
end if
sBytes = oFile.ReadBytes(4) -- read 4 bytes
if (sBytes.ilk = #symbol) then -- if error reading bytes
put "_ReadCentralDirectory:error reading bytes:" & sBytes
return -4 -- return with error
end if
if (sBytes = sSignature) then -- if we matched the signature
bFound = TRUE
exit repeat -- exit our scanning search
else -- if we didn't find signature,
iOffset = iOffset + 1 -- increase offset
if (iOffset > iFileSize) then -- if we are now past start of file
exit repeat -- stop our search
end if
end if
end repeat
if NOT (bFound) then -- if at end of search, and didn't find signature
put "_ReadCentralDirectory:couldn't find central directory record!"
return -5 -- return error
end if
-- otherwise, we should now be positioned right after the end of central
-- dir signature, so we start reading data at 'number of this disk'
iDiskNum = oFile.ReadUnsignedShort() -- number of this disk
iDiskNumWithDir = oFile.ReadUnsignedShort() -- number of disk with start of central dir
iNumEntriesThisDisk = oFile.ReadUnsignedShort() -- # of entries in the central dir on this disk
iNumEntries = oFile.ReadUnsignedShort() -- # of entries in central directory
iSizeOfCentralDir = oFile.ReadUnsignedLong() -- size of central directory
iOffsetToCentralDir = oFile.ReadUnsignedLong() -- offset to start of central directory
iCommentLength = oFile.ReadUnsignedShort() -- length of the comment
if (iCommentLength > 0) then -- if we have a zip comment,
sComment = oFile.ReadBytes(iCommentLength) -- get the comment text
else
sComment = "" -- otherwise assume blank
end if
lData = [:] -- construct end of central dir data
lData.addProp(#disknum, iDiskNum)
lData.addProp(#disknumwithdir, iDiskNumWithDir)
lData.addProp(#numentriesthisdisk, iNumEntriesThisDisk)
lData.addProp(#numentries, iNumEntries)
lData.addProp(#sizeofcentraldir, iSizeOfCentralDir)
lData.addProp(#offsettocentraldir, iOffsetToCentralDir)
lData.addProp(#commentlength, iCommentLength)
lData.addProp(#comment, sComment)
return lData
end
-- given file pointer and data for end of central dir,
-- read the Central Dir
on _ReadCentralDirectory me, oFile, lEndOfCentralDir
-- seek to beginning of central directory
sError = oFile.SetFilePosition(lEndOfCentralDir[#offsettocentraldir])
if (sError.word[1] = "Error") then -- if couldn't seek to central dir
put "_ReadCentralDirectory:error seeking to start of central dir:" & sError
return -6 -- return error
end if
sSignature = me._GetZipSignature(#centralfileheader) -- get signature of central file header
iNumFiles = lEndOfCentralDir[#numentries] -- get # entries in directory
lDirectory = []
repeat with i = 1 to iNumFiles -- scan through all files in the directory
sSig = oFile.ReadBytes(4) -- read the file signature
if (sSig <> sSignature) then -- if it doesn't match,
--put i && "_ReadCentralDirectory:error matching signature:" & sSig
--put "...actually read..."
--repeat with j = 1 to sSig.length
-- put j && chartonum(sSig.char[j])
--end repeat
return -7 -- return error code
--return sSig
end if
iVersionMadeBy = oFile.ReadUnsignedShort() -- read version created by
iVersionNeeded = oFile.ReadUnsignedShort() -- read version required
iGeneralFlag = oFile.ReadUnsignedShort() -- read general flag data
iCompressionMethod = oFile.ReadUnsignedShort() -- read compression method of this file
iTime = oFile.ReadUnsignedShort() -- read time of this file
iDate = oFile.ReadUnsignedShort() -- read date of this file
iCRC = oFile.ReadUnsignedLong() -- read CRC-32 of this file
iCompressedSize = oFile.ReadUnsignedLong() -- compressed size of this file
iUncompressedSize = oFile.ReadUnsignedLong() -- uncompressed size of this file
iFilenameLength = oFile.ReadUnsignedShort() -- length of the path/filename
iDiskNumberStart = oFile.ReadUnsignedShort() -- disk number start (?)
iExtraFieldLength = oFile.ReadUnsignedShort() -- extra field length (?)
iCommentLength = oFile.ReadUnsignedShort() -- length of the file comment
iInternalAttributes = oFile.ReadUnsignedShort() -- internal file attributes
iExternalAttributes = oFile.ReadUnsignedLong() -- external file attributes
iLocalHeaderOffset = oFile.ReadUnsignedLong() -- offset of local header
sFilename = oFile.ReadBytes(iFilenameLength) -- path/filename
if (iExtraFieldLength > 0) then -- if we have extra field length,
sExtraField = oFile.ReadBytes(iExtraFieldLength) -- we extra field length
else
sExtraField = ""
end if
if (iCommentLength > 0) then -- if we have a file comment,
sComment = oFile.ReadBytes(iCommentLength) -- read the file comment
else
sComment = ""
end if
sTime = me._ConvertTime(iTime) -- convert time bits to a string
sDate = me._ConvertDate(iDate) -- convert date bits to a string
-- now add this entry to our list
lRecord = [:]
lRecord.addProp(#arc_filesize, iUncompressedSize)
lRecord.addProp(#arc_filedate, sDate)
lRecord.addProp(#arc_filetime, sTime)
lRecord.addProp(#arc_folder, me._SplitPathFile(sFilename, #path, "/"))
lRecord.addProp(#arc_filename, me._SplitPathFile(sFilename, #file, "/"))
lDirectory.add(lRecord)
end repeat
return lDirectory
end
-- return a string for the requested type of ZIP signature
-- we use this to scan/verify locations in the zip file
on _GetZipSignature me, sType
case (sType) of
#endofcentraldir: -- end of central directory signature
lSig = [ 80, 75, 05, 06 ] -- 50/4b/05/06 = 0x06054b50
#centralfileheader: -- central file header signature
lSig = [ 80, 75, 01, 02 ] -- 50/4b/01/02 = 0x02014b50
otherwise:
lSig = []
end case
sSig = ""
repeat with i = 1 to lSig.count
sSig = sSig & numToChar(lSig[i])
end repeat
return sSig
end
-- use ConvertData xtra (free, for use with BinaryIO Xtra)
-- to do bitshifting to get values
on _ConvertTime me, iTime
iHour = cdatBitShiftRight(iTime, 11) -- shift right to remove min/sec
iMin = iTime
repeat with i = 11 to 15 -- clear out 'hour' bits
iMin = cdatSetBit(iMin, i, 0)
end repeat
iMin = cdatBitShiftRight(iMin, 5) -- shift to clear 'second' bits
iSec = iMin
repeat with i = 5 to 10 -- clear out 'minute' bits
iSec = cdatSetBit(iSec, i, 0)
end repeat
iSec = iSec * 2 -- because only half-seconds are recorded in MS-DOS format
sTime = "" -- construct the time string
lItems = [ iHour, iMin, iSec ]
repeat with i = 1 to lItems.count
sTemp = string(lItems[i])
if (sTemp.length < 2) then -- ensure leading 0's if req'd
sTemp = "0" & sTemp
end if
sTime = sTime & sTemp
if (i < lItems.count) then
sTime = sTime & ":"
end if
end repeat
return sTime
end
-- use ConvertData xtra to convert the numerical date
-- to a string (by bitshifting the number)
on _ConvertDate me, iDate
iYear = cdatBitShiftRight(iDate, 9) -- shift right to remove month/day bits
iYear = iYear + 1980
iMonth = iDate
repeat with i = 9 to 15 -- clear out 'year' bits
iMonth = cdatSetBit(iMonth, i, 0)
end repeat
iMonth = cdatBitShiftRight(iMonth, 5)
iDay = iMonth
repeat with i = 5 to 8 -- clear out 'month' bits
iDay = cdatSetBit(iDay, i, 0)
end repeat
sDay = string(iDay)
if (sDay.length < 2) then
sDay = "0" & sDay
end if
sMonth = string(iMonth)
if (sMonth.length < 2) then
sMonth = "0" & sMonth
end if
sDate = sDay & "-" & sMonth & "-" & iYear
return sDate
end
-- return TRUE if the given xtra is installed, FALSE if not
on _IsXtraPresent me, sXtraName
bFound = FALSE
dNumXtras = the number of xtras
repeat with i = 1 to dNumXtras
if (xtra(i).name = sXtraName) then
bFound = TRUE
exit repeat
end if
end repeat
return bFound
end
-- Given an error number, return the corresponding message.
-- If an error number isn't specified, construct a list
-- of all the error messages.
on _GetErrorMessage me, iErrorNum
if (voidP(iErrorNum)) then
-- compile a list of all error messages
lErrors = [ -1, -2, -3, -4, -5, -6, -7, -997, -998, -999 ]
sMsg = ""
repeat with i = 1 to lErrors.count
iError = lErrors[i]
sMsg = sMsg & me._GetErrorMessage(iError)
if (iError < lErrors.count) then
sMsg = sMsg & RETURN
end if
end repeat
else
-- if I create this as a prop list, how do I return just a specific item??
case (iErrorNum) of
-1: sMsg = "( -1) Error opening file"
-2: sMsg = "( -2) Error getting filesize"
-3: sMsg = "( -3) Error setting file position"
-4: sMsg = "( -4) Error reading from file"
-5: sMsg = "( -5) Couldn't find end of central directory record"
-6: sMsg = "( -6) Error seeking to start of central directory"
-7: sMsg = "( -7) Error matching central directory signature"
-997: sMsg = "(-997) ConvertData Xtra not present"
-998: sMsg = "(-998) BinaryIO Xtra not present"
-999: sMsg = "(-999) Could not get valid instance of BinaryIO Xtra"
otherwise:
sMsg = "unknown error number:" && iErrorNum
end case
end if
return sMsg
end
-- Handlers taken from KA Toolkit Scripts
-- given a full path and filename, return either the path or file component only
-- sType is either #path or #file, or #both and we then return prop list
-- we can supply the optional item delimiter cDelim, if none supplied we use
-- the last character of the moviepath (\ on PC, : on MAC)
on _SplitPathFile me, sText, sType, cDelim
if (voidP(cDelim)) then
the itemdelimiter = the last char of the moviepath
else
the itemdelimiter = cDelim
end if
if (sText.items.count = 1) then
sFile = sText
sPath = ""
else
sFile = the last item of sText
iLen = sText.length - sFile.length
sPath = char 1 to iLen of sText
end if
case (sType) of
#path: return sPath
#file: return sFile
#both: return [ #path: sPath, #filename: sFile ]
end case
end
