Files
2026-08-15 17:50:48 +02:00

322 lines
14 KiB
Lua

--[[lit-meta
name = 'TohruMKDM/inflate'
version = '2.0.0'
homepage = 'https://github.com/TohruMKDM/lua-inflate'
description = 'ZIP archive inflation in pure Lua.'
tags = {'zlib', 'inflate', 'inflation', 'compression'}
license = 'MIT'
author = {name = 'Tohru~ (トール)', email = 'admin@ikaros.pw'}
contributors = {'zerkman', 'samhocevar'}
]]
---@diagnostic disable-next-line: undefined-global
local bitOp = bit32 or bit
if not bitOp then
error('This library requires bit operations to work property.')
end
local rshift, lshift, band, bxor, bnot = bitOp.rshift, bitOp.lshift, bitOp.band, bitOp.bxor, bitOp.bnot
local byte, char, sub, find = string.byte, string.char, string.sub, string.find
local concat, unpack = table.concat, unpack or table.unpack
local min = math.min
local ORDER = {17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16}
local CRC32 = {[0] = 0, 1996959894, -301047508, -1727442502, 124634137, 1886057615, -379345611, -1637575261, 249268274, 2044508324, -522852066, -1747789432, 162941995, 2125561021, -407360249, -1866523247, 498536548, 1789927666, -205950648, -2067906082, 450548861, 1843258603, -187386543, -2083289657, 325883990, 1684777152, -43845254,-1973040660, 335633487, 1661365465, -99664541, -1928851979, 997073096, 1281953886, -715111964, -1570279054, 1006888145, 1258607687, -770865667, -1526024853, 901097722, 1119000684, -608450090, -1396901568, 853044451, 1172266101, -589951537, -1412350631, 651767980, 1373503546, -925412992, -1076862698, 565507253, 1454621731, -809855591, -1195530993, 671266974, 1594198024, -972236366, -1324619484, 795835527, 1483230225, -1050600021, -1234817731, 1994146192, 31158534, -1731059524, -271249366, 1907459465, 112637215, -1614814043, -390540237, 2013776290, 251722036, -1777751922, -519137256, 2137656763, 141376813, -1855689577, -429695999, 1802195444, 476864866, -2056965928, -228458418, 1812370925, 453092731, -2113342271, -183516073, 1706088902, 314042704, -1950435094, -54949764, 1658658271, 366619977, -1932296973, -69972891, 1303535960, 984961486, -1547960204, -725929758, 1256170817, 1037604311, -1529756563, -740887301, 1131014506, 879679996, -1385723834, -631195440, 1141124467, 855842277, -1442165665, -586318647, 1342533948, 654459306, -1106571248, -921952122, 1466479909, 544179635, -1184443383, -832445281, 1591671054, 702138776, -1328506846, -942167884, 1504918807, 783551873, -1212326853, -1061524307, -306674912, -1698712650, 62317068, 1957810842, -355121351, -1647151185, 81470997,1943803523, -480048366, -1805370492, 225274430, 2053790376, -468791541, -1828061283, 167816743, 2097651377, -267414716, -2029476910, 503444072, 1762050814, -144550051, -2140837941, 426522225, 1852507879, -19653770, -1982649376, 282753626, 1742555852, -105259153, -1900089351, 397917763, 1622183637, -690576408, -1580100738, 953729732, 1340076626, -776247311, -1497606297, 1068828381, 1219638859, -670225446, -1358292148, 906185462, 1090812512, -547295293, -1469587627, 829329135, 1181335161, -882789492, -1134132454, 628085408, 1382605366, -871598187, -1156888829, 570562233, 1426400815, -977650754, -1296233688, 733239954, 1555261956, -1026031705, -1244606671, 752459403, 1541320221, -1687895376, -328994266, 1969922972, 40735498, -1677130071, -351390145, 1913087877, 83908371, -1782625662, -491226604, 2075208622, 213261112, -1831694693, -438977011, 2094854071, 198958881, -2032938284, -237706686, 1759359992, 534414190, -2118248755, -155638181, 1873836001, 414664567, -2012718362, -15766928, 1711684554, 285281116, -1889165569, -127750551, 1634467795, 376229701, -1609899400, -686959890, 1308918612, 956543938, -1486412191, -799009033, 1231636301, 1047427035, -1362007478, -640263460, 1088359270, 936918000, -1447252397, -558129467, 1202900863, 817233897, -1111625188, -893730166, 1404277552, 615818150, -1160759803, -841546093, 1423857449, 601450431, -1285129682, -1000256840, 1567103746, 711928724, -1274298825, -1022587231, 1510334235, 755167117}
local NBT = {2, 3, 7}
local CNT = {144, 112, 24, 8}
local DPT = {8, 9, 7, 8}
local STATIC_HUFFMAN = {[0] = 5, 261, 133, 389, 69, 325, 197, 453, 37, 293, 165, 421, 101, 357, 229, 485, 21, 277, 149, 405, 85, 341, 213, 469, 53, 309, 181, 437, 117, 373, 245, 501}
local STATIC_BITS = 5
local CHUNK_SIZE = 4096
local function generateCrc32(input)
local crc = -1
for i = 1, #input do
local c = byte(input, i)
crc = bxor(CRC32[bxor(c, band(crc, 255))], rshift(crc, 8))
end
crc = bnot(crc)
if crc < 0 then
crc = crc + 4294967296
end
return crc
end
local function flushBits(stream, int)
stream.bits = rshift(stream.bits, int)
stream.count = stream.count - int
end
local function peekBits(stream, int)
local buffer, bits, count, position = stream.buffer, stream.bits, stream.count, stream.position
while count < int do
bits = bits + lshift(byte(buffer, position), count)
position = position + 1
count = count + 8
end
stream.bits = bits
stream.position = position
stream.count = count
return band(bits, lshift(1, int) - 1)
end
local function getBits(stream, int)
local result = peekBits(stream, int)
stream.bits = rshift(stream.bits, int)
stream.count = stream.count - int
return result
end
local function getElement(stream, hufftable, int)
local element = hufftable[peekBits(stream, int)]
local length = band(element, 15)
local result = rshift(element, 4)
stream.bits = rshift(stream.bits, length)
stream.count = stream.count - length
return result
end
local function huffman(depths)
local size = #depths
local blocks, codes, hufftable = {[0] = 0}, {}, {}
local bits, code = 1, 0
for i = 1, size do
local depth = depths[i]
if depth > bits then
bits = depth
end
blocks[depth] = (blocks[depth] or 0) + 1
end
for i = 1, bits do
code = (code + (blocks[i - 1] or 0)) * 2
codes[i] = code
end
for i = 1, size do
local depth = depths[i]
if depth > 0 then
local element = (i - 1) * 16 + depth
local rcode = 0
for j = 1, depth do
rcode = rcode + lshift(band(1, rshift(codes[depth], j - 1)), depth - j)
end
for j = 0, 2 ^ bits - 1, 2 ^ depth do
hufftable[j + rcode] = element
end
codes[depth] = codes[depth] + 1
end
end
return hufftable, bits
end
local function loop(output, stream, litTable, litBits, distTable, distBits)
local index = #output + 1
local lit
repeat
lit = getElement(stream, litTable, litBits)
if lit < 256 then
output[index] = lit
index = index + 1
elseif lit > 256 then
local bits, size, dist = 0, 3, 1
if lit < 265 then
size = size + lit - 257
elseif lit < 285 then
bits = rshift(lit - 261, 2)
size = size + lshift(band(lit - 261, 3) + 4, bits)
else
size = 258
end
if bits > 0 then
size = size + getBits(stream, bits)
end
local element = getElement(stream, distTable, distBits)
if element < 4 then
dist = dist + element
else
bits = rshift(element - 2, 1)
dist = dist + lshift(band(element, 1) + 2, bits) + getBits(stream, bits)
end
local position = index - dist
repeat
output[index] = output[position]
index = index + 1
position = position + 1
size = size - 1
until size == 0
end
until lit == 256
end
local function dynamic(output, stream)
local lit, dist, length = 257 + getBits(stream, 5), 1 + getBits(stream, 5), 4 + getBits(stream, 4)
local depths = {}
for i = 1, length do
depths[ORDER[i]] = getBits(stream, 3)
end
for i = length + 1, 19 do
depths[ORDER[i]] = 0
end
local lengthTable, lengthBits = huffman(depths)
local i = 1
local total = lit + dist + 1
repeat
local element = getElement(stream, lengthTable, lengthBits)
if element < 16 then
depths[i] = element
i = i + 1
elseif element < 19 then
local int = NBT[element - 15]
local count = 0
local num = 3 + getBits(stream, int)
if element == 16 then
count = depths[i - 1]
elseif element == 18 then
num = num + 8
end
for _ = 1, num do
depths[i] = count
i = i + 1
end
end
until i == total
local litDepths, distDepths = {}, {}
for j = 1, lit do
litDepths[j] = depths[j]
end
for j = lit + 1, #depths do
distDepths[#distDepths + 1] = depths[j]
end
local litTable, litBits = huffman(litDepths)
local distTable, distBits = huffman(distDepths)
loop(output, stream, litTable, litBits, distTable, distBits)
end
local function static(output, stream)
local depths = {}
for i = 1, 4 do
local depth = DPT[i]
for _ = 1, CNT[i] do
depths[#depths + 1] = depth
end
end
local litTable, litBits = huffman(depths)
loop(output, stream, litTable, litBits, STATIC_HUFFMAN, STATIC_BITS)
end
local function uncompressed(output, stream)
flushBits(stream, band(stream.count, 7))
local length = getBits(stream, 16); getBits(stream, 16)
local buffer, position = stream.buffer, stream.position
for i = position, position + length - 1 do
output[#output + 1] = byte(buffer, i, i)
end
stream.position = position + length
end
local function int2le(buffer, position)
local a, b = byte(buffer, position, position + 1)
return b * 256 + a
end
local function int4le(buffer, position)
local a, b, c, d = byte(buffer, position, position + 3)
return ((d * 256 + c) * 256 + b) * 256 + a
end
local inflate = {}
---@class BitStream
---@field buffer string Character Buffer
---@field position integer Position in the character buffer
---@field bits integer Bits buffer
---@field count integer Number of bits in the buffer
local BitStream = {}
BitStream.__index = BitStream
---Creates a new bitstream object with the specified buffer
---@param buffer string The character buffer to use for the bitstream.
---@return BitStream
function inflate.new(buffer)
local EOCD
local pos = 1
repeat
local i, j = find(buffer, 'PK\5\6', pos, true)
if i then
EOCD = i
pos = j + 1
end
until not i
if EOCD then
buffer = sub(buffer, 1, EOCD + 19)..'\0\0'
end
local object = {buffer = buffer, position = 0, bits = 0, count = 0}
return setmetatable(object, BitStream)
end
---Update the chunk size to be used in inflation. Defaults to `4096`
---@param size integer The new chunk size, should be a power of 2.
function inflate.setChunkSize(size)
CHUNK_SIZE = size
end
---Returns an iterator that spans the list of files in the stream
---@return fun(): name: string?, offset: integer?, size: integer?, packed: boolean?, crc: integer?
function BitStream:files()
local buffer = self.buffer
local position = int4le(buffer, #buffer - 5) + 1
return function()
if int4le(buffer, position) ~= 33639248 then
return
end
local packed = int2le(buffer, position + 10) ~= 0
local crc = int4le(buffer, position + 16)
local length = int2le(buffer, position + 28)
local offset = int4le(buffer, position + 42) + 1
local name = sub(buffer, position + 46, position + 45 + length)
position = position + 46 + length + int2le(buffer, position + 30) + int2le(buffer, position + 32)
return name, offset + 30 + length + int2le(buffer, offset + 28), int4le(buffer, offset + 18), packed, crc
end
end
---Inflates the bitstream starting from the specified offset and optionally performs checksum verification
---@param offset integer The position at which to begin inflating.
---@param crc? integer The checksum value to use for verification.
---@return string
function BitStream:inflate(offset, crc)
local output, buffer = {}, {}
local last, typ
self.bits = 0
self.count = 0
self.position = offset
repeat
last, typ = getBits(self, 1), getBits(self, 2)
typ = typ == 0 and uncompressed(output, self) or typ == 1 and static(output, self) or typ == 2 and dynamic(output, self)
until last == 1
local size = #output
for i = 1, size, CHUNK_SIZE do
buffer[#buffer + 1] = char(unpack(output, i, min(i + CHUNK_SIZE - 1, size)))
end
local result = concat(buffer)
if crc and crc ~= generateCrc32(result) then
error("Checksum verification failed: the computed CRC does not match the expected value.", 2)
end
return result
end
---Extracts a specific file from the bitstream
---@param filepath string The file to unzip.
---@param verify? boolean Whether or not to perform checksum verification.
---@return string
function BitStream:unzip(filepath, verify)
for name, offset, size, packed, crc in self:files() do
if name == filepath then
return packed and self:inflate(offset, verify and crc or nil) or sub(self.buffer, offset, offset + size - 1)
end
end
error('File "'..filepath..'" not found in ZIP archive.')
end
--- Extracts unpacked contents from the bitstream at the specified offset and size
--- @param offset integer The starting position from which to extract the contents.
--- @param size integer The size of the contents to extract.
function BitStream:extract(offset, size)
return sub(self.buffer, offset, offset + size - 1)
end
return inflate