From 5fb55268f4615484e9539fe19cd24ac0d55a3f93 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 19 Feb 2025 18:28:00 +0000 Subject: [PATCH] fix: address errors in moonwave comments and convert indentation --- lib/init.luau | 1342 +++++++++++++++++++++++++------------------------ 1 file changed, 672 insertions(+), 670 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 6c159a2..5b07824 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -7,125 +7,126 @@ local MAX_SUPPORTED_PKZIP_VERSION = 63 -- Little endian constant signatures used in the ZIP file format local SIGNATURES = table.freeze({ - -- Marks the beginning of each file in the ZIP - LOCAL_FILE = 0x04034b50, - -- Marks the start of an data descriptor - DATA_DESCRIPTOR = 0x08074b50, - -- Marks entries in the central directory - CENTRAL_DIR = 0x02014b50, - -- Marks the end of the central directory - END_OF_CENTRAL_DIR = 0x06054b50, + -- Marks the beginning of each file in the ZIP + LOCAL_FILE = 0x04034b50, + -- Marks the start of an data descriptor + DATA_DESCRIPTOR = 0x08074b50, + -- Marks entries in the central directory + CENTRAL_DIR = 0x02014b50, + -- Marks the end of the central directory + END_OF_CENTRAL_DIR = 0x06054b50, }) -- Decompression routines for each supported compression method local DECOMPRESSION_ROUTINES: { [number]: { name: CompressionMethod, decompress: (buffer, number, validateCrc.CrcValidationOptions) -> buffer } } = - { - -- `STORE` decompression method - No compression - [0x00] = { - name = "STORE" :: CompressionMethod, - decompress = function(buf, _, validation): buffer - validateCrc(buf, validation) - return buf - end, - }, + { + -- `STORE` decompression method - No compression + [0x00] = { + name = "STORE" :: CompressionMethod, + decompress = function(buf, _, validation): buffer + validateCrc(buf, validation) + return buf + end, + }, - -- `DEFLATE` decompression method - Compressed raw deflate chunks - [0x08] = { - name = "DEFLATE" :: CompressionMethod, - decompress = function(buf, uncompressedSize, validation): buffer - local decompressed = inflate(buf, uncompressedSize) - validateCrc(decompressed, validation) - return decompressed - end, - }, - } + -- `DEFLATE` decompression method - Compressed raw deflate chunks + [0x08] = { + name = "DEFLATE" :: CompressionMethod, + decompress = function(buf, uncompressedSize, validation): buffer + local decompressed = inflate(buf, uncompressedSize) + validateCrc(decompressed, validation) + return decompressed + end, + }, + } -- Set of placeholder entry properties for incompatible entries local EMPTY_PROPERTIES: ZipEntryProperties = table.freeze({ - versionMadeBy = 0, - compressedSize = 0, - size = 0, - attributes = 0, - timestamp = 0, - crc = 0, + versionMadeBy = 0, + compressedSize = 0, + size = 0, + attributes = 0, + timestamp = 0, + crc = 0, }) -- Lookup table for the OS that created the ZIP local MADE_BY_OS_LOOKUP: { [number]: MadeByOS } = { - [0x0] = "FAT", - [0x1] = "AMIGA", - [0x2] = "VMS", - [0x3] = "UNIX", - [0x4] = "VM/CMS", - [0x5] = "Atari ST", - [0x6] = "OS/2", - [0x7] = "MAC", - [0x8] = "Z-System", - [0x9] = "CP/M", - [0xa] = "NTFS", - [0xb] = "MVS", - [0xc] = "VSE", - [0xd] = "Acorn RISCOS", - [0xe] = "VFAT", - [0xf] = "Alternate MVS", - [0x10] = "BeOS", - [0x11] = "TANDEM", - [0x12] = "OS/400", - [0x13] = "OS/X", + [0x0] = "FAT", + [0x1] = "AMIGA", + [0x2] = "VMS", + [0x3] = "UNIX", + [0x4] = "VM/CMS", + [0x5] = "Atari ST", + [0x6] = "OS/2", + [0x7] = "MAC", + [0x8] = "Z-System", + [0x9] = "CP/M", + [0xa] = "NTFS", + [0xb] = "MVS", + [0xc] = "VSE", + [0xd] = "Acorn RISCOS", + [0xe] = "VFAT", + [0xf] = "Alternate MVS", + [0x10] = "BeOS", + [0x11] = "TANDEM", + [0x12] = "OS/400", + [0x13] = "OS/X", } --[=[ - @class ZipEntry - - A single entry (a file or a directory) in a ZIP file, and its properties. + @class ZipEntry + + A single entry (a file or a directory) in a ZIP file, and its properties. ]=] local ZipEntry = {} --[=[ - @interface ZipEntry - @within ZipEntry + @interface ZipEntry + @within ZipEntry - @field name string -- File path within ZIP, '/' suffix indicates directory - @field versionMadeBy { software: string, os: MadeByOS } -- Version of software and OS that created the ZIP - @field compressedSize number -- Compressed size in bytes - @field size number -- Uncompressed size in bytes - @field offset number -- Absolute position of local header in ZIP - @field timestamp number -- MS-DOS format timestamp - @field method CompressionMethod -- Method used to compress the file - @field crc number -- CRC32 checksum of the uncompressed data - @field isDirectory boolean -- Whether the entry is a directory or not - @field isText boolean -- Whether the entry is plain ASCII text or binary - @field attributes number -- File attributes - @field parent ZipEntry? -- Parent directory entry, `nil` if entry is root - @field children { ZipEntry } -- Children of the entry, if it was a directory, empty array for files + @field name string -- File path within ZIP, '/' suffix indicates directory + @field versionMadeBy { software: string, os: MadeByOS } -- Version of software and OS that created the ZIP + @field compressedSize number -- Compressed size in bytes + @field size number -- Uncompressed size in bytes + @field offset number -- Absolute position of local header in ZIP + @field timestamp number -- MS-DOS format timestamp + @field method CompressionMethod -- Method used to compress the file + @field crc number -- CRC32 checksum of the uncompressed data + @field isDirectory boolean -- Whether the entry is a directory or not + @field isText boolean -- Whether the entry is plain ASCII text or binary + @field attributes number -- File attributes + @field parent ZipEntry? -- Parent directory entry, `nil` if entry is root + @field children { ZipEntry } -- Children of the entry, if it was a directory, empty array for files ]=] export type ZipEntry = typeof(setmetatable({} :: ZipEntryInner, { __index = ZipEntry })) type ZipEntryInner = { - name: string, + name: string, - versionMadeBy: { - software: string, - os: MadeByOS, - }, + versionMadeBy: { + software: string, + os: MadeByOS, + }, - compressedSize: number, - size: number, - offset: number, - timestamp: number, - method: CompressionMethod, - crc: number, - isDirectory: boolean, - isText: boolean, - attributes: number, - parent: ZipEntry?, - children: { ZipEntry }, + compressedSize: number, + size: number, + offset: number, + timestamp: number, + method: CompressionMethod, + crc: number, + isDirectory: boolean, + isText: boolean, + attributes: number, + parent: ZipEntry?, + children: { ZipEntry }, } -- stylua: ignore --[=[ - @type MadeByOS +@within ZipEntry +@type MadeByOS "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown" - The OS that created the ZIP. + The OS that created the ZIP. ]=] export type MadeByOS = | "FAT" -- 0x0; MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) @@ -151,811 +152,812 @@ export type MadeByOS = | "Unknown" -- 0x14 - 0xff; Unused --[=[ - @type CompressionMethod + @within ZipEntry + @type CompressionMethod "STORE" | "DEFLATE" - The method used to compress the file: either `STORE` or `DEFLATE`. - - - `STORE` - No compression - - `DEFLATE` - Compressed raw deflate chunks + The method used to compress the file: + - `STORE` - No compression + - `DEFLATE` - Compressed raw deflate chunks ]=] export type CompressionMethod = "STORE" | "DEFLATE" --[=[ - @interface ZipEntryProperties - @within ZipEntry - @private + @interface ZipEntryProperties + @within ZipEntry + @private - A set of properties that describe a ZIP entry. Used internally for construction of - [ZipEntry] objects. + A set of properties that describe a ZIP entry. Used internally for construction of + [ZipEntry] objects. ]=] type ZipEntryProperties = { - versionMadeBy: number, - compressedSize: number, - size: number, - attributes: number, - timestamp: number, - method: CompressionMethod?, - crc: number, + versionMadeBy: number, + compressedSize: number, + size: number, + attributes: number, + timestamp: number, + method: CompressionMethod?, + crc: number, } --[=[ - @within ZipEntry - @function new - @private - - @param offset number -- Offset of the entry in the ZIP file - @param name string -- File path within ZIP, '/' suffix indicates directory - @param properties ZipEntryProperties -- Properties of the entry - @return ZipEntry -- The constructed entry ---]=] -function ZipEntry.new(offset: number, name: string, properties: ZipEntryProperties): ZipEntry - local versionMadeByOS = bit32.rshift(properties.versionMadeBy, 8) - local versionMadeByVersion = bit32.band(properties.versionMadeBy, 0x00ff) + @within ZipEntry + @function new + @private - return setmetatable( - { - name = name, - versionMadeBy = { - software = string.format("%d.%d", versionMadeByVersion / 10, versionMadeByVersion % 10), - os = MADE_BY_OS_LOOKUP[versionMadeByOS] :: MadeByOS, - }, - compressedSize = properties.compressedSize, - size = properties.size, - offset = offset, - timestamp = properties.timestamp, - method = properties.method, - crc = properties.crc, - isDirectory = string.sub(name, -1) == "/", - attributes = properties.attributes, - parent = nil, - children = {}, - } :: ZipEntryInner, - { __index = ZipEntry } - ) + @param offset number -- Offset of the entry in the ZIP file + @param name string -- File path within ZIP, '/' suffix indicates directory + @param properties ZipEntryProperties -- Properties of the entry + @return ZipEntry -- The constructed entry +]=] +function ZipEntry.new(offset: number, name: string, properties: ZipEntryProperties): ZipEntry + local versionMadeByOS = bit32.rshift(properties.versionMadeBy, 8) + local versionMadeByVersion = bit32.band(properties.versionMadeBy, 0x00ff) + + return setmetatable( + { + name = name, + versionMadeBy = { + software = string.format("%d.%d", versionMadeByVersion / 10, versionMadeByVersion % 10), + os = MADE_BY_OS_LOOKUP[versionMadeByOS] :: MadeByOS, + }, + compressedSize = properties.compressedSize, + size = properties.size, + offset = offset, + timestamp = properties.timestamp, + method = properties.method, + crc = properties.crc, + isDirectory = string.sub(name, -1) == "/", + attributes = properties.attributes, + parent = nil, + children = {}, + } :: ZipEntryInner, + { __index = ZipEntry } + ) end --[=[ - @within ZipEntry - @method isSymlink + @within ZipEntry + @method isSymlink - Returns whether the entry is a symlink. + Returns whether the entry is a symlink. - @return boolean + @return boolean ]=] function ZipEntry.isSymlink(self: ZipEntry): boolean - return bit32.band(self.attributes, 0xA0000000) == 0xA0000000 + return bit32.band(self.attributes, 0xA0000000) == 0xA0000000 end --[=[ - @within ZipEntry - @method getPath + @within ZipEntry + @method getPath - Resolves the path of the entry based on its relationship with other entries. It is recommended to use this - method instead of accessing the `name` property directly, although they should be equivalent. + Resolves the path of the entry based on its relationship with other entries. It is recommended to use this + method instead of accessing the `name` property directly, although they should be equivalent. - :::warning - Never use this method when extracting files from the ZIP, since it can contain absolute paths - (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`), - causing unintended overwrites of files. - ::: + :::warning + Never use this method when extracting files from the ZIP, since it can contain absolute paths + (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`), + causing unintended overwrites of files. + ::: - @return string -- The path of the entry + @return string -- The path of the entry ]=] function ZipEntry.getPath(self: ZipEntry): string - if self.name == "/" then - return "/" - end + if self.name == "/" then + return "/" + end - -- Get just the entry name without the path - local name = string.match(self.name, "([^/]+)/?$") or self.name + -- Get just the entry name without the path + local name = string.match(self.name, "([^/]+)/?$") or self.name - if not self.parent or self.parent.name == "/" then - return self.name - end + if not self.parent or self.parent.name == "/" then + return self.name + end - -- Combine parent path with entry name - local path = string.gsub(self.parent:getPath() .. name, "//+", "/") - return path + -- Combine parent path with entry name + local path = string.gsub(self.parent:getPath() .. name, "//+", "/") + return path end --[=[ - @within ZipEntry - @method getSafePath + @within ZipEntry + @method getSafePath - Resolves the path of the entry based on its relationship with other entries and returns it - only if it is safe to use for extraction, otherwise returns `nil`. + Resolves the path of the entry based on its relationship with other entries and returns it + only if it is safe to use for extraction, otherwise returns `nil`. - @return string? -- Optional path of the entry if it was safe + @return string? -- Optional path of the entry if it was safe ]=] function ZipEntry.getSafePath(self: ZipEntry): string? - local pathStr = self:getPath() + local pathStr = self:getPath() - if path.isSafe(pathStr) then - return pathStr - end + if path.isSafe(pathStr) then + return pathStr + end - return nil + return nil end --[=[ - @within ZipEntry - @method sanitizePath + @within ZipEntry + @method sanitizePath - Sanitizes the path of the entry, potentially losing information, but ensuring the path is - safe to use for extraction. + Sanitizes the path of the entry, potentially losing information, but ensuring the path is + safe to use for extraction. - @return string -- The sanitized path of the entry + @return string -- The sanitized path of the entry ]=] function ZipEntry.sanitizePath(self: ZipEntry): string - local pathStr = self:getPath() - return path.sanitize(pathStr) + local pathStr = self:getPath() + return path.sanitize(pathStr) end --[=[ - @within ZipEntry - @method getParent + @within ZipEntry + @method getParent - Calculates the compression efficiency of the entry, or `nil` if the entry is a directory. + Calculates the compression efficiency of the entry, or `nil` if the entry is a directory. - Uses the formula: `round((1 - compressedSize / size) * 100)` and outputs a percentage. + Uses the formula: `round((1 - compressedSize / size) * 100)` and outputs a percentage. - @return number? -- Optional compression efficiency of the entry + @return number? -- Optional compression efficiency of the entry ]=] function ZipEntry.compressionEfficiency(self: ZipEntry): number? - if self.size == 0 or self.compressedSize == 0 then - return nil - end + if self.size == 0 or self.compressedSize == 0 then + return nil + end - local ratio = 1 - self.compressedSize / self.size - return math.round(ratio * 100) + local ratio = 1 - self.compressedSize / self.size + return math.round(ratio * 100) end --[=[ - @within ZipEntry - @method isFile + @within ZipEntry + @method isFile - Returns whether the entry is a file, i.e., not a directory or symlink. + Returns whether the entry is a file, i.e., not a directory or symlink. - @return boolean -- Whether the entry is a file + @return boolean -- Whether the entry is a file ]=] function ZipEntry.isFile(self: ZipEntry): boolean - return not (self.isDirectory and self:isSymlink()) + return not (self.isDirectory and self:isSymlink()) end --[=[ - @interface UnixMode + @within ZipEntry + @interface UnixMode - A object representation of the UNIX mode. + A object representation of the UNIX mode. - @field perms -- The permission octal - @field typeFlags -- The type flags octal + @field perms string -- The permission octal + @field typeFlags string -- The type flags octal ]=] export type UnixMode = { perms: string, typeFlags: string } --[=[ - @within ZipEntry - @method unixMode + @within ZipEntry + @method unixMode - Parses the entry's attributes to extract a UNIX mode, represented as a [UnixMode]. + Parses the entry's attributes to extract a UNIX mode, represented as a [UnixMode]. - @return UnixMode? -- The UNIX mode of the entry, or `nil` if the entry is not a UNIX file + @return UnixMode? -- The UNIX mode of the entry, or `nil` if the entry is not a UNIX file ]=] function ZipEntry.unixMode(self: ZipEntry): UnixMode? - if self.versionMadeBy.os ~= "UNIX" then - return nil - end + if self.versionMadeBy.os ~= "UNIX" then + return nil + end - local mode = bit32.rshift(self.attributes, 16) - local typeFlags = bit32.band(self.attributes, 0x1FF) - local perms = bit32.band(mode, 0x01FF) + local mode = bit32.rshift(self.attributes, 16) + local typeFlags = bit32.band(self.attributes, 0x1FF) + local perms = bit32.band(mode, 0x01FF) - return { - perms = string.format("0o%o", perms), - typeFlags = string.format("0o%o", typeFlags), - } + return { + perms = string.format("0o%o", perms), + typeFlags = string.format("0o%o", typeFlags), + } end --[=[ - @class ZipReader + @class ZipReader - The main class which represents a decoded state of a ZIP file, holding references - to its entries. This is the primary point of interaction with the ZIP file's contents. + The main class which represents a decoded state of a ZIP file, holding references + to its entries. This is the primary point of interaction with the ZIP file's contents. ]=] local ZipReader = {} --[=[ - @interface ZipReader - @within ZipReader + @interface ZipReader + @within ZipReader - @field data -- The buffer containing the raw bytes of the ZIP - @field comment -- Comment associated with the ZIP - @field entries -- The decoded entries present - @field directories -- The directories and their respective entries - @field root -- The entry of the root directory + @field data buffer -- The buffer containing the raw bytes of the ZIP + @field comment string -- Comment associated with the ZIP + @field entries { ZipEntry } -- The decoded entries present + @field directories { [string]: ZipEntry } -- The directories and their respective entries + @field root ZipEntry -- The entry of the root directory ]=] export type ZipReader = typeof(setmetatable({} :: ZipReaderInner, { __index = ZipReader })) type ZipReaderInner = { - data: buffer, - comment: string, - entries: { ZipEntry }, - directories: { [string]: ZipEntry }, - root: ZipEntry, + data: buffer, + comment: string, + entries: { ZipEntry }, + directories: { [string]: ZipEntry }, + root: ZipEntry, } --[=[ - @within ZipReader - @function new + @within ZipReader + @function new - Creates a new ZipReader instance from the raw bytes of a ZIP file. - - **Errors if the ZIP file is invalid.** + Creates a new ZipReader instance from the raw bytes of a ZIP file. + + **Errors if the ZIP file is invalid.** - @param data -- The buffer containing the raw bytes of the ZIP - @return ZipReader -- The new ZipReader instance + @param data buffer -- The buffer containing the raw bytes of the ZIP + @return ZipReader -- The new ZipReader instance ]=] function ZipReader.new(data): ZipReader - local root = ZipEntry.new(0, "/", EMPTY_PROPERTIES) - root.isDirectory = true + local root = ZipEntry.new(0, "/", EMPTY_PROPERTIES) + root.isDirectory = true - local this = setmetatable( - { - data = data, - entries = {}, - directories = {}, - root = root, - } :: ZipReaderInner, - { __index = ZipReader } - ) + local this = setmetatable( + { + data = data, + entries = {}, + directories = {}, + root = root, + } :: ZipReaderInner, + { __index = ZipReader } + ) - this:parseCentralDirectory() - this:buildDirectoryTree() - return this + this:parseCentralDirectory() + this:buildDirectoryTree() + return this end --[=[ - @within ZipReader - @method parseCentralDirectory - @private + @within ZipReader + @method parseCentralDirectory + @private - Parses the central directory of the ZIP file and populates the `entries` and `directories` - fields. Used internally during initialization of the [ZipReader]. + Parses the central directory of the ZIP file and populates the `entries` and `directories` + fields. Used internally during initialization of the [ZipReader]. - **Errors if the ZIP file is invalid.** - - @error "Could not find End of Central Directory signature" - @error "Invalid Central Directory offset or size" - @error "Invalid Central Directory entry signature" - @error "Found different entries than specified in Central Directory" + **Errors if the ZIP file is invalid.** + + @error "Could not find End of Central Directory signature" + @error "Invalid Central Directory offset or size" + @error "Invalid Central Directory entry signature" + @error "Found different entries than specified in Central Directory" ]=] function ZipReader.parseCentralDirectory(self: ZipReader): () - -- 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 - local bufSize = buffer.len(self.data) + -- 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 + local bufSize = buffer.len(self.data) - -- Start from the minimum possible position of EoCD (22 bytes from end) - local minPos = math.max(0, bufSize - (22 + 65535) --[[ max comment size: 64 KiB ]]) - local pos = bufSize - 22 + -- Start from the minimum possible position of EoCD (22 bytes from end) + local minPos = math.max(0, bufSize - (22 + 65535) --[[ max comment size: 64 KiB ]]) + local pos = bufSize - 22 - -- Search backwards for the EoCD signature - while pos >= minPos do - if buffer.readu32(self.data, pos) == SIGNATURES.END_OF_CENTRAL_DIR then - break - end - pos -= 1 - end + -- Search backwards for the EoCD signature + while pos >= minPos do + if buffer.readu32(self.data, pos) == SIGNATURES.END_OF_CENTRAL_DIR then + break + end + pos -= 1 + end - -- Verify we found the signature - if pos < minPos then - error("Could not find End of Central Directory signature") - end + -- Verify we found the signature + if pos < minPos then + error("Could not find End of Central Directory signature") + end - -- End of Central Directory format: - -- Offset Bytes Description - -- 0 4 End of central directory signature - -- 4 2 Number of this disk - -- 6 2 Disk where central directory starts - -- 8 2 Number of central directory records on this disk - -- 10 2 Total number of central directory records - -- 12 4 Size of central directory (bytes) - -- 16 4 Offset of start of central directory - -- 20 2 Comment length (n) - -- 22 n Comment + -- End of Central Directory format: + -- Offset Bytes Description + -- 0 4 End of central directory signature + -- 4 2 Number of this disk + -- 6 2 Disk where central directory starts + -- 8 2 Number of central directory records on this disk + -- 10 2 Total number of central directory records + -- 12 4 Size of central directory (bytes) + -- 16 4 Offset of start of central directory + -- 20 2 Comment length (n) + -- 22 n Comment - local cdSize = buffer.readu32(self.data, pos + 12) - local cdEntries = buffer.readu16(self.data, pos + 10) - local cdOffset = buffer.readu32(self.data, pos + 16) + local cdSize = buffer.readu32(self.data, pos + 12) + local cdEntries = buffer.readu16(self.data, pos + 10) + local cdOffset = buffer.readu32(self.data, pos + 16) - -- Strict validation of CD boundaries and entry count - if cdOffset >= bufSize or cdOffset + cdSize > bufSize then - error("Invalid Central Directory offset or size") - end + -- Strict validation of CD boundaries and entry count + if cdOffset >= bufSize or cdOffset + cdSize > bufSize then + error("Invalid Central Directory offset or size") + end - -- Track actual entries found - local entriesFound = 0 - pos = cdOffset - while pos < cdOffset + cdSize do - if buffer.readu32(self.data, pos) ~= SIGNATURES.CENTRAL_DIR then - error("Invalid Central Directory entry signature") - end + -- Track actual entries found + local entriesFound = 0 + pos = cdOffset + while pos < cdOffset + cdSize do + if buffer.readu32(self.data, pos) ~= SIGNATURES.CENTRAL_DIR then + error("Invalid Central Directory entry signature") + end - -- Central Directory Entry format: - -- Offset Bytes Description - -- 0 4 Central directory entry signature - -- 4 2 Version made by - -- 8 2 General purpose bitflags - -- 10 2 Compression method (8 = DEFLATE) - -- 12 4 Last mod time/date - -- 16 4 CRC-32 - -- 20 4 Compressed size - -- 24 4 Uncompressed size - -- 28 2 File name length (n) - -- 30 2 Extra field length (m) - -- 32 2 Comment length (k) - -- 36 2 Internal file attributes - -- 38 4 External file attributes - -- 42 4 Local header offset - -- 46 n File name - -- 46+n m Extra field - -- 46+n+m k Comment + -- Central Directory Entry format: + -- Offset Bytes Description + -- 0 4 Central directory entry signature + -- 4 2 Version made by + -- 8 2 General purpose bitflags + -- 10 2 Compression method (8 = DEFLATE) + -- 12 4 Last mod time/date + -- 16 4 CRC-32 + -- 20 4 Compressed size + -- 24 4 Uncompressed size + -- 28 2 File name length (n) + -- 30 2 Extra field length (m) + -- 32 2 Comment length (k) + -- 36 2 Internal file attributes + -- 38 4 External file attributes + -- 42 4 Local header offset + -- 46 n File name + -- 46+n m Extra field + -- 46+n+m k Comment - local versionMadeBy = buffer.readu16(self.data, pos + 4) - local _bitflags = buffer.readu16(self.data, pos + 8) - local timestamp = buffer.readu32(self.data, pos + 12) - local compressionMethod = buffer.readu16(self.data, pos + 10) - local crc = buffer.readu32(self.data, pos + 16) - local compressedSize = buffer.readu32(self.data, pos + 20) - local size = buffer.readu32(self.data, pos + 24) - local nameLength = buffer.readu16(self.data, pos + 28) - local extraLength = buffer.readu16(self.data, pos + 30) - local commentLength = buffer.readu16(self.data, pos + 32) - local internalAttrs = buffer.readu16(self.data, pos + 36) - local externalAttrs = buffer.readu32(self.data, pos + 38) - local offset = buffer.readu32(self.data, pos + 42) - local name = buffer.readstring(self.data, pos + 46, nameLength) + local versionMadeBy = buffer.readu16(self.data, pos + 4) + local _bitflags = buffer.readu16(self.data, pos + 8) + local timestamp = buffer.readu32(self.data, pos + 12) + local compressionMethod = buffer.readu16(self.data, pos + 10) + local crc = buffer.readu32(self.data, pos + 16) + local compressedSize = buffer.readu32(self.data, pos + 20) + local size = buffer.readu32(self.data, pos + 24) + local nameLength = buffer.readu16(self.data, pos + 28) + local extraLength = buffer.readu16(self.data, pos + 30) + local commentLength = buffer.readu16(self.data, pos + 32) + local internalAttrs = buffer.readu16(self.data, pos + 36) + local externalAttrs = buffer.readu32(self.data, pos + 38) + local offset = buffer.readu32(self.data, pos + 42) + local name = buffer.readstring(self.data, pos + 46, nameLength) - table.insert( - self.entries, - ZipEntry.new(offset, name, { - versionMadeBy = versionMadeBy, - compressedSize = compressedSize, - size = size, - crc = crc, - method = DECOMPRESSION_ROUTINES[compressionMethod].name :: CompressionMethod, - timestamp = timestamp, - attributes = externalAttrs, - isText = bit32.band(internalAttrs, 0x0001) ~= 0, - }) - ) + table.insert( + self.entries, + ZipEntry.new(offset, name, { + versionMadeBy = versionMadeBy, + compressedSize = compressedSize, + size = size, + crc = crc, + method = DECOMPRESSION_ROUTINES[compressionMethod].name :: CompressionMethod, + timestamp = timestamp, + attributes = externalAttrs, + isText = bit32.band(internalAttrs, 0x0001) ~= 0, + }) + ) - pos = pos + 46 + nameLength + extraLength + commentLength - entriesFound += 1 - end + pos = pos + 46 + nameLength + extraLength + commentLength + entriesFound += 1 + end - if entriesFound ~= cdEntries then - error("Found different entries than specified in Central Directory") - end + if entriesFound ~= cdEntries then + error("Found different entries than specified in Central Directory") + end - local cdCommentLength = buffer.readu16(self.data, pos + 20) - self.comment = buffer.readstring(self.data, pos + 22, cdCommentLength) + local cdCommentLength = buffer.readu16(self.data, pos + 20) + self.comment = buffer.readstring(self.data, pos + 22, cdCommentLength) end --[=[ - @within ZipReader - @method buildDirectoryTree - @private + @within ZipReader + @method buildDirectoryTree + @private - Builds the directory tree from the entries. Used internally during initialization of the - [ZipReader]. + Builds the directory tree from the entries. Used internally during initialization of the + [ZipReader]. ]=] function ZipReader.buildDirectoryTree(self: ZipReader): () - -- Sort entries to process directories first; I could either handle - -- directories and files in separate passes over the entries, or sort - -- the entries so I handled the directories first -- I decided to do - -- the latter - table.sort(self.entries, function(a, b) - if a.isDirectory ~= b.isDirectory then - return a.isDirectory - end - return a.name < b.name - end) + -- Sort entries to process directories first; I could either handle + -- directories and files in separate passes over the entries, or sort + -- the entries so I handled the directories first -- I decided to do + -- the latter + table.sort(self.entries, function(a, b) + if a.isDirectory ~= b.isDirectory then + return a.isDirectory + end + return a.name < b.name + end) - for _, entry in self.entries do - local parts = {} - -- Split entry path into individual components - -- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"} - for part in string.gmatch(entry.name, "([^/]+)/?") do - table.insert(parts, part) - end + for _, entry in self.entries do + local parts = {} + -- Split entry path into individual components + -- e.g. "folder/subfolder/file.txt" -> {"folder", "subfolder", "file.txt"} + for part in string.gmatch(entry.name, "([^/]+)/?") do + table.insert(parts, part) + end - -- Start from root directory - local current = self.root - local path = "" + -- Start from root directory + local current = self.root + local path = "" - -- Process each path component - for i, part in parts do - path ..= part + -- Process each path component + for i, part in parts do + path ..= part - if i < #parts or entry.isDirectory then - -- Create missing directory entries for intermediate paths - if not self.directories[path] then - if entry.isDirectory and i == #parts then - -- Existing directory entry, reuse it - self.directories[path] = entry - else - -- Create new directory entry for intermediate paths or undefined - -- parent directories in the ZIP - local dir = ZipEntry.new(0, path .. "/", { - versionMadeBy = 0, - compressedSize = 0, - size = 0, - crc = 0, - compressionMethod = "STORED", - timestamp = entry.timestamp, - attributes = entry.attributes, - }) + if i < #parts or entry.isDirectory then + -- Create missing directory entries for intermediate paths + if not self.directories[path] then + if entry.isDirectory and i == #parts then + -- Existing directory entry, reuse it + self.directories[path] = entry + else + -- Create new directory entry for intermediate paths or undefined + -- parent directories in the ZIP + local dir = ZipEntry.new(0, path .. "/", { + versionMadeBy = 0, + compressedSize = 0, + size = 0, + crc = 0, + compressionMethod = "STORED", + timestamp = entry.timestamp, + attributes = entry.attributes, + }) - dir.versionMadeBy = entry.versionMadeBy - dir.isDirectory = true - dir.parent = current - self.directories[path] = dir - end + dir.versionMadeBy = entry.versionMadeBy + dir.isDirectory = true + dir.parent = current + self.directories[path] = dir + end - -- Track directory in both lookup table and parent's children - table.insert(current.children, self.directories[path]) - end + -- Track directory in both lookup table and parent's children + table.insert(current.children, self.directories[path]) + end - -- Move deeper into the tree - current = self.directories[path] - continue - end + -- Move deeper into the tree + current = self.directories[path] + continue + end - -- Link file entry to its parent directory - entry.parent = current - table.insert(current.children, entry) - end - end + -- Link file entry to its parent directory + entry.parent = current + table.insert(current.children, entry) + end + end end --[=[ - @within ZipReader - @method findEntry - - Finds a [ZipEntry] by its path in the ZIP archive. + @within ZipReader + @method findEntry + + Finds a [ZipEntry] by its path in the ZIP archive. - @param path string -- Path to the entry to find - @return ZipEntry? -- The found entry, or `nil` if not found + @param path string -- Path to the entry to find + @return ZipEntry? -- The found entry, or `nil` if not found ]=] function ZipReader.findEntry(self: ZipReader, path: string): ZipEntry? - if path == "/" then - -- If the root directory's entry was requested we do not - -- need to do any additional work - return self.root - end + if path == "/" then + -- If the root directory's entry was requested we do not + -- need to do any additional work + return self.root + end - -- Normalize path by removing leading and trailing slashes - -- This ensures consistent lookup regardless of input format - -- e.g., "/folder/file.txt/" -> "folder/file.txt" - path = string.gsub(path, "^/", ""):gsub("/$", "") + -- Normalize path by removing leading and trailing slashes + -- This ensures consistent lookup regardless of input format + -- e.g., "/folder/file.txt/" -> "folder/file.txt" + path = string.gsub(path, "^/", ""):gsub("/$", "") - -- First check regular files and explicit directories - for _, entry in self.entries do - -- Compare normalized paths - if string.gsub(entry.name, "/$", "") == path then - return entry - end - end + -- First check regular files and explicit directories + for _, entry in self.entries do + -- Compare normalized paths + if string.gsub(entry.name, "/$", "") == path then + return entry + end + end - -- If not found, check virtual directory entries - -- These are directories that were created implicitly - return self.directories[path] + -- If not found, check virtual directory entries + -- These are directories that were created implicitly + return self.directories[path] end --[=[ - @interface ExtractionOptions - @within ZipReader - @ignore + @interface ExtractionOptions + @within ZipReader + @ignore - Options accepted by the [ZipReader:extract] method. + Options accepted by the [ZipReader:extract] method. - @field followSymlinks boolean? -- Whether to follow symlinks - @field decompress boolean -- Whether to decompress the entry or only return the raw data - @field type ("binary" | "text")? -- The type of data to return, automatically inferred based on the type of contents if not specified - @field skipCrcValidation boolean? -- Whether to skip CRC validation - @field skipSizeValidation boolean? -- Whether to skip size validation + @field followSymlinks boolean? -- Whether to follow symlinks + @field decompress boolean -- Whether to decompress the entry or only return the raw data + @field type ("binary" | "text")? -- The type of data to return, automatically inferred based on the type of contents if not specified + @field skipCrcValidation boolean? -- Whether to skip CRC validation + @field skipSizeValidation boolean? -- Whether to skip size validation ]=] type ExtractionOptions = { - followSymlinks: boolean?, - decompress: boolean?, - type: ("binary" | "text")?, - skipCrcValidation: boolean?, - skipSizeValidation: boolean?, + followSymlinks: boolean?, + decompress: boolean?, + type: ("binary" | "text")?, + skipCrcValidation: boolean?, + skipSizeValidation: boolean?, } --[=[ - @within ZipReader - @method extract + @within ZipReader + @method extract - Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for - extracting directories. + Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for + extracting directories. - @error "Cannot extract directory" -- If the entry is a directory, use [ZipReader:extractDirectory] instead - @error "Invalid local file header" -- Invalid ZIP file, local header signature did not match - @error "Unsupported PKZip spec version: {versionNeeded}" -- The ZIP file was created with an unsupported version of the ZIP specification - @error "Symlink path not found" -- If `followSymlinks` of options is `true` and the symlink path was not found - @error "Unsupported compression, ID: {compressionMethod}" -- The entry was compressed using an unsupported compression method + @error "Cannot extract directory" -- If the entry is a directory, use [ZipReader:extractDirectory] instead + @error "Invalid local file header" -- Invalid ZIP file, local header signature did not match + @error "Unsupported PKZip spec version: {versionNeeded}" -- The ZIP file was created with an unsupported version of the ZIP specification + @error "Symlink path not found" -- If `followSymlinks` of options is `true` and the symlink path was not found + @error "Unsupported compression, ID: {compressionMethod}" -- The entry was compressed using an unsupported compression method - @param entry ZipEntry -- The entry to extract - @param options ExtractionOptions? -- Options for the extraction - @return buffer | string -- The extracted data + @param entry ZipEntry -- The entry to extract + @param options ExtractionOptions? -- Options for the extraction + @return buffer | string -- The extracted data ]=] function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: ExtractionOptions?): buffer | string - -- Local File Header format: - -- Offset Bytes Description - -- 0 4 Local file header signature - -- 4 2 Version needed to extract - -- 6 2 General purpose bitflags - -- 8 2 Compression method (8 = DEFLATE) - -- 14 4 CRC32 checksum - -- 18 4 Compressed size - -- 22 4 Uncompressed size - -- 26 2 File name length (n) - -- 28 2 Extra field length (m) - -- 30 n File name - -- 30+n m Extra field - -- 30+n+m - File data + -- Local File Header format: + -- Offset Bytes Description + -- 0 4 Local file header signature + -- 4 2 Version needed to extract + -- 6 2 General purpose bitflags + -- 8 2 Compression method (8 = DEFLATE) + -- 14 4 CRC32 checksum + -- 18 4 Compressed size + -- 22 4 Uncompressed size + -- 26 2 File name length (n) + -- 28 2 Extra field length (m) + -- 30 n File name + -- 30+n m Extra field + -- 30+n+m - File data - if entry.isDirectory then - error("Cannot extract directory") - end + if entry.isDirectory then + error("Cannot extract directory") + end - local defaultOptions: ExtractionOptions = { - followSymlinks = false, - decompress = true, - type = if entry.isText then "text" else "binary", - skipValidation = false, - } + local defaultOptions: ExtractionOptions = { + followSymlinks = false, + decompress = true, + type = if entry.isText then "text" else "binary", + skipValidation = false, + } - -- TODO: Use a `Partial` type function for this in the future! - local optionsOrDefault: { - followSymlinks: boolean, - decompress: boolean, - type: "binary" | "text", - skipCrcValidation: boolean, - skipSizeValidation: boolean, - } = if options - then setmetatable(options, { __index = defaultOptions }) :: any - else defaultOptions + -- TODO: Use a `Partial` type function for this in the future! + local optionsOrDefault: { + followSymlinks: boolean, + decompress: boolean, + type: "binary" | "text", + skipCrcValidation: boolean, + skipSizeValidation: boolean, + } = if options + then setmetatable(options, { __index = defaultOptions }) :: any + else defaultOptions - local pos = entry.offset - if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then - error("Invalid local file header") - end + local pos = entry.offset + if buffer.readu32(self.data, pos) ~= SIGNATURES.LOCAL_FILE then + error("Invalid local file header") + end - -- Validate that the version needed to extract is supported - local versionNeeded = buffer.readu16(self.data, pos + 4) - assert(MAX_SUPPORTED_PKZIP_VERSION >= versionNeeded, `Unsupported PKZip spec version: {versionNeeded}`) + -- Validate that the version needed to extract is supported + local versionNeeded = buffer.readu16(self.data, pos + 4) + assert(MAX_SUPPORTED_PKZIP_VERSION >= versionNeeded, `Unsupported PKZip spec version: {versionNeeded}`) - local bitflags = buffer.readu16(self.data, pos + 6) - local crcChecksum = buffer.readu32(self.data, pos + 14) - local compressedSize = buffer.readu32(self.data, pos + 18) - local uncompressedSize = buffer.readu32(self.data, pos + 22) - local nameLength = buffer.readu16(self.data, pos + 26) - local extraLength = buffer.readu16(self.data, pos + 28) + local bitflags = buffer.readu16(self.data, pos + 6) + local crcChecksum = buffer.readu32(self.data, pos + 14) + local compressedSize = buffer.readu32(self.data, pos + 18) + local uncompressedSize = buffer.readu32(self.data, pos + 22) + local nameLength = buffer.readu16(self.data, pos + 26) + local extraLength = buffer.readu16(self.data, pos + 28) - pos = pos + 30 + nameLength + extraLength + pos = pos + 30 + nameLength + extraLength - if bit32.band(bitflags, 0x08) ~= 0 then - -- The bit at offset 3 was set, meaning we did not have the file sizes - -- and CRC checksum at the time of the creation of the ZIP. Instead, they - -- were appended after the compressed data chunks in a data descriptor + if bit32.band(bitflags, 0x08) ~= 0 then + -- The bit at offset 3 was set, meaning we did not have the file sizes + -- and CRC checksum at the time of the creation of the ZIP. Instead, they + -- were appended after the compressed data chunks in a data descriptor - -- Data Descriptor format: - -- Offset Bytes Description - -- 0 0 or 4 0x08074b50 (optional signature) - -- 0 or 4 4 CRC32 checksum - -- 4 or 8 4 Compressed size - -- 8 or 12 4 Uncompressed size + -- Data Descriptor format: + -- Offset Bytes Description + -- 0 0 or 4 0x08074b50 (optional signature) + -- 0 or 4 4 CRC32 checksum + -- 4 or 8 4 Compressed size + -- 8 or 12 4 Uncompressed size - -- Start at the compressed data - local descriptorPos = pos - while true do - -- Try reading a u32 starting from current offset - local leading = buffer.readu32(self.data, descriptorPos) + -- Start at the compressed data + local descriptorPos = pos + while true do + -- Try reading a u32 starting from current offset + local leading = buffer.readu32(self.data, descriptorPos) - if leading == SIGNATURES.DATA_DESCRIPTOR then - -- If we find a data descriptor signature, that must mean - -- the current offset points is the start of the descriptor - break - end + if leading == SIGNATURES.DATA_DESCRIPTOR then + -- If we find a data descriptor signature, that must mean + -- the current offset points is the start of the descriptor + break + end - if leading == entry.crc then - -- If we find our file's CRC checksum, that means the data - -- descriptor signature was omitted, so our chunk starts 4 - -- bytes before - descriptorPos -= 4 - break - end + if leading == entry.crc then + -- If we find our file's CRC checksum, that means the data + -- descriptor signature was omitted, so our chunk starts 4 + -- bytes before + descriptorPos -= 4 + break + end - -- Skip to the next byte - descriptorPos += 1 - end + -- Skip to the next byte + descriptorPos += 1 + end - crcChecksum = buffer.readu32(self.data, descriptorPos + 4) - compressedSize = buffer.readu32(self.data, descriptorPos + 8) - uncompressedSize = buffer.readu32(self.data, descriptorPos + 12) - end + crcChecksum = buffer.readu32(self.data, descriptorPos + 4) + compressedSize = buffer.readu32(self.data, descriptorPos + 8) + uncompressedSize = buffer.readu32(self.data, descriptorPos + 12) + end - local content = buffer.create(compressedSize) - buffer.copy(content, 0, self.data, pos, compressedSize) + local content = buffer.create(compressedSize) + buffer.copy(content, 0, self.data, pos, compressedSize) - if optionsOrDefault.decompress then - local compressionMethod = buffer.readu16(self.data, entry.offset + 8) - local algo = DECOMPRESSION_ROUTINES[compressionMethod] - if algo == nil then - error(`Unsupported compression, ID: {compressionMethod}`) - end + if optionsOrDefault.decompress then + local compressionMethod = buffer.readu16(self.data, entry.offset + 8) + local algo = DECOMPRESSION_ROUTINES[compressionMethod] + if algo == nil then + error(`Unsupported compression, ID: {compressionMethod}`) + end - if optionsOrDefault.followSymlinks then - local linkPath = buffer.tostring(algo.decompress(content, 0, { - expected = 0x00000000, - skip = true, - })) + if optionsOrDefault.followSymlinks then + local linkPath = buffer.tostring(algo.decompress(content, 0, { + expected = 0x00000000, + skip = true, + })) - -- Check if the path was a relative path - if path.isRelative(linkPath) then - if string.sub(linkPath, -1) ~= "/" then - linkPath ..= "/" - end + -- Check if the path was a relative path + if path.isRelative(linkPath) then + if string.sub(linkPath, -1) ~= "/" then + linkPath ..= "/" + end - linkPath = path.canonicalize(`{(entry.parent or self.root).name}{linkPath}`) - end + linkPath = path.canonicalize(`{(entry.parent or self.root).name}{linkPath}`) + end - optionsOrDefault.followSymlinks = false - optionsOrDefault.type = "binary" - optionsOrDefault.skipCrcValidation = true - optionsOrDefault.skipSizeValidation = true - content = - self:extract(self:findEntry(linkPath) or error("Symlink path not found"), optionsOrDefault) :: buffer - end + optionsOrDefault.followSymlinks = false + optionsOrDefault.type = "binary" + optionsOrDefault.skipCrcValidation = true + optionsOrDefault.skipSizeValidation = true + content = + self:extract(self:findEntry(linkPath) or error("Symlink path not found"), optionsOrDefault) :: buffer + end - content = algo.decompress(content, uncompressedSize, { - expected = crcChecksum, - skip = optionsOrDefault.skipCrcValidation, - }) + content = algo.decompress(content, uncompressedSize, { + expected = crcChecksum, + skip = optionsOrDefault.skipCrcValidation, + }) - -- Unless skipping validation is requested, we make sure the uncompressed size matches - assert( - optionsOrDefault.skipSizeValidation or uncompressedSize == buffer.len(content), - "Validation failed; uncompressed size does not match" - ) - end + -- Unless skipping validation is requested, we make sure the uncompressed size matches + assert( + optionsOrDefault.skipSizeValidation or uncompressedSize == buffer.len(content), + "Validation failed; uncompressed size does not match" + ) + end - return if optionsOrDefault.type == "text" then buffer.tostring(content) else content + return if optionsOrDefault.type == "text" then buffer.tostring(content) else content end --[=[ - @within ZipReader - @method extractDirectory + @within ZipReader + @method extractDirectory - Extracts all the files in a specified directory, skipping any directory entries. + Extracts all the files in a specified directory, skipping any directory entries. - **Errors if [ZipReader:extract] errors on an entry in the directory.** + **Errors if [ZipReader:extract] errors on an entry in the directory.** - @param path string -- The path to the directory to extract - @param options ExtractionOptions? -- Options for the extraction - @return { [string]: buffer } | { [string]: string } -- A map of extracted file paths and their contents + @param path string -- The path to the directory to extract + @param options ExtractionOptions? -- Options for the extraction + @return { [string]: buffer } | { [string]: string } -- A map of extracted file paths and their contents ]=] function ZipReader.extractDirectory( - self: ZipReader, - path: string, - options: ExtractionOptions? + self: ZipReader, + path: string, + options: ExtractionOptions? ): { [string]: buffer } | { [string]: string } - local files: { [string]: buffer } | { [string]: string } = {} - -- Normalize path by removing leading slash for consistent prefix matching - path = string.gsub(path, "^/", "") + local files: { [string]: buffer } | { [string]: string } = {} + -- Normalize path by removing leading slash for consistent prefix matching + path = string.gsub(path, "^/", "") - -- Iterate through all entries to find files within target directory - for _, entry in self.entries do - -- Check if entry is a file (not directory) and its path starts with target directory - if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then - -- Store extracted content mapped to full path - files[entry.name] = self:extract(entry, options) - end - end + -- Iterate through all entries to find files within target directory + for _, entry in self.entries do + -- Check if entry is a file (not directory) and its path starts with target directory + if not entry.isDirectory and string.sub(entry.name, 1, #path) == path then + -- Store extracted content mapped to full path + files[entry.name] = self:extract(entry, options) + end + end - -- Return a map of file to contents - return files + -- Return a map of file to contents + return files end --[=[ - @within ZipReader - @method listDirectory + @within ZipReader + @method listDirectory - Lists the entries within a specified directory path. + Lists the entries within a specified directory path. - @error "Not a directory" -- If the path does not exist or is not a directory + @error "Not a directory" -- If the path does not exist or is not a directory - @param path string -- The path to the directory to list - @return { ZipEntry } -- The list of entries in the directory + @param path string -- The path to the directory to list + @return { ZipEntry } -- The list of entries in the directory ]=] function ZipReader.listDirectory(self: ZipReader, path: string): { ZipEntry } - -- Locate the entry with the path - local entry = self:findEntry(path) - if not entry or not entry.isDirectory then - -- If an entry was not found, we error - error("Not a directory") - end + -- Locate the entry with the path + local entry = self:findEntry(path) + if not entry or not entry.isDirectory then + -- If an entry was not found, we error + error("Not a directory") + end - -- Return the children of our discovered entry - return entry.children + -- Return the children of our discovered entry + return entry.children end --[=[ - @within ZipReader - @method walk + @within ZipReader + @method walk - Recursively alks through the zip file, calling the provided callback for each entry - with the current entry and its depth. + Recursively alks through the zip file, calling the provided callback for each entry + with the current entry and its depth. - @param callback (entry: ZipEntry, depth: number) -> () -- The function to call for each entry + @param callback (entry: ZipEntry, depth: number) -> () -- The function to call for each entry ]=] function ZipReader.walk(self: ZipReader, callback: (entry: ZipEntry, depth: number) -> ()): () - -- Wrapper function which recursively calls callback for every child - -- in an entry - local function walkEntry(entry: ZipEntry, depth: number) - callback(entry, depth) + -- Wrapper function which recursively calls callback for every child + -- in an entry + local function walkEntry(entry: ZipEntry, depth: number) + callback(entry, depth) - for _, child in entry.children do - -- ooo spooky recursion... blame this if shit go wrong - walkEntry(child, depth + 1) - end - end + for _, child in entry.children do + -- ooo spooky recursion... blame this if shit go wrong + walkEntry(child, depth + 1) + end + end - walkEntry(self.root, 0) + walkEntry(self.root, 0) end --[=[ - @interface ZipStatistics - @within ZipReader + @interface ZipStatistics + @within ZipReader - @prop fileCount number -- The number of files in the zip - @prop dirCount number -- The number of directories in the zip - @prop totalSize number -- The total size of all files in the zip + @field fileCount number -- The number of files in the zip + @field dirCount number -- The number of directories in the zip + @field totalSize number -- The total size of all files in the zip ]=] export type ZipStatistics = { fileCount: number, dirCount: number, totalSize: number } --[=[ - @within ZipReader - @method getStats + @within ZipReader + @method getStats - Retrieves statistics about the ZIP file. + Retrieves statistics about the ZIP file. - @return ZipStatistics -- The statistics about the ZIP file + @return ZipStatistics -- The statistics about the ZIP file ]=] function ZipReader.getStats(self: ZipReader): ZipStatistics - local stats: ZipStatistics = { - fileCount = 0, - dirCount = 0, - totalSize = 0, - } + local stats: ZipStatistics = { + fileCount = 0, + dirCount = 0, + totalSize = 0, + } - -- Iterate through the entries, updating stats - for _, entry in self.entries do - if entry.isDirectory then - stats.dirCount += 1 - continue - end + -- Iterate through the entries, updating stats + for _, entry in self.entries do + if entry.isDirectory then + stats.dirCount += 1 + continue + end - stats.fileCount += 1 - stats.totalSize += entry.size - end + stats.fileCount += 1 + stats.totalSize += entry.size + end - return stats + return stats end return { - -- Creates a `ZipReader` from a `buffer` of ZIP data. - load = function(data: buffer) - return ZipReader.new(data) - end, + -- Creates a `ZipReader` from a `buffer` of ZIP data. + load = function(data: buffer) + return ZipReader.new(data) + end, }