mirror of
https://github.com/0x5eal/luau-unzip.git
synced 2025-04-10 17:20:53 +01:00
style: apply stylua formatter
This commit is contained in:
parent
792682e46d
commit
c2e638cbec
6 changed files with 911 additions and 774 deletions
|
@ -1,35 +1,36 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local zip = require("../lib")
|
local zip = require("../lib")
|
||||||
|
|
||||||
local file = fs.readFile("test.zip")
|
local file = fs.readFile("test.zip")
|
||||||
local reader = zip.load(buffer.fromstring(file))
|
local reader = zip.load(buffer.fromstring(file))
|
||||||
|
|
||||||
print("Directory structure:")
|
print("Directory structure:")
|
||||||
reader:walk(function(entry, depth)
|
reader:walk(function(entry, depth)
|
||||||
local prefix = string.rep(" ", depth)
|
local prefix = string.rep(" ", depth)
|
||||||
local suffix = if not entry.isDirectory then string.format(" (%d bytes)", entry.size) else ""
|
local suffix = if not entry.isDirectory then string.format(" (%d bytes)", entry.size) else ""
|
||||||
print(prefix .. entry.name .. suffix)
|
print(prefix .. entry.name .. suffix)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print("\nContents of `/`:")
|
print("\nContents of `/`:")
|
||||||
local assets = reader:listDirectory("/")
|
local assets = reader:listDirectory("/")
|
||||||
for _, entry in assets do
|
for _, entry in assets do
|
||||||
print(entry.name, if entry.isDirectory then "DIR" else entry.size)
|
print(entry.name, if entry.isDirectory then "DIR" else entry.size)
|
||||||
if not entry.isDirectory then
|
if not entry.isDirectory then
|
||||||
local extracted = reader:extract(entry, { isString = true })
|
local extracted = reader:extract(entry, { isString = true })
|
||||||
print("Content:", extracted)
|
print("Content:", extracted)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get archive statistics
|
-- Get archive statistics
|
||||||
local stats = reader:getStats()
|
local stats = reader:getStats()
|
||||||
print(string.format([[
|
print(string.format(
|
||||||
|
[[
|
||||||
Archive stats:
|
|
||||||
Files: %d
|
Archive stats:
|
||||||
Directories: %d
|
Files: %d
|
||||||
Total size: %d bytes]],
|
Directories: %d
|
||||||
stats.fileCount,
|
Total size: %d bytes]],
|
||||||
stats.dirCount,
|
stats.fileCount,
|
||||||
stats.totalSize
|
stats.dirCount,
|
||||||
))
|
stats.totalSize
|
||||||
|
))
|
||||||
|
|
60
lib/crc.luau
60
lib/crc.luau
|
@ -1,30 +1,30 @@
|
||||||
local CRC32_TABLE = table.create(256)
|
local CRC32_TABLE = table.create(256)
|
||||||
|
|
||||||
-- Initialize the lookup table and lock it in place
|
-- Initialize the lookup table and lock it in place
|
||||||
for i = 0, 255 do
|
for i = 0, 255 do
|
||||||
local crc = i
|
local crc = i
|
||||||
for _ = 1, 8 do
|
for _ = 1, 8 do
|
||||||
if bit32.band(crc, 1) == 1 then
|
if bit32.band(crc, 1) == 1 then
|
||||||
crc = bit32.bxor(bit32.rshift(crc, 1), 0xEDB88320)
|
crc = bit32.bxor(bit32.rshift(crc, 1), 0xEDB88320)
|
||||||
else
|
else
|
||||||
crc = bit32.rshift(crc, 1)
|
crc = bit32.rshift(crc, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
CRC32_TABLE[i] = crc
|
CRC32_TABLE[i] = crc
|
||||||
end
|
end
|
||||||
|
|
||||||
table.freeze(CRC32_TABLE)
|
table.freeze(CRC32_TABLE)
|
||||||
|
|
||||||
local function crc32(buf: buffer): number
|
local function crc32(buf: buffer): number
|
||||||
local crc = 0xFFFFFFFF
|
local crc = 0xFFFFFFFF
|
||||||
|
|
||||||
for i = 0, buffer.len(buf) - 1 do
|
for i = 0, buffer.len(buf) - 1 do
|
||||||
local byte = buffer.readu8(buf, i)
|
local byte = buffer.readu8(buf, i)
|
||||||
local index = bit32.band(bit32.bxor(crc, byte), 0xFF)
|
local index = bit32.band(bit32.bxor(crc, byte), 0xFF)
|
||||||
crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[index])
|
crc = bit32.bxor(bit32.rshift(crc, 8), CRC32_TABLE[index])
|
||||||
end
|
end
|
||||||
|
|
||||||
return bit32.bxor(crc, 0xFFFFFFFF)
|
return bit32.bxor(crc, 0xFFFFFFFF)
|
||||||
end
|
end
|
||||||
|
|
||||||
return crc32
|
return crc32
|
||||||
|
|
674
lib/inflate.luau
674
lib/inflate.luau
|
@ -1,61 +1,61 @@
|
||||||
local Tree = {}
|
local Tree = {}
|
||||||
|
|
||||||
export type Tree = typeof(setmetatable({} :: TreeInner, { __index = Tree }))
|
export type Tree = typeof(setmetatable({} :: TreeInner, { __index = Tree }))
|
||||||
type TreeInner = {
|
type TreeInner = {
|
||||||
table: { number }, -- len: 16 (🏳️⚧️❓)
|
table: { number }, -- len: 16 (🏳️⚧️❓)
|
||||||
trans: { number }, -- len: 288
|
trans: { number }, -- len: 288
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tree.new(): Tree
|
function Tree.new(): Tree
|
||||||
return setmetatable(
|
return setmetatable(
|
||||||
{
|
{
|
||||||
table = table.create(16, 0),
|
table = table.create(16, 0),
|
||||||
trans = table.create(288, 0),
|
trans = table.create(288, 0),
|
||||||
} :: TreeInner,
|
} :: TreeInner,
|
||||||
{ __index = Tree }
|
{ __index = Tree }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local Data = {}
|
local Data = {}
|
||||||
export type Data = typeof(setmetatable({} :: DataInner, { __index = Data }))
|
export type Data = typeof(setmetatable({} :: DataInner, { __index = Data }))
|
||||||
export type DataInner = {
|
export type DataInner = {
|
||||||
source: buffer,
|
source: buffer,
|
||||||
sourceIndex: number,
|
sourceIndex: number,
|
||||||
tag: number,
|
tag: number,
|
||||||
bitcount: number,
|
bitcount: number,
|
||||||
|
|
||||||
dest: buffer,
|
dest: buffer,
|
||||||
destLen: number,
|
destLen: number,
|
||||||
|
|
||||||
ltree: Tree,
|
ltree: Tree,
|
||||||
dtree: Tree,
|
dtree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
function Data.new(source: buffer, dest: buffer): Data
|
function Data.new(source: buffer, dest: buffer): Data
|
||||||
return setmetatable(
|
return setmetatable(
|
||||||
{
|
{
|
||||||
source = source,
|
source = source,
|
||||||
sourceIndex = 0,
|
sourceIndex = 0,
|
||||||
tag = 0,
|
tag = 0,
|
||||||
bitcount = 0,
|
bitcount = 0,
|
||||||
dest = dest,
|
dest = dest,
|
||||||
destLen = 0,
|
destLen = 0,
|
||||||
ltree = Tree.new(),
|
ltree = Tree.new(),
|
||||||
dtree = Tree.new(),
|
dtree = Tree.new(),
|
||||||
} :: DataInner,
|
} :: DataInner,
|
||||||
{ __index = Data }
|
{ __index = Data }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Static structures
|
-- Static structures
|
||||||
local staticLengthTree = Tree.new()
|
local staticLengthTree = Tree.new()
|
||||||
local staticDistTree = Tree.new()
|
local staticDistTree = Tree.new()
|
||||||
|
|
||||||
-- Extra bits and base tables
|
-- Extra bits and base tables
|
||||||
local lengthBits = table.create(30, 0)
|
local lengthBits = table.create(30, 0)
|
||||||
local lengthBase = table.create(30, 0)
|
local lengthBase = table.create(30, 0)
|
||||||
local distBits = table.create(30, 0)
|
local distBits = table.create(30, 0)
|
||||||
local distBase = table.create(30, 0)
|
local distBase = table.create(30, 0)
|
||||||
|
|
||||||
-- Special ordering of code length codes
|
-- Special ordering of code length codes
|
||||||
-- stylua: ignore
|
-- stylua: ignore
|
||||||
|
@ -64,282 +64,282 @@ local clcIndex = {
|
||||||
10, 5, 11, 4, 12, 3, 13, 2,
|
10, 5, 11, 4, 12, 3, 13, 2,
|
||||||
14, 1, 15
|
14, 1, 15
|
||||||
}
|
}
|
||||||
|
|
||||||
local codeTree = Tree.new()
|
local codeTree = Tree.new()
|
||||||
local lengths = table.create(288 + 32, 0)
|
local lengths = table.create(288 + 32, 0)
|
||||||
|
|
||||||
local function buildBitsBase(bits: { number }, base: { number }, delta: number, first: number)
|
local function buildBitsBase(bits: { number }, base: { number }, delta: number, first: number)
|
||||||
local sum = first
|
local sum = first
|
||||||
|
|
||||||
-- build bits table
|
-- build bits table
|
||||||
for i = 0, delta - 1 do
|
for i = 0, delta - 1 do
|
||||||
bits[i] = 0
|
bits[i] = 0
|
||||||
end
|
end
|
||||||
for i = 0, 29 - delta do
|
for i = 0, 29 - delta do
|
||||||
bits[i + delta] = math.floor(i / delta)
|
bits[i + delta] = math.floor(i / delta)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- build base table
|
-- build base table
|
||||||
for i = 0, 29 do
|
for i = 0, 29 do
|
||||||
base[i] = sum
|
base[i] = sum
|
||||||
sum = sum + bit32.lshift(1, bits[i])
|
sum = sum + bit32.lshift(1, bits[i])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function buildFixedTrees(lengthTree: Tree, distTree: Tree)
|
local function buildFixedTrees(lengthTree: Tree, distTree: Tree)
|
||||||
-- build fixed length tree
|
-- build fixed length tree
|
||||||
for i = 0, 6 do
|
for i = 0, 6 do
|
||||||
lengthTree.table[i] = 0
|
lengthTree.table[i] = 0
|
||||||
end
|
end
|
||||||
lengthTree.table[7] = 24
|
lengthTree.table[7] = 24
|
||||||
lengthTree.table[8] = 152
|
lengthTree.table[8] = 152
|
||||||
lengthTree.table[9] = 112
|
lengthTree.table[9] = 112
|
||||||
|
|
||||||
for i = 0, 23 do
|
for i = 0, 23 do
|
||||||
lengthTree.trans[i] = 256 + i
|
lengthTree.trans[i] = 256 + i
|
||||||
end
|
end
|
||||||
for i = 0, 143 do
|
for i = 0, 143 do
|
||||||
lengthTree.trans[24 + i] = i
|
lengthTree.trans[24 + i] = i
|
||||||
end
|
end
|
||||||
for i = 0, 7 do
|
for i = 0, 7 do
|
||||||
lengthTree.trans[24 + 144 + i] = 280 + i
|
lengthTree.trans[24 + 144 + i] = 280 + i
|
||||||
end
|
end
|
||||||
for i = 0, 111 do
|
for i = 0, 111 do
|
||||||
lengthTree.trans[24 + 144 + 8 + i] = 144 + i
|
lengthTree.trans[24 + 144 + 8 + i] = 144 + i
|
||||||
end
|
end
|
||||||
|
|
||||||
-- build fixed distance tree
|
-- build fixed distance tree
|
||||||
for i = 0, 4 do
|
for i = 0, 4 do
|
||||||
distTree.table[i] = 0
|
distTree.table[i] = 0
|
||||||
end
|
end
|
||||||
distTree.table[5] = 32
|
distTree.table[5] = 32
|
||||||
|
|
||||||
for i = 0, 31 do
|
for i = 0, 31 do
|
||||||
distTree.trans[i] = i
|
distTree.trans[i] = i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local offs = table.create(16, 0)
|
local offs = table.create(16, 0)
|
||||||
|
|
||||||
local function buildTree(t: Tree, lengths: { number }, off: number, num: number)
|
local function buildTree(t: Tree, lengths: { number }, off: number, num: number)
|
||||||
-- clear code length count table
|
-- clear code length count table
|
||||||
for i = 0, 15 do
|
for i = 0, 15 do
|
||||||
t.table[i] = 0
|
t.table[i] = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- scan symbol lengths, and sum code length counts
|
-- scan symbol lengths, and sum code length counts
|
||||||
for i = 0, num - 1 do
|
for i = 0, num - 1 do
|
||||||
t.table[lengths[off + i]] += 1
|
t.table[lengths[off + i]] += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
t.table[0] = 0
|
t.table[0] = 0
|
||||||
|
|
||||||
-- compute offset table for distribution sort
|
-- compute offset table for distribution sort
|
||||||
local sum = 0
|
local sum = 0
|
||||||
for i = 0, 15 do
|
for i = 0, 15 do
|
||||||
offs[i] = sum
|
offs[i] = sum
|
||||||
sum = sum + t.table[i]
|
sum = sum + t.table[i]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create code->symbol translation table
|
-- create code->symbol translation table
|
||||||
for i = 0, num - 1 do
|
for i = 0, num - 1 do
|
||||||
local len = lengths[off + i]
|
local len = lengths[off + i]
|
||||||
if len > 0 then
|
if len > 0 then
|
||||||
t.trans[offs[len]] = i
|
t.trans[offs[len]] = i
|
||||||
offs[len] += 1
|
offs[len] += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getBit(d: Data): number
|
local function getBit(d: Data): number
|
||||||
if d.bitcount <= 0 then
|
if d.bitcount <= 0 then
|
||||||
d.tag = buffer.readu8(d.source, d.sourceIndex)
|
d.tag = buffer.readu8(d.source, d.sourceIndex)
|
||||||
d.sourceIndex += 1
|
d.sourceIndex += 1
|
||||||
d.bitcount = 8
|
d.bitcount = 8
|
||||||
end
|
end
|
||||||
|
|
||||||
local bit = bit32.band(d.tag, 1)
|
local bit = bit32.band(d.tag, 1)
|
||||||
d.tag = bit32.rshift(d.tag, 1)
|
d.tag = bit32.rshift(d.tag, 1)
|
||||||
d.bitcount -= 1
|
d.bitcount -= 1
|
||||||
|
|
||||||
return bit
|
return bit
|
||||||
end
|
end
|
||||||
|
|
||||||
local function readBits(d: Data, num: number?, base: number): number
|
local function readBits(d: Data, num: number?, base: number): number
|
||||||
if not num then
|
if not num then
|
||||||
return base
|
return base
|
||||||
end
|
end
|
||||||
|
|
||||||
while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do
|
while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do
|
||||||
d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount))
|
d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount))
|
||||||
d.sourceIndex += 1
|
d.sourceIndex += 1
|
||||||
d.bitcount += 8
|
d.bitcount += 8
|
||||||
end
|
end
|
||||||
|
|
||||||
local val = bit32.band(d.tag, bit32.rshift(0xffff, 16 - num))
|
local val = bit32.band(d.tag, bit32.rshift(0xffff, 16 - num))
|
||||||
d.tag = bit32.rshift(d.tag, num)
|
d.tag = bit32.rshift(d.tag, num)
|
||||||
d.bitcount -= num
|
d.bitcount -= num
|
||||||
|
|
||||||
return val + base
|
return val + base
|
||||||
end
|
end
|
||||||
|
|
||||||
local function decodeSymbol(d: Data, t: Tree): number
|
local function decodeSymbol(d: Data, t: Tree): number
|
||||||
while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do
|
while d.bitcount < 24 and d.sourceIndex < buffer.len(d.source) do
|
||||||
d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount))
|
d.tag = bit32.bor(d.tag, bit32.lshift(buffer.readu8(d.source, d.sourceIndex), d.bitcount))
|
||||||
d.sourceIndex += 1
|
d.sourceIndex += 1
|
||||||
d.bitcount += 8
|
d.bitcount += 8
|
||||||
end
|
end
|
||||||
|
|
||||||
local sum, cur, len = 0, 0, 0
|
local sum, cur, len = 0, 0, 0
|
||||||
local tag = d.tag
|
local tag = d.tag
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
cur = 2 * cur + bit32.band(tag, 1)
|
cur = 2 * cur + bit32.band(tag, 1)
|
||||||
tag = bit32.rshift(tag, 1)
|
tag = bit32.rshift(tag, 1)
|
||||||
len += 1
|
len += 1
|
||||||
sum += t.table[len]
|
sum += t.table[len]
|
||||||
cur -= t.table[len]
|
cur -= t.table[len]
|
||||||
until cur < 0
|
until cur < 0
|
||||||
|
|
||||||
d.tag = tag
|
d.tag = tag
|
||||||
d.bitcount -= len
|
d.bitcount -= len
|
||||||
|
|
||||||
return t.trans[sum + cur]
|
return t.trans[sum + cur]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function decodeTrees(d: Data, lengthTree: Tree, distTree: Tree)
|
local function decodeTrees(d: Data, lengthTree: Tree, distTree: Tree)
|
||||||
local hlit = readBits(d, 5, 257)
|
local hlit = readBits(d, 5, 257)
|
||||||
local hdist = readBits(d, 5, 1)
|
local hdist = readBits(d, 5, 1)
|
||||||
local hclen = readBits(d, 4, 4)
|
local hclen = readBits(d, 4, 4)
|
||||||
|
|
||||||
for i = 0, 18 do
|
for i = 0, 18 do
|
||||||
lengths[i] = 0
|
lengths[i] = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
for i = 0, hclen - 1 do
|
for i = 0, hclen - 1 do
|
||||||
lengths[clcIndex[i + 1]] = readBits(d, 3, 0)
|
lengths[clcIndex[i + 1]] = readBits(d, 3, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
buildTree(codeTree, lengths, 0, 19)
|
buildTree(codeTree, lengths, 0, 19)
|
||||||
|
|
||||||
local num = 0
|
local num = 0
|
||||||
while num < hlit + hdist do
|
while num < hlit + hdist do
|
||||||
local sym = decodeSymbol(d, codeTree)
|
local sym = decodeSymbol(d, codeTree)
|
||||||
|
|
||||||
if sym == 16 then
|
if sym == 16 then
|
||||||
local prev = lengths[num - 1]
|
local prev = lengths[num - 1]
|
||||||
for _ = 1, readBits(d, 2, 3) do
|
for _ = 1, readBits(d, 2, 3) do
|
||||||
lengths[num] = prev
|
lengths[num] = prev
|
||||||
num += 1
|
num += 1
|
||||||
end
|
end
|
||||||
elseif sym == 17 then
|
elseif sym == 17 then
|
||||||
for _ = 1, readBits(d, 3, 3) do
|
for _ = 1, readBits(d, 3, 3) do
|
||||||
lengths[num] = 0
|
lengths[num] = 0
|
||||||
num += 1
|
num += 1
|
||||||
end
|
end
|
||||||
elseif sym == 18 then
|
elseif sym == 18 then
|
||||||
for _ = 1, readBits(d, 7, 11) do
|
for _ = 1, readBits(d, 7, 11) do
|
||||||
lengths[num] = 0
|
lengths[num] = 0
|
||||||
num += 1
|
num += 1
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
lengths[num] = sym
|
lengths[num] = sym
|
||||||
num += 1
|
num += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
buildTree(lengthTree, lengths, 0, hlit)
|
buildTree(lengthTree, lengths, 0, hlit)
|
||||||
buildTree(distTree, lengths, hlit, hdist)
|
buildTree(distTree, lengths, hlit, hdist)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function inflateBlockData(d: Data, lengthTree: Tree, distTree: Tree)
|
local function inflateBlockData(d: Data, lengthTree: Tree, distTree: Tree)
|
||||||
while true do
|
while true do
|
||||||
local sym = decodeSymbol(d, lengthTree)
|
local sym = decodeSymbol(d, lengthTree)
|
||||||
|
|
||||||
if sym == 256 then
|
if sym == 256 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if sym < 256 then
|
if sym < 256 then
|
||||||
buffer.writeu8(d.dest, d.destLen, sym)
|
buffer.writeu8(d.dest, d.destLen, sym)
|
||||||
d.destLen += 1
|
d.destLen += 1
|
||||||
else
|
else
|
||||||
sym -= 257
|
sym -= 257
|
||||||
|
|
||||||
local length = readBits(d, lengthBits[sym], lengthBase[sym])
|
local length = readBits(d, lengthBits[sym], lengthBase[sym])
|
||||||
local dist = decodeSymbol(d, distTree)
|
local dist = decodeSymbol(d, distTree)
|
||||||
|
|
||||||
local offs = d.destLen - readBits(d, distBits[dist], distBase[dist])
|
local offs = d.destLen - readBits(d, distBits[dist], distBase[dist])
|
||||||
|
|
||||||
for i = offs, offs + length - 1 do
|
for i = offs, offs + length - 1 do
|
||||||
buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.dest, i))
|
buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.dest, i))
|
||||||
d.destLen += 1
|
d.destLen += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function inflateUncompressedBlock(d: Data)
|
local function inflateUncompressedBlock(d: Data)
|
||||||
while d.bitcount > 8 do
|
while d.bitcount > 8 do
|
||||||
d.sourceIndex -= 1
|
d.sourceIndex -= 1
|
||||||
d.bitcount -= 8
|
d.bitcount -= 8
|
||||||
end
|
end
|
||||||
|
|
||||||
local length = buffer.readu8(d.source, d.sourceIndex + 1)
|
local length = buffer.readu8(d.source, d.sourceIndex + 1)
|
||||||
length = 256 * length + buffer.readu8(d.source, d.sourceIndex)
|
length = 256 * length + buffer.readu8(d.source, d.sourceIndex)
|
||||||
|
|
||||||
local invlength = buffer.readu8(d.source, d.sourceIndex + 3)
|
local invlength = buffer.readu8(d.source, d.sourceIndex + 3)
|
||||||
invlength = 256 * invlength + buffer.readu8(d.source, d.sourceIndex + 2)
|
invlength = 256 * invlength + buffer.readu8(d.source, d.sourceIndex + 2)
|
||||||
|
|
||||||
if length ~= bit32.bxor(invlength, 0xffff) then
|
if length ~= bit32.bxor(invlength, 0xffff) then
|
||||||
error("Invalid block length")
|
error("Invalid block length")
|
||||||
end
|
end
|
||||||
|
|
||||||
d.sourceIndex += 4
|
d.sourceIndex += 4
|
||||||
|
|
||||||
for _ = 1, length do
|
for _ = 1, length do
|
||||||
buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.source, d.sourceIndex))
|
buffer.writeu8(d.dest, d.destLen, buffer.readu8(d.source, d.sourceIndex))
|
||||||
d.destLen += 1
|
d.destLen += 1
|
||||||
d.sourceIndex += 1
|
d.sourceIndex += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
d.bitcount = 0
|
d.bitcount = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function uncompress(source: buffer): buffer
|
local function uncompress(source: buffer): buffer
|
||||||
local dest = buffer.create(buffer.len(source) * 4)
|
local dest = buffer.create(buffer.len(source) * 4)
|
||||||
local d = Data.new(source, dest)
|
local d = Data.new(source, dest)
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
local bfinal = getBit(d)
|
local bfinal = getBit(d)
|
||||||
local btype = readBits(d, 2, 0)
|
local btype = readBits(d, 2, 0)
|
||||||
|
|
||||||
if btype == 0 then
|
if btype == 0 then
|
||||||
inflateUncompressedBlock(d)
|
inflateUncompressedBlock(d)
|
||||||
elseif btype == 1 then
|
elseif btype == 1 then
|
||||||
inflateBlockData(d, staticLengthTree, staticDistTree)
|
inflateBlockData(d, staticLengthTree, staticDistTree)
|
||||||
elseif btype == 2 then
|
elseif btype == 2 then
|
||||||
decodeTrees(d, d.ltree, d.dtree)
|
decodeTrees(d, d.ltree, d.dtree)
|
||||||
inflateBlockData(d, d.ltree, d.dtree)
|
inflateBlockData(d, d.ltree, d.dtree)
|
||||||
else
|
else
|
||||||
error("Invalid block type")
|
error("Invalid block type")
|
||||||
end
|
end
|
||||||
until bfinal == 1
|
until bfinal == 1
|
||||||
|
|
||||||
if d.destLen < buffer.len(dest) then
|
if d.destLen < buffer.len(dest) then
|
||||||
local result = buffer.create(d.destLen)
|
local result = buffer.create(d.destLen)
|
||||||
buffer.copy(result, 0, dest, 0, d.destLen)
|
buffer.copy(result, 0, dest, 0, d.destLen)
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
return dest
|
return dest
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Initialize static trees and tables
|
-- Initialize static trees and tables
|
||||||
buildFixedTrees(staticLengthTree, staticDistTree)
|
buildFixedTrees(staticLengthTree, staticDistTree)
|
||||||
buildBitsBase(lengthBits, lengthBase, 4, 3)
|
buildBitsBase(lengthBits, lengthBase, 4, 3)
|
||||||
buildBitsBase(distBits, distBase, 2, 1)
|
buildBitsBase(distBits, distBase, 2, 1)
|
||||||
lengthBits[28] = 0
|
lengthBits[28] = 0
|
||||||
lengthBase[28] = 258
|
lengthBase[28] = 258
|
||||||
|
|
||||||
return uncompress
|
return uncompress
|
||||||
|
|
746
lib/init.luau
746
lib/init.luau
|
@ -1,51 +1,51 @@
|
||||||
local inflate = require("./inflate")
|
local inflate = require("./inflate")
|
||||||
local crc32 = require("./crc")
|
local crc32 = require("./crc")
|
||||||
|
|
||||||
-- Little endian constant signatures used in the ZIP file format
|
-- Little endian constant signatures used in the ZIP file format
|
||||||
local SIGNATURES = table.freeze({
|
local SIGNATURES = table.freeze({
|
||||||
-- Marks the beginning of each file in the ZIP
|
-- Marks the beginning of each file in the ZIP
|
||||||
LOCAL_FILE = 0x04034b50,
|
LOCAL_FILE = 0x04034b50,
|
||||||
-- Marks entries in the central directory
|
-- Marks entries in the central directory
|
||||||
CENTRAL_DIR = 0x02014b50,
|
CENTRAL_DIR = 0x02014b50,
|
||||||
-- Marks the end of the central directory
|
-- Marks the end of the central directory
|
||||||
END_OF_CENTRAL_DIR = 0x06054b50,
|
END_OF_CENTRAL_DIR = 0x06054b50,
|
||||||
})
|
})
|
||||||
|
|
||||||
type CrcValidationOptions = {
|
type CrcValidationOptions = {
|
||||||
skip: boolean,
|
skip: boolean,
|
||||||
expected: number,
|
expected: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function validateCrc(decompressed: buffer, validation: CrcValidationOptions)
|
local function validateCrc(decompressed: buffer, validation: CrcValidationOptions)
|
||||||
-- Unless skipping validation is requested, we verify the checksum
|
-- Unless skipping validation is requested, we verify the checksum
|
||||||
if validation.skip then
|
if validation.skip then
|
||||||
local computed = crc32(decompressed)
|
local computed = crc32(decompressed)
|
||||||
assert(
|
assert(
|
||||||
validation.expected == computed,
|
validation.expected == computed,
|
||||||
`Validation failed; CRC checksum does not match: {string.format("%x", computed)} ~= {string.format(
|
`Validation failed; CRC checksum does not match: {string.format("%x", computed)} ~= {string.format(
|
||||||
"%x",
|
"%x",
|
||||||
computed
|
computed
|
||||||
)} (expected ~= got)`
|
)} (expected ~= got)`
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local DECOMPRESSION_ROUTINES: { [number]: (buffer, validation: CrcValidationOptions) -> buffer } = table.freeze({
|
local DECOMPRESSION_ROUTINES: { [number]: (buffer, validation: CrcValidationOptions) -> buffer } = table.freeze({
|
||||||
[0x00] = function(buf, validation)
|
[0x00] = function(buf, validation)
|
||||||
validateCrc(buf, validation)
|
validateCrc(buf, validation)
|
||||||
return buf
|
return buf
|
||||||
end,
|
end,
|
||||||
[0x08] = function(buf, validation)
|
[0x08] = function(buf, validation)
|
||||||
local decompressed = inflate(buf)
|
local decompressed = inflate(buf)
|
||||||
validateCrc(decompressed, validation)
|
validateCrc(decompressed, validation)
|
||||||
return decompressed
|
return decompressed
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- TODO: ERROR HANDLING!
|
-- TODO: ERROR HANDLING!
|
||||||
|
|
||||||
local ZipEntry = {}
|
local ZipEntry = {}
|
||||||
export type ZipEntry = typeof(setmetatable({} :: ZipEntryInner, { __index = ZipEntry }))
|
export type ZipEntry = typeof(setmetatable({} :: ZipEntryInner, { __index = ZipEntry }))
|
||||||
-- stylua: ignore
|
-- stylua: ignore
|
||||||
type ZipEntryInner = {
|
type ZipEntryInner = {
|
||||||
name: string, -- File path within ZIP, '/' suffix indicates directory
|
name: string, -- File path within ZIP, '/' suffix indicates directory
|
||||||
|
@ -57,37 +57,37 @@ type ZipEntryInner = {
|
||||||
parent: ZipEntry?, -- The parent of the current entry, nil for root
|
parent: ZipEntry?, -- The parent of the current entry, nil for root
|
||||||
children: { ZipEntry }, -- The children of the entry
|
children: { ZipEntry }, -- The children of the entry
|
||||||
}
|
}
|
||||||
|
|
||||||
function ZipEntry.new(name: string, size: number, offset: number, timestamp: number, crc: number): ZipEntry
|
function ZipEntry.new(name: string, size: number, offset: number, timestamp: number, crc: number): ZipEntry
|
||||||
return setmetatable(
|
return setmetatable(
|
||||||
{
|
{
|
||||||
name = name,
|
name = name,
|
||||||
size = size,
|
size = size,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
crc = crc,
|
crc = crc,
|
||||||
isDirectory = string.sub(name, -1) == "/",
|
isDirectory = string.sub(name, -1) == "/",
|
||||||
parent = nil,
|
parent = nil,
|
||||||
children = {},
|
children = {},
|
||||||
} :: ZipEntryInner,
|
} :: ZipEntryInner,
|
||||||
{ __index = ZipEntry }
|
{ __index = ZipEntry }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ZipEntry.getPath(self: ZipEntry): string
|
function ZipEntry.getPath(self: ZipEntry): string
|
||||||
local path = self.name
|
local path = self.name
|
||||||
local current = self.parent
|
local current = self.parent
|
||||||
|
|
||||||
while current and current.name ~= "/" do
|
while current and current.name ~= "/" do
|
||||||
path = current.name .. path
|
path = current.name .. path
|
||||||
current = current.parent
|
current = current.parent
|
||||||
end
|
end
|
||||||
|
|
||||||
return path
|
return path
|
||||||
end
|
end
|
||||||
|
|
||||||
local ZipReader = {}
|
local ZipReader = {}
|
||||||
export type ZipReader = typeof(setmetatable({} :: ZipReaderInner, { __index = ZipReader }))
|
export type ZipReader = typeof(setmetatable({} :: ZipReaderInner, { __index = ZipReader }))
|
||||||
-- stylua: ignore
|
-- stylua: ignore
|
||||||
type ZipReaderInner = {
|
type ZipReaderInner = {
|
||||||
data: buffer, -- The buffer containing the raw bytes of the ZIP
|
data: buffer, -- The buffer containing the raw bytes of the ZIP
|
||||||
|
@ -95,296 +95,298 @@ type ZipReaderInner = {
|
||||||
directories: { [string]: ZipEntry }, -- The directories and their respective entries
|
directories: { [string]: ZipEntry }, -- The directories and their respective entries
|
||||||
root: ZipEntry, -- The entry of the root directory
|
root: ZipEntry, -- The entry of the root directory
|
||||||
}
|
}
|
||||||
|
|
||||||
function ZipReader.new(data): ZipReader
|
function ZipReader.new(data): ZipReader
|
||||||
local root = ZipEntry.new("/", 0, 0, 0, 0)
|
local root = ZipEntry.new("/", 0, 0, 0, 0)
|
||||||
root.isDirectory = true
|
root.isDirectory = true
|
||||||
|
|
||||||
local this = setmetatable(
|
local this = setmetatable(
|
||||||
{
|
{
|
||||||
data = data,
|
data = data,
|
||||||
entries = {},
|
entries = {},
|
||||||
directories = {},
|
directories = {},
|
||||||
root = root,
|
root = root,
|
||||||
} :: ZipReaderInner,
|
} :: ZipReaderInner,
|
||||||
{ __index = ZipReader }
|
{ __index = ZipReader }
|
||||||
)
|
)
|
||||||
|
|
||||||
this:parseCentralDirectory()
|
this:parseCentralDirectory()
|
||||||
this:buildDirectoryTree()
|
this:buildDirectoryTree()
|
||||||
return this
|
return this
|
||||||
end
|
end
|
||||||
|
|
||||||
function ZipReader.parseCentralDirectory(self: ZipReader): ()
|
function ZipReader.parseCentralDirectory(self: ZipReader): ()
|
||||||
-- ZIP files are read from the end, starting with the End of Central Directory record
|
-- ZIP files are read from the end, starting with the End of Central Directory record
|
||||||
-- The EoCD is at least 22 bytes and contains pointers to the rest of the ZIP structure
|
-- The EoCD is at least 22 bytes and contains pointers to the rest of the ZIP structure
|
||||||
local bufSize = buffer.len(self.data)
|
local bufSize = buffer.len(self.data)
|
||||||
local pos = bufSize - 22
|
local pos = bufSize - 22
|
||||||
|
|
||||||
-- Search backwards for the EoCD signature
|
-- Search backwards for the EoCD signature
|
||||||
while pos > 0 do
|
while pos > 0 do
|
||||||
-- Read 4 bytes as uint32 in little-endian format
|
-- Read 4 bytes as uint32 in little-endian format
|
||||||
if buffer.readu32(self.data, pos) == SIGNATURES.END_OF_CENTRAL_DIR then
|
if buffer.readu32(self.data, pos) == SIGNATURES.END_OF_CENTRAL_DIR then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
pos = pos - 1
|
pos = pos - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Central Directory offset is stored 16 bytes into the EoCD record
|
-- Central Directory offset is stored 16 bytes into the EoCD record
|
||||||
local cdOffset = buffer.readu32(self.data, pos + 16)
|
local cdOffset = buffer.readu32(self.data, pos + 16)
|
||||||
-- Number of entries is stored 10 bytes into the EoCD record
|
-- Number of entries is stored 10 bytes into the EoCD record
|
||||||
local cdEntries = buffer.readu16(self.data, pos + 10)
|
local cdEntries = buffer.readu16(self.data, pos + 10)
|
||||||
|
|
||||||
-- Process each entry in the Central Directory
|
-- Process each entry in the Central Directory
|
||||||
pos = cdOffset
|
pos = cdOffset
|
||||||
for i = 1, cdEntries do
|
for i = 1, cdEntries do
|
||||||
-- Central Directory Entry format:
|
-- Central Directory Entry format:
|
||||||
-- Offset Bytes Description
|
-- Offset Bytes Description
|
||||||
-- ------------------------------------------------
|
-- ------------------------------------------------
|
||||||
-- 0 4 Central directory entry signature
|
-- 0 4 Central directory entry signature
|
||||||
-- 28 2 File name length (n)
|
-- 28 2 File name length (n)
|
||||||
-- 30 2 Extra field length (m)
|
-- 30 2 Extra field length (m)
|
||||||
-- 32 2 Comment length (k)
|
-- 32 2 Comment length (k)
|
||||||
-- 12 4 Last mod time/date
|
-- 12 4 Last mod time/date
|
||||||
-- 16 4 CRC-32
|
-- 16 4 CRC-32
|
||||||
-- 24 4 Uncompressed size
|
-- 24 4 Uncompressed size
|
||||||
-- 42 4 Local header offset
|
-- 42 4 Local header offset
|
||||||
-- 46 n File name
|
-- 46 n File name
|
||||||
-- 46+n m Extra field
|
-- 46+n m Extra field
|
||||||
-- 46+n+m k Comment
|
-- 46+n+m k Comment
|
||||||
|
|
||||||
local nameLength = buffer.readu16(self.data, pos + 28)
|
local nameLength = buffer.readu16(self.data, pos + 28)
|
||||||
local extraLength = buffer.readu16(self.data, pos + 30)
|
local extraLength = buffer.readu16(self.data, pos + 30)
|
||||||
local commentLength = buffer.readu16(self.data, pos + 32)
|
local commentLength = buffer.readu16(self.data, pos + 32)
|
||||||
local timestamp = buffer.readu32(self.data, pos + 12)
|
local timestamp = buffer.readu32(self.data, pos + 12)
|
||||||
local crc = buffer.readu32(self.data, pos + 16)
|
local crc = buffer.readu32(self.data, pos + 16)
|
||||||
local size = buffer.readu32(self.data, pos + 24)
|
local size = buffer.readu32(self.data, pos + 24)
|
||||||
local offset = buffer.readu32(self.data, pos + 42)
|
local offset = buffer.readu32(self.data, pos + 42)
|
||||||
|
|
||||||
local nameBuffer = buffer.create(nameLength)
|
local nameBuffer = buffer.create(nameLength)
|
||||||
buffer.copy(nameBuffer, 0, self.data, pos + 46, nameLength)
|
buffer.copy(nameBuffer, 0, self.data, pos + 46, nameLength)
|
||||||
local name = buffer.tostring(nameBuffer)
|
local name = buffer.tostring(nameBuffer)
|
||||||
|
|
||||||
local entry = ZipEntry.new(name, size, offset, timestamp, crc)
|
local entry = ZipEntry.new(name, size, offset, timestamp, crc)
|
||||||
table.insert(self.entries, entry)
|
table.insert(self.entries, entry)
|
||||||
|
|
||||||
pos = pos + 46 + nameLength + extraLength + commentLength
|
pos = pos + 46 + nameLength + extraLength + commentLength
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ZipReader.buildDirectoryTree(self: ZipReader): ()
|
function ZipReader.buildDirectoryTree(self: ZipReader): ()
|
||||||
for _, entry in self.entries do
|
for _, entry in self.entries do
|
||||||
local parts = {}
|
local parts = {}
|
||||||
-- Split entry path into individual components
|
-- Split entry path into individual components
|
||||||
-- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"}
|
-- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"}
|
||||||
for part in string.gmatch(entry.name, "([^/]+)/?") do
|
for part in string.gmatch(entry.name, "([^/]+)/?") do
|
||||||
table.insert(parts, part)
|
table.insert(parts, part)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Start from root directory
|
-- Start from root directory
|
||||||
local current = self.root
|
local current = self.root
|
||||||
local path = ""
|
local path = ""
|
||||||
|
|
||||||
-- Process each path component
|
-- Process each path component
|
||||||
for i, part in parts do
|
for i, part in parts do
|
||||||
path ..= part
|
path ..= part
|
||||||
if i < #parts then
|
if i < #parts then
|
||||||
-- Create missing directory entries for intermediate paths
|
-- Create missing directory entries for intermediate paths
|
||||||
if not self.directories[path] then
|
if not self.directories[path] then
|
||||||
local dir = ZipEntry.new(path, 0, 0, entry.timestamp, 0)
|
local dir = ZipEntry.new(path, 0, 0, entry.timestamp, 0)
|
||||||
dir.isDirectory = true
|
dir.isDirectory = true
|
||||||
dir.parent = current
|
dir.parent = current
|
||||||
|
|
||||||
-- Track directory in both lookup table and parent's children
|
-- Track directory in both lookup table and parent's children
|
||||||
self.directories[path] = dir
|
self.directories[path] = dir
|
||||||
table.insert(current.children, dir)
|
table.insert(current.children, dir)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Move deeper into the tree
|
-- Move deeper into the tree
|
||||||
current = self.directories[path]
|
current = self.directories[path]
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Link file entry to its parent directory
|
-- Link file entry to its parent directory
|
||||||
entry.parent = current
|
entry.parent = current
|
||||||
table.insert(current.children, entry)
|
table.insert(current.children, entry)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ZipReader.findEntry(self: ZipReader, path: string): ZipEntry
|
function ZipReader.findEntry(self: ZipReader, path: string): ZipEntry
|
||||||
if path == "/" then
|
if path == "/" then
|
||||||
-- If the root directory's entry was requested we do not
|
-- If the root directory's entry was requested we do not
|
||||||
-- need to do any additional work
|
-- need to do any additional work
|
||||||
return self.root
|
return self.root
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Normalize path by removing leading and trailing slashes
|
-- Normalize path by removing leading and trailing slashes
|
||||||
-- This ensures consistent lookup regardless of input format
|
-- This ensures consistent lookup regardless of input format
|
||||||
-- e.g., "/folder/file.txt/" -> "folder/file.txt"
|
-- e.g., "/folder/file.txt/" -> "folder/file.txt"
|
||||||
path = string.gsub(path, "^/", ""):gsub("/$", "")
|
path = string.gsub(path, "^/", ""):gsub("/$", "")
|
||||||
|
|
||||||
-- First check regular files and explicit directories
|
-- First check regular files and explicit directories
|
||||||
for _, entry in self.entries do
|
for _, entry in self.entries do
|
||||||
-- Compare normalized paths
|
-- Compare normalized paths
|
||||||
if string.gsub(entry.name, "/$", "") == path then
|
if string.gsub(entry.name, "/$", "") == path then
|
||||||
return entry
|
return entry
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If not found, check virtual directory entries
|
-- If not found, check virtual directory entries
|
||||||
-- These are directories that were created implicitly
|
-- These are directories that were created implicitly
|
||||||
return self.directories[path]
|
return self.directories[path]
|
||||||
end
|
end
|
||||||
|
|
||||||
type ExtractionOptions = {
|
type ExtractionOptions = {
|
||||||
decompress: boolean?,
|
decompress: boolean?,
|
||||||
isString: boolean?,
|
isString: boolean?,
|
||||||
skipValidation: boolean?,
|
skipValidation: boolean?,
|
||||||
}
|
}
|
||||||
function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: ExtractionOptions?): buffer | string
|
function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: ExtractionOptions?): buffer | string
|
||||||
-- Local File Header format:
|
-- Local File Header format:
|
||||||
-- Offset Bytes Description
|
-- Offset Bytes Description
|
||||||
-- 0 4 Local file header signature
|
-- 0 4 Local file header signature
|
||||||
-- 8 2 Compression method (8 = DEFLATE)
|
-- 8 2 Compression method (8 = DEFLATE)
|
||||||
-- 14 4 CRC32 checksume
|
-- 14 4 CRC32 checksume
|
||||||
-- 18 4 Compressed size
|
-- 18 4 Compressed size
|
||||||
-- 22 4 Uncompressed size
|
-- 22 4 Uncompressed size
|
||||||
-- 26 2 File name length (n)
|
-- 26 2 File name length (n)
|
||||||
-- 28 2 Extra field length (m)
|
-- 28 2 Extra field length (m)
|
||||||
-- 30 n File name
|
-- 30 n File name
|
||||||
-- 30+n m Extra field
|
-- 30+n m Extra field
|
||||||
-- 30+n+m - File data
|
-- 30+n+m - File data
|
||||||
|
|
||||||
if entry.isDirectory then
|
if entry.isDirectory then
|
||||||
error("Cannot extract directory")
|
error("Cannot extract directory")
|
||||||
end
|
end
|
||||||
|
|
||||||
local defaultOptions: ExtractionOptions = {
|
local defaultOptions: ExtractionOptions = {
|
||||||
decompress = true,
|
decompress = true,
|
||||||
isString = false,
|
isString = false,
|
||||||
skipValidation = false,
|
skipValidation = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO: Use a `Partial` type function for this in the future!
|
-- TODO: Use a `Partial` type function for this in the future!
|
||||||
local optionsOrDefault: {
|
local optionsOrDefault: {
|
||||||
decompress: boolean,
|
decompress: boolean,
|
||||||
isString: boolean,
|
isString: boolean,
|
||||||
skipValidation: boolean
|
skipValidation: boolean,
|
||||||
} = if options then setmetatable(options, { __index = defaultOptions }) :: any else defaultOptions
|
} = if options
|
||||||
|
then setmetatable(options, { __index = defaultOptions }) :: any
|
||||||
local pos = entry.offset
|
else defaultOptions
|
||||||
if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then
|
|
||||||
error("Invalid local file header")
|
local pos = entry.offset
|
||||||
end
|
if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then
|
||||||
|
error("Invalid local file header")
|
||||||
local crcChecksum = buffer.readu32(self.data, pos + 14)
|
end
|
||||||
local compressedSize = buffer.readu32(self.data, pos + 18)
|
|
||||||
local uncompressedSize = buffer.readu32(self.data, pos + 22)
|
local crcChecksum = buffer.readu32(self.data, pos + 14)
|
||||||
local nameLength = buffer.readu16(self.data, pos + 26)
|
local compressedSize = buffer.readu32(self.data, pos + 18)
|
||||||
local extraLength = buffer.readu16(self.data, pos + 28)
|
local uncompressedSize = buffer.readu32(self.data, pos + 22)
|
||||||
|
local nameLength = buffer.readu16(self.data, pos + 26)
|
||||||
pos = pos + 30 + nameLength + extraLength
|
local extraLength = buffer.readu16(self.data, pos + 28)
|
||||||
|
|
||||||
local content = buffer.create(compressedSize)
|
pos = pos + 30 + nameLength + extraLength
|
||||||
buffer.copy(content, 0, self.data, pos, compressedSize)
|
|
||||||
|
local content = buffer.create(compressedSize)
|
||||||
if optionsOrDefault.decompress then
|
buffer.copy(content, 0, self.data, pos, compressedSize)
|
||||||
local compressionMethod = buffer.readu16(self.data, entry.offset + 8)
|
|
||||||
local decompress = DECOMPRESSION_ROUTINES[compressionMethod]
|
if optionsOrDefault.decompress then
|
||||||
if decompress == nil then
|
local compressionMethod = buffer.readu16(self.data, entry.offset + 8)
|
||||||
error(`Unsupported compression, ID: {compressionMethod}`)
|
local decompress = DECOMPRESSION_ROUTINES[compressionMethod]
|
||||||
end
|
if decompress == nil then
|
||||||
|
error(`Unsupported compression, ID: {compressionMethod}`)
|
||||||
content = decompress(content, {
|
end
|
||||||
expected = crcChecksum,
|
|
||||||
skip = optionsOrDefault.skipValidation,
|
content = decompress(content, {
|
||||||
})
|
expected = crcChecksum,
|
||||||
|
skip = optionsOrDefault.skipValidation,
|
||||||
-- Unless skipping validation is requested, we make sure the uncompressed size matches
|
})
|
||||||
assert(
|
|
||||||
optionsOrDefault.skipValidation or uncompressedSize == buffer.len(content),
|
-- Unless skipping validation is requested, we make sure the uncompressed size matches
|
||||||
"Validation failed; uncompressed size does not match"
|
assert(
|
||||||
)
|
optionsOrDefault.skipValidation or uncompressedSize == buffer.len(content),
|
||||||
end
|
"Validation failed; uncompressed size does not match"
|
||||||
|
)
|
||||||
return if optionsOrDefault.isString then buffer.tostring(content) else content
|
end
|
||||||
end
|
|
||||||
|
return if optionsOrDefault.isString then buffer.tostring(content) else content
|
||||||
function ZipReader.extractDirectory(
|
end
|
||||||
self: ZipReader,
|
|
||||||
path: string,
|
function ZipReader.extractDirectory(
|
||||||
options: ExtractionOptions
|
self: ZipReader,
|
||||||
): { [string]: buffer } | { [string]: string }
|
path: string,
|
||||||
local files: { [string]: buffer } | { [string]: string } = {}
|
options: ExtractionOptions
|
||||||
-- Normalize path by removing leading slash for consistent prefix matching
|
): { [string]: buffer } | { [string]: string }
|
||||||
path = string.gsub(path, "^/", "")
|
local files: { [string]: buffer } | { [string]: string } = {}
|
||||||
|
-- Normalize path by removing leading slash for consistent prefix matching
|
||||||
-- Iterate through all entries to find files within target directory
|
path = string.gsub(path, "^/", "")
|
||||||
for _, entry in self.entries do
|
|
||||||
-- Check if entry is a file (not directory) and its path starts with target directory
|
-- Iterate through all entries to find files within target directory
|
||||||
if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then
|
for _, entry in self.entries do
|
||||||
-- Store extracted content mapped to full path
|
-- Check if entry is a file (not directory) and its path starts with target directory
|
||||||
files[entry.name] = self:extract(entry, options)
|
if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then
|
||||||
end
|
-- Store extracted content mapped to full path
|
||||||
end
|
files[entry.name] = self:extract(entry, options)
|
||||||
|
end
|
||||||
-- Return a map of file to contents
|
end
|
||||||
return files
|
|
||||||
end
|
-- Return a map of file to contents
|
||||||
|
return files
|
||||||
function ZipReader.listDirectory(self: ZipReader, path: string): { ZipEntry }
|
end
|
||||||
-- Locate the entry with the path
|
|
||||||
local entry = self:findEntry(path)
|
function ZipReader.listDirectory(self: ZipReader, path: string): { ZipEntry }
|
||||||
if not entry or not entry.isDirectory then
|
-- Locate the entry with the path
|
||||||
-- If an entry was not found, we error
|
local entry = self:findEntry(path)
|
||||||
error("Not a directory")
|
if not entry or not entry.isDirectory then
|
||||||
end
|
-- If an entry was not found, we error
|
||||||
|
error("Not a directory")
|
||||||
-- Return the children of our discovered entry
|
end
|
||||||
return entry.children
|
|
||||||
end
|
-- Return the children of our discovered entry
|
||||||
|
return entry.children
|
||||||
function ZipReader.walk(self: ZipReader, callback: (entry: ZipEntry, depth: number) -> ()): ()
|
end
|
||||||
-- Wrapper function which recursively calls callback for every child
|
|
||||||
-- in an entry
|
function ZipReader.walk(self: ZipReader, callback: (entry: ZipEntry, depth: number) -> ()): ()
|
||||||
local function walkEntry(entry: ZipEntry, depth: number)
|
-- Wrapper function which recursively calls callback for every child
|
||||||
callback(entry, depth)
|
-- in an entry
|
||||||
|
local function walkEntry(entry: ZipEntry, depth: number)
|
||||||
for _, child in entry.children do
|
callback(entry, depth)
|
||||||
-- ooo spooky recursion... blame this if shit go wrong
|
|
||||||
walkEntry(child, depth + 1)
|
for _, child in entry.children do
|
||||||
end
|
-- ooo spooky recursion... blame this if shit go wrong
|
||||||
end
|
walkEntry(child, depth + 1)
|
||||||
|
end
|
||||||
walkEntry(self.root, 0)
|
end
|
||||||
end
|
|
||||||
|
walkEntry(self.root, 0)
|
||||||
export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number }
|
end
|
||||||
function ZipReader.getStats(self: ZipReader): ZipStatistics
|
|
||||||
local stats: ZipStatistics = {
|
export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number }
|
||||||
fileCount = 0,
|
function ZipReader.getStats(self: ZipReader): ZipStatistics
|
||||||
dirCount = 0,
|
local stats: ZipStatistics = {
|
||||||
totalSize = 0,
|
fileCount = 0,
|
||||||
}
|
dirCount = 0,
|
||||||
|
totalSize = 0,
|
||||||
-- Iterate through the entries, updating stats
|
}
|
||||||
for _, entry in self.entries do
|
|
||||||
if entry.isDirectory then
|
-- Iterate through the entries, updating stats
|
||||||
stats.dirCount = stats.dirCount + 1
|
for _, entry in self.entries do
|
||||||
continue
|
if entry.isDirectory then
|
||||||
end
|
stats.dirCount = stats.dirCount + 1
|
||||||
|
continue
|
||||||
stats.fileCount = stats.fileCount + 1
|
end
|
||||||
stats.totalSize = stats.totalSize + entry.size
|
|
||||||
end
|
stats.fileCount = stats.fileCount + 1
|
||||||
|
stats.totalSize = stats.totalSize + entry.size
|
||||||
return stats
|
end
|
||||||
end
|
|
||||||
|
return stats
|
||||||
return {
|
end
|
||||||
-- Creates a `ZipReader` from a `buffer` of ZIP data.
|
|
||||||
load = function(data: buffer)
|
return {
|
||||||
return ZipReader.new(data)
|
-- Creates a `ZipReader` from a `buffer` of ZIP data.
|
||||||
end,
|
load = function(data: buffer)
|
||||||
}
|
return ZipReader.new(data)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
131
pesde.lock
131
pesde.lock
|
@ -2,6 +2,80 @@ name = "0x5eal/unzip"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
target = "luau"
|
target = "luau"
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune"]
|
||||||
|
resolved_ty = "standard"
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "src/init.luau"
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune".dependencies]
|
||||||
|
"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"]
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "jiwonz/dirs"
|
||||||
|
version = "0.1.2"
|
||||||
|
index_url = "https://github.com/daimond113/pesde-index"
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.dependencies]
|
||||||
|
pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||||
|
|
||||||
|
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "src/init.luau"
|
||||||
|
|
||||||
|
[graph."jiwonz/pathfs"."0.1.0 lune"]
|
||||||
|
resolved_ty = "standard"
|
||||||
|
|
||||||
|
[graph."jiwonz/pathfs"."0.1.0 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "init.luau"
|
||||||
|
|
||||||
|
[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "jiwonz/pathfs"
|
||||||
|
version = "0.1.0"
|
||||||
|
index_url = "https://github.com/daimond113/pesde-index"
|
||||||
|
|
||||||
|
[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "init.luau"
|
||||||
|
|
||||||
|
[graph."lukadev_0/option"."1.2.0 lune"]
|
||||||
|
resolved_ty = "standard"
|
||||||
|
|
||||||
|
[graph."lukadev_0/option"."1.2.0 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
|
[graph."lukadev_0/option"."1.2.0 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "lukadev_0/option"
|
||||||
|
version = "1.2.0"
|
||||||
|
index_url = "https://github.com/daimond113/pesde-index"
|
||||||
|
|
||||||
|
[graph."lukadev_0/option"."1.2.0 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
|
[graph."lukadev_0/result"."1.2.0 lune"]
|
||||||
|
resolved_ty = "standard"
|
||||||
|
|
||||||
|
[graph."lukadev_0/result"."1.2.0 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
|
[graph."lukadev_0/result"."1.2.0 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "lukadev_0/result"
|
||||||
|
version = "1.2.0"
|
||||||
|
index_url = "https://github.com/daimond113/pesde-index"
|
||||||
|
|
||||||
|
[graph."lukadev_0/result"."1.2.0 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
[graph."lukadev_0/result"."1.2.0 luau"]
|
[graph."lukadev_0/result"."1.2.0 luau"]
|
||||||
direct = ["result", { name = "lukadev_0/result", version = "^1.2.0" }, "standard"]
|
direct = ["result", { name = "lukadev_0/result", version = "^1.2.0" }, "standard"]
|
||||||
resolved_ty = "standard"
|
resolved_ty = "standard"
|
||||||
|
@ -19,3 +93,60 @@ index_url = "https://github.com/pesde-pkg/index"
|
||||||
[graph."lukadev_0/result"."1.2.0 luau".pkg_ref.target]
|
[graph."lukadev_0/result"."1.2.0 luau".pkg_ref.target]
|
||||||
environment = "luau"
|
environment = "luau"
|
||||||
lib = "lib/init.luau"
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune"]
|
||||||
|
direct = ["stylua", { name = "pesde/stylua", version = "^2.0.2", target = "lune" }, "dev"]
|
||||||
|
resolved_ty = "dev"
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
bin = "init.luau"
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune".dependencies]
|
||||||
|
"lukadev_0/option" = ["1.2.0 lune", "option"]
|
||||||
|
"lukadev_0/result" = ["1.2.0 lune", "result"]
|
||||||
|
"pesde/toolchainlib" = ["0.1.7 lune", "core"]
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "pesde/stylua"
|
||||||
|
version = "2.0.2"
|
||||||
|
index_url = "https://github.com/pesde-pkg/index"
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune".pkg_ref.dependencies]
|
||||||
|
core = [{ name = "pesde/toolchainlib", version = "^0.1.3", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "standard"]
|
||||||
|
option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||||
|
result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||||
|
|
||||||
|
[graph."pesde/stylua"."2.0.2 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
bin = "init.luau"
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune"]
|
||||||
|
resolved_ty = "standard"
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune".target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "src/init.luau"
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune".dependencies]
|
||||||
|
"jiwonz/dirs" = ["0.1.2 lune", "dirs"]
|
||||||
|
"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"]
|
||||||
|
"lukadev_0/option" = ["1.2.0 lune", "option"]
|
||||||
|
"lukadev_0/result" = ["1.2.0 lune", "result"]
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune".pkg_ref]
|
||||||
|
ref_ty = "pesde"
|
||||||
|
name = "pesde/toolchainlib"
|
||||||
|
version = "0.1.7"
|
||||||
|
index_url = "https://github.com/daimond113/pesde-index"
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune".pkg_ref.dependencies]
|
||||||
|
dirs = [{ name = "jiwonz/dirs", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||||
|
option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||||
|
pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||||
|
result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||||
|
|
||||||
|
[graph."pesde/toolchainlib"."0.1.7 lune".pkg_ref.target]
|
||||||
|
environment = "lune"
|
||||||
|
lib = "src/init.luau"
|
||||||
|
|
|
@ -13,3 +13,6 @@ default = "https://github.com/pesde-pkg/index"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
result = { name = "lukadev_0/result", version = "^1.2.0" }
|
result = { name = "lukadev_0/result", version = "^1.2.0" }
|
||||||
|
|
||||||
|
[dev_dependencies]
|
||||||
|
stylua = { name = "pesde/stylua", version = "^2.0.2", target = "lune" }
|
||||||
|
|
Loading…
Add table
Reference in a new issue