# signature { license=0, seed=457995617, key="d534f4d29243cd23a944088294a8057c" }
--
-- ZIP file library
--

local zlib = require("zlib")
local bor  = bit32.bor
local band = bit32.band

local shl = bit32.lshift
local shr = bit32.rshift

local _M = {}
_M._VERSION = "zip 0.4.4"

local byte = string.byte
local char = string.char


local function get2( f)
	local L,H = byte(f:read(2) or "\0\0", 1,2)
	return bor( L, shl(H,8))
end               

local function get4( f)
	local a,b,c,d = byte(f:read(4) or "\0\0\0\0", 1,4)
	return bor( a, shl(b,8), shl(c,16), shl(d,24))
end               

local function put2( f, v)
  	f:write( char(band(v,255), band( shr(v,8), 255)))
end               

local function put4( f, v)
	f:write( 
		char(
			band(     v,     255), 
			band( shr(v, 8), 255),
			band( shr(v,16), 255),
			band( shr(v,24), 255)
		)
	)
end               

--
-- creates a table containing the central directory entry
-- then tests the local file header from ZIP file and adds a data field
-- which it reads from ZIP file and eventually inflates
--
local function zipExtract( zip, name )
	local e = zip.dir[name]
	if not e then return nil, "file not in ZIP" end
	
	local zipF = zip.file
    zipF:seek("set", e.localFileHeader)
	if zipF:read(4) ~= "PK\3\4" then return nil, "not a local file header" end

	local T = {dir = e}
	zipF:read(22)	-- 4+2+4+ 4+4+4

	local skip = get2(zipF)  + get2(zipF) 
	zipF:read( skip )
	
	local data = zipF:read(e.cmpSize)
	if not data then return nil, "can't read data" end
	
	if e.compression == 0 then      -- t.data
		T.data = data
	elseif e.compression == 8 then
		T.data = zlib.inflate(-15)( data )
	else 
		return nil, "unsupported compression"
	end
	
	return T
end              

local function readCDirEntry( zipF )
	local T = {}
	if zipF:read(4) ~= "PK\1\2" then return nil, "not a central directory entry" end

	zipF:read(4)
	T.gpflag = get2(zipF)
	T.compression = get2(zipF)
	T.mTime   = get2(zipF)
	T.mDate   = get2(zipF)
	T.CRC     = get4(zipF)
	T.cmpSize = get4(zipF)
	T.orgSize = get4(zipF)

	local nameLen = get2(zipF)
	local skip = get2(zipF) + get2(zipF)
	zipF:read(8) -- disk#, internal, external attr's
	T.localFileHeader = get4(zipF)
	T.fileName = zipF:read( nameLen)
	zipF:read(skip)
	
	return T		
end

local function zipClose ( zip )
	zip.file:close()
	zip.file = nil
	zip.dir = nil
end

function _M.open( ZipFileName )
	local zipF = io.open( ZipFileName, "rb")
	if not zipF then return nil, "ZIP file not found" end

	zipF:seek("end", -22) -- goto end of central directory
	if zipF:read(4) ~= "PK\5\6" then return nil, "not a ZIP file" end
	
	zipF:read(6) --ndisks, thisdisk, records on this disk
    local nEntries = get2(zipF)
    zipF:read(4) -- bytes in central directory
    local cDirOffset = get4(zipF)

	zipF:seek("set",cDirOffset) 
	
	local dir = {}
	for i=1,nEntries do
		local T,err = readCDirEntry(zipF)
		if T then
			dir[T.fileName] = T
		else
			return nil, err
		end
	end
	return { file = zipF, dir = dir, extract = zipExtract, close = zipClose }
end

local function zipStore( zip, fileName, data, uncompressed, mtime )
	if not zip or not fileName then return end
	data = data or ""
	local ts = os.date( "*t", mtime )
	local zipF = zip.file
	local T = { fileName = fileName, CRC = zlib.crc32()(data) }

	T.mTime   = shl(ts.hour , 11) + shl( ts.min, 5) + ts.sec
	T.mDate   = shl(ts.year - 1980, 9) + shl( ts.month, 5) + ts.day
	T.localFileHeader = zipF:seek()

	if uncompressed then
		T.compression = 0
		T.orgSize = #data	-- orgSize = original size
		T.cmpSize = T.orgSize
	else
		local _
		data, _, T.orgSize, T.cmpSize = zlib.deflate(9,-15)( data, "finish")
		T.compression = 8
	end

	zipF:write( "PK\3\4\20\0\2\0")	-- local file header signature, version, flags
	put2( zipF, T.compression)
	put2( zipF, T.mTime)
	put2( zipF, T.mDate)
	put4( zipF, T.CRC)
	put4( zipF, T.cmpSize)
	put4( zipF, T.orgSize)
	put4( zipF, #fileName)		-- name length, extra len==0
	zipF:write( fileName )
	zipF:write( data )
	
	zip.dir[ T.fileName ] = T
	return T
end

local function zipDone( zip )
	local zipF = zip.file
	if not zipF then return end
	
	local cDirOffset = zipF:seek()
	local files = 0
	
	for _,T in pairs(zip.dir) do
		zipF:write( "PK\1\2\20\0\20\0\2\0")	-- central dir signature, versions, flags
		put2( zipF, T.compression)
		put2( zipF, T.mTime)
		put2( zipF, T.mDate)
		put4( zipF, T.CRC)
		put4( zipF, T.cmpSize)
		put4( zipF, T.orgSize)
		put2( zipF, #T.fileName)
		zipF:write( "\0\0\0\0\0\0\0\0\0\0\0\0" )	-- extra field len, comment length, disk nunber, attribs
		put4( zipF, T.localFileHeader )
		zipF:write( T.fileName )
		files = files +1
	end
	
	local cDirSize = zipF:seek() - cDirOffset
	zipF:write( "PK\5\6\0\0\0\0")	-- central dir end
	put2( zipF, files)
	put2( zipF, files)
	put4( zipF, cDirSize )
	put4( zipF, cDirOffset )
	zipF:write( "\0\0" )	 -- comment length

	return zipClose( zip )
end

function _M.create( ZipFileName )
	local zipF = io.open( ZipFileName, "wb")
	if not zipF then return nil, "can't create ZIP file" end

	local Z = { file = zipF, dir = {}, store = zipStore, close = zipDone }
	setmetatable( Z, {__gc = zipDone })

	return Z
end

return _M

