mirror of
https://github.com/0x5eal/luau-unzip.git
synced 2025-05-04 11:23:48 +01:00
style: apply stylua formatter
This commit is contained in:
parent
9d3c815fbb
commit
03320fe090
9 changed files with 489 additions and 485 deletions
|
@ -5,7 +5,7 @@ local asciitable = require("../luau_packages/asciitable")
|
||||||
local file = fs.readFile("tests/data/files_and_dirs.zip")
|
local file = fs.readFile("tests/data/files_and_dirs.zip")
|
||||||
local reader = zip.load(buffer.fromstring(file))
|
local reader = zip.load(buffer.fromstring(file))
|
||||||
|
|
||||||
--- Transforms a tree of recursive `{ [string]: EntryData }` to a recursive tree of
|
--- Transforms a tree of recursive `{ [string]: EntryData }` to a recursive tree of
|
||||||
--- `{ [string]: string }`
|
--- `{ [string]: string }`
|
||||||
local function formatTree(tree: Tree): Tree<string>
|
local function formatTree(tree: Tree): Tree<string>
|
||||||
local result: Tree<string> = {}
|
local result: Tree<string> = {}
|
||||||
|
|
|
@ -431,7 +431,6 @@ function ZipReader.extract(self: ZipReader, entry: ZipEntry, options: Extraction
|
||||||
skip = true,
|
skip = true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
-- Check if the path was a relative path
|
-- Check if the path was a relative path
|
||||||
if path.isRelative(linkPath) then
|
if path.isRelative(linkPath) then
|
||||||
if string.sub(linkPath, -1) ~= "/" then
|
if string.sub(linkPath, -1) ~= "/" then
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
--- Canonicalize a path by removing redundant components
|
--- Canonicalize a path by removing redundant components
|
||||||
local function canonicalize(path: string): string
|
local function canonicalize(path: string): string
|
||||||
-- NOTE: It is fine for us to use `/` here because ZIP file names
|
-- NOTE: It is fine for us to use `/` here because ZIP file names
|
||||||
-- always use `/` as the path separator
|
-- always use `/` as the path separator
|
||||||
local components = string.split(path, "/")
|
local components = string.split(path, "/")
|
||||||
local result = {}
|
local result = {}
|
||||||
for _, component in components do
|
for _, component in components do
|
||||||
if component == "." then
|
if component == "." then
|
||||||
-- Skip current directory
|
-- Skip current directory
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
if component == ".." then
|
if component == ".." then
|
||||||
-- Traverse one upwards
|
-- Traverse one upwards
|
||||||
table.remove(result, #result)
|
table.remove(result, #result)
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Otherwise, add the component to the result
|
-- Otherwise, add the component to the result
|
||||||
table.insert(result, component)
|
table.insert(result, component)
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.concat(result, "/")
|
return table.concat(result, "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Check if a path is absolute
|
--- Check if a path is absolute
|
||||||
local function isAbsolute(path: string): boolean
|
local function isAbsolute(path: string): boolean
|
||||||
return (string.match(path, "^/") or string.match(path, "^[a-zA-Z]:[\\/]") or string.match(path, "^//")) ~= nil
|
return (string.match(path, "^/") or string.match(path, "^[a-zA-Z]:[\\/]") or string.match(path, "^//")) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Check if a path is relative
|
--- Check if a path is relative
|
||||||
local function isRelative(path: string): boolean
|
local function isRelative(path: string): boolean
|
||||||
return not isAbsolute(path)
|
return not isAbsolute(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canonicalize = canonicalize,
|
canonicalize = canonicalize,
|
||||||
isAbsolute = isAbsolute,
|
isAbsolute = isAbsolute,
|
||||||
isRelative = isRelative,
|
isRelative = isRelative,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
local crc32 = require("../crc")
|
local crc32 = require("../crc")
|
||||||
|
|
||||||
export type CrcValidationOptions = {
|
export 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 not validation.skip then
|
if not 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
|
||||||
|
|
||||||
return validateCrc
|
return validateCrc
|
||||||
|
|
|
@ -1,40 +1,41 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
local serde = require("@lune/serde")
|
local serde = require("@lune/serde")
|
||||||
|
|
||||||
local frktest = require("../lune_packages/frktest")
|
local frktest = require("../lune_packages/frktest")
|
||||||
local check = frktest.assert.check
|
local check = frktest.assert.check
|
||||||
|
|
||||||
local ZipReader = require("../lib")
|
local ZipReader = require("../lib")
|
||||||
|
|
||||||
return function(test: typeof(frktest.test))
|
return function(test: typeof(frktest.test))
|
||||||
test.suite("Edge case tests", function()
|
test.suite("Edge case tests", function()
|
||||||
test.case("Handles misaligned comment properly", function()
|
test.case("Handles misaligned comment properly", function()
|
||||||
local data = fs.readFile("tests/data/misaligned_comment.zip")
|
local data = fs.readFile("tests/data/misaligned_comment.zip")
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
check.equal(zip.comment, "short.")
|
check.equal(zip.comment, "short.")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test.case("Follows symlinks correctly", function()
|
test.case("Follows symlinks correctly", function()
|
||||||
-- TODO: More test files with symlinks
|
-- TODO: More test files with symlinks
|
||||||
|
|
||||||
local data = fs.readFile("tests/data/pandoc_soft_links.zip")
|
local data = fs.readFile("tests/data/pandoc_soft_links.zip")
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
local entry = assert(zip:findEntry("/pandoc-3.2-arm64/bin/pandoc-lua"))
|
local entry = assert(zip:findEntry("/pandoc-3.2-arm64/bin/pandoc-lua"))
|
||||||
assert(entry:isSymlink(), "Entry type must be a symlink")
|
assert(entry:isSymlink(), "Entry type must be a symlink")
|
||||||
|
|
||||||
local targetPath = zip:extract(entry, { isString = true }) :: string
|
local targetPath = zip:extract(entry, { isString = true }) :: string
|
||||||
check.equal(targetPath, "pandoc")
|
check.equal(targetPath, "pandoc")
|
||||||
|
|
||||||
local bin = zip:extract(entry, { isString = false, followSymlinks = true }) :: buffer
|
local bin = zip:extract(entry, { isString = false, followSymlinks = true }) :: buffer
|
||||||
local expectedBin = process.spawn("unzip", { "-p", "tests/data/pandoc_soft_links.zip", "pandoc-3.2-arm64/bin/pandoc" })
|
local expectedBin =
|
||||||
check.is_true(expectedBin.ok)
|
process.spawn("unzip", { "-p", "tests/data/pandoc_soft_links.zip", "pandoc-3.2-arm64/bin/pandoc" })
|
||||||
|
check.is_true(expectedBin.ok)
|
||||||
-- Compare hashes instead of the entire binary to improve speed and not print out
|
|
||||||
-- the entire binary data in case there's a mismatch
|
-- Compare hashes instead of the entire binary to improve speed and not print out
|
||||||
check.equal(serde.hash("blake3", bin), serde.hash("blake3", expectedBin.stdout))
|
-- the entire binary data in case there's a mismatch
|
||||||
end)
|
check.equal(serde.hash("blake3", bin), serde.hash("blake3", expectedBin.stdout))
|
||||||
end)
|
end)
|
||||||
end
|
end)
|
||||||
|
end
|
||||||
|
|
|
@ -1,79 +1,79 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
local frktest = require("../lune_packages/frktest")
|
local frktest = require("../lune_packages/frktest")
|
||||||
local check = frktest.assert.check
|
local check = frktest.assert.check
|
||||||
|
|
||||||
local ZipReader = require("../lib")
|
local ZipReader = require("../lib")
|
||||||
|
|
||||||
-- Reuse the same ZIP files from metadata tests
|
-- Reuse the same ZIP files from metadata tests
|
||||||
local ZIPS = fs.readDir("tests/data")
|
local ZIPS = fs.readDir("tests/data")
|
||||||
local FALLIBLES = {
|
local FALLIBLES = {
|
||||||
"invalid_cde_number_of_files_allocation_greater_offset.zip",
|
"invalid_cde_number_of_files_allocation_greater_offset.zip",
|
||||||
-- FIXME: Incorrectly handled, file tree is empty and walk silently errors
|
-- FIXME: Incorrectly handled, file tree is empty and walk silently errors
|
||||||
-- "invalid_cde_number_of_files_allocation_smaller_offset.zip",
|
-- "invalid_cde_number_of_files_allocation_smaller_offset.zip",
|
||||||
"invalid_offset.zip",
|
"invalid_offset.zip",
|
||||||
"invalid_offset2.zip",
|
"invalid_offset2.zip",
|
||||||
-- FIXME: Does not error when it should
|
-- FIXME: Does not error when it should
|
||||||
-- "comment_garbage.zip",
|
-- "comment_garbage.zip",
|
||||||
"chinese.zip",
|
"chinese.zip",
|
||||||
"non_utf8.zip", -- FIXME: Lune breaks for non utf8 data in process stdout
|
"non_utf8.zip", -- FIXME: Lune breaks for non utf8 data in process stdout
|
||||||
"pandoc_soft_links.zip", -- Soft links are tested separately in edge_cases
|
"pandoc_soft_links.zip", -- Soft links are tested separately in edge_cases
|
||||||
}
|
}
|
||||||
|
|
||||||
return function(test: typeof(frktest.test))
|
return function(test: typeof(frktest.test))
|
||||||
test.suite("ZIP extraction tests", function()
|
test.suite("ZIP extraction tests", function()
|
||||||
for _, file in ZIPS do
|
for _, file in ZIPS do
|
||||||
if not string.match(file, "%.zip$") then
|
if not string.match(file, "%.zip$") then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
local checkErr: ((...any) -> any?) -> nil = if table.find(FALLIBLES, file)
|
local checkErr: ((...any) -> any?) -> nil = if table.find(FALLIBLES, file)
|
||||||
then check.should_error
|
then check.should_error
|
||||||
else check.should_not_error
|
else check.should_not_error
|
||||||
|
|
||||||
test.case(`Extracts files correctly - {file}`, function()
|
test.case(`Extracts files correctly - {file}`, function()
|
||||||
checkErr(function()
|
checkErr(function()
|
||||||
local zipPath = "tests/data/" .. file
|
local zipPath = "tests/data/" .. file
|
||||||
local data = fs.readFile(zipPath)
|
local data = fs.readFile(zipPath)
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
-- Test both string and buffer extraction
|
-- Test both string and buffer extraction
|
||||||
local stringOptions = { isString = true, decompress = true }
|
local stringOptions = { isString = true, decompress = true }
|
||||||
local bufferOptions = { isString = false, decompress = true }
|
local bufferOptions = { isString = false, decompress = true }
|
||||||
|
|
||||||
-- Extract and verify each file
|
-- Extract and verify each file
|
||||||
zip:walk(function(entry)
|
zip:walk(function(entry)
|
||||||
if entry.isDirectory then
|
if entry.isDirectory then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Extract with unzip for comparison
|
-- Extract with unzip for comparison
|
||||||
local unzipOutput = process.spawn(`unzip`, { "-p", zipPath, entry:getPath() })
|
local unzipOutput = process.spawn(`unzip`, { "-p", zipPath, entry:getPath() })
|
||||||
|
|
||||||
-- NOTE: We use assert since we don't know whether to expect true or false
|
-- NOTE: We use assert since we don't know whether to expect true or false
|
||||||
assert(unzipOutput.ok)
|
assert(unzipOutput.ok)
|
||||||
|
|
||||||
-- Test string extraction
|
-- Test string extraction
|
||||||
local contentString = zip:extract(entry, stringOptions) :: string
|
local contentString = zip:extract(entry, stringOptions) :: string
|
||||||
check.equal(#contentString, entry.size)
|
check.equal(#contentString, entry.size)
|
||||||
check.equal(contentString, unzipOutput.stdout)
|
check.equal(contentString, unzipOutput.stdout)
|
||||||
|
|
||||||
-- Test buffer extraction
|
-- Test buffer extraction
|
||||||
local contentBuffer = zip:extract(entry, bufferOptions) :: buffer
|
local contentBuffer = zip:extract(entry, bufferOptions) :: buffer
|
||||||
check.equal(buffer.len(contentBuffer), entry.size)
|
check.equal(buffer.len(contentBuffer), entry.size)
|
||||||
check.equal(buffer.tostring(contentBuffer), unzipOutput.stdout)
|
check.equal(buffer.tostring(contentBuffer), unzipOutput.stdout)
|
||||||
|
|
||||||
-- Test directory extraction
|
-- Test directory extraction
|
||||||
local parentPath = entry:getPath():match("(.+)/[^/]*$") or "/"
|
local parentPath = entry:getPath():match("(.+)/[^/]*$") or "/"
|
||||||
local dirContents = zip:extractDirectory(parentPath, stringOptions)
|
local dirContents = zip:extractDirectory(parentPath, stringOptions)
|
||||||
check.not_nil(dirContents[entry:getPath()])
|
check.not_nil(dirContents[entry:getPath()])
|
||||||
check.equal(dirContents[entry:getPath()], unzipOutput.stdout)
|
check.equal(dirContents[entry:getPath()], unzipOutput.stdout)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return
|
return
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
150
tests/list.luau
150
tests/list.luau
|
@ -1,75 +1,75 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
local frktest = require("../lune_packages/frktest")
|
local frktest = require("../lune_packages/frktest")
|
||||||
local check = frktest.assert.check
|
local check = frktest.assert.check
|
||||||
|
|
||||||
local ZipReader = require("../lib")
|
local ZipReader = require("../lib")
|
||||||
|
|
||||||
return function(test: typeof(frktest.test))
|
return function(test: typeof(frktest.test))
|
||||||
test.suite("ZIP listing tests (top-level)", function()
|
test.suite("ZIP listing tests (top-level)", function()
|
||||||
test.case("Lists all entries correctly", function()
|
test.case("Lists all entries correctly", function()
|
||||||
-- Read our test zip file
|
-- Read our test zip file
|
||||||
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
-- Get listing from our implementation
|
-- Get listing from our implementation
|
||||||
local entries = {}
|
local entries = {}
|
||||||
for _, entry in zip:listDirectory("/") do
|
for _, entry in zip:listDirectory("/") do
|
||||||
table.insert(entries, entry:getPath())
|
table.insert(entries, entry:getPath())
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get listing from zip command
|
-- Get listing from zip command
|
||||||
local result = process.spawn("zip", {"-sf", "tests/data/files_and_dirs.zip"})
|
local result = process.spawn("zip", { "-sf", "tests/data/files_and_dirs.zip" })
|
||||||
check.is_true(result.ok)
|
check.is_true(result.ok)
|
||||||
local zipOutput = result.stdout
|
local zipOutput = result.stdout
|
||||||
|
|
||||||
-- Parse zip command output into sorted array
|
-- Parse zip command output into sorted array
|
||||||
local zipEntries = {}
|
local zipEntries = {}
|
||||||
for line in string.gmatch(zipOutput, "[^\r\n]+") do
|
for line in string.gmatch(zipOutput, "[^\r\n]+") do
|
||||||
-- Skip header/footer lines
|
-- Skip header/footer lines
|
||||||
if not string.match(line, "^Archive contains:") and not string.match(line, "^Total %d+ entries") then
|
if not string.match(line, "^Archive contains:") and not string.match(line, "^Total %d+ entries") then
|
||||||
table.insert(zipEntries, string.match(line, "^%s*(.+)$"))
|
table.insert(zipEntries, string.match(line, "^%s*(.+)$"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Compare results
|
-- Compare results
|
||||||
for _, entry in entries do
|
for _, entry in entries do
|
||||||
check.not_nil(table.find(zipEntries, entry))
|
check.not_nil(table.find(zipEntries, entry))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test.case("Lists directories correctly", function()
|
test.case("Lists directories correctly", function()
|
||||||
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
local dirs = {}
|
local dirs = {}
|
||||||
for _, entry in zip:listDirectory("/") do
|
for _, entry in zip:listDirectory("/") do
|
||||||
if entry.isDirectory then
|
if entry.isDirectory then
|
||||||
table.insert(dirs, entry:getPath())
|
table.insert(dirs, entry:getPath())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Verify all directory paths end with /
|
-- Verify all directory paths end with /
|
||||||
for _, dir in dirs do
|
for _, dir in dirs do
|
||||||
check.equal(string.sub(dir, -1), "/")
|
check.equal(string.sub(dir, -1), "/")
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test.case("Directory statistics match", function()
|
test.case("Directory statistics match", function()
|
||||||
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
local data = fs.readFile("tests/data/files_and_dirs.zip")
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
local stats = zip:getStats()
|
local stats = zip:getStats()
|
||||||
|
|
||||||
-- Get file count from zip command
|
-- Get file count from zip command
|
||||||
local result = process.spawn("zip", {"-sf", "tests/data/files_and_dirs.zip"})
|
local result = process.spawn("zip", { "-sf", "tests/data/files_and_dirs.zip" })
|
||||||
check.is_true(result.ok)
|
check.is_true(result.ok)
|
||||||
|
|
||||||
-- Parse file count from last line of zip output
|
-- Parse file count from last line of zip output
|
||||||
local fileCount = tonumber(string.match(result.stdout, "Total (%d+) entries.*$"))
|
local fileCount = tonumber(string.match(result.stdout, "Total (%d+) entries.*$"))
|
||||||
|
|
||||||
check.equal(stats.fileCount + stats.dirCount, fileCount)
|
check.equal(stats.fileCount + stats.dirCount, fileCount)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,139 +1,143 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
local DateTime = require("@lune/datetime")
|
local DateTime = require("@lune/datetime")
|
||||||
|
|
||||||
local frktest = require("../lune_packages/frktest")
|
local frktest = require("../lune_packages/frktest")
|
||||||
local check = frktest.assert.check
|
local check = frktest.assert.check
|
||||||
|
|
||||||
local unzip = require("../lib")
|
local unzip = require("../lib")
|
||||||
|
|
||||||
local ZIPS = fs.readDir("tests/data")
|
local ZIPS = fs.readDir("tests/data")
|
||||||
local FALLIBLES = {
|
local FALLIBLES = {
|
||||||
"invalid_cde_number_of_files_allocation_greater_offset.zip",
|
"invalid_cde_number_of_files_allocation_greater_offset.zip",
|
||||||
"invalid_cde_number_of_files_allocation_smaller_offset.zip",
|
"invalid_cde_number_of_files_allocation_smaller_offset.zip",
|
||||||
"invalid_offset.zip",
|
"invalid_offset.zip",
|
||||||
"invalid_offset2.zip",
|
"invalid_offset2.zip",
|
||||||
"misaligned_comment.zip",
|
"misaligned_comment.zip",
|
||||||
"comment_garbage.zip",
|
"comment_garbage.zip",
|
||||||
"chinese.zip" -- FIXME: Support encoding other than UTF-8 and ASCII using OS APIs after FFI
|
"chinese.zip", -- FIXME: Support encoding other than UTF-8 and ASCII using OS APIs after FFI
|
||||||
}
|
}
|
||||||
|
|
||||||
local METHOD_NAME_TRANSFORMATIONS: { [string]: unzip.CompressionMethod } = {
|
local METHOD_NAME_TRANSFORMATIONS: { [string]: unzip.CompressionMethod } = {
|
||||||
["Defl:N"] = "DEFLATE",
|
["Defl:N"] = "DEFLATE",
|
||||||
["Stored"] = "STORE",
|
["Stored"] = "STORE",
|
||||||
}
|
}
|
||||||
|
|
||||||
local function timestampToValues(dosTimestamp: number): DateTime.DateTimeValues
|
local function timestampToValues(dosTimestamp: number): DateTime.DateTimeValues
|
||||||
local time = bit32.band(dosTimestamp, 0xFFFF)
|
local time = bit32.band(dosTimestamp, 0xFFFF)
|
||||||
local date = bit32.band(bit32.rshift(dosTimestamp, 16), 0xFFFF)
|
local date = bit32.band(bit32.rshift(dosTimestamp, 16), 0xFFFF)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
year = bit32.band(bit32.rshift(date, 9), 0x7f) + 1980,
|
year = bit32.band(bit32.rshift(date, 9), 0x7f) + 1980,
|
||||||
month = bit32.band(bit32.rshift(date, 5), 0x0f),
|
month = bit32.band(bit32.rshift(date, 5), 0x0f),
|
||||||
day = bit32.band(date, 0x1f),
|
day = bit32.band(date, 0x1f),
|
||||||
|
|
||||||
hour = bit32.band(bit32.rshift(time, 11), 0x1f),
|
hour = bit32.band(bit32.rshift(time, 11), 0x1f),
|
||||||
minute = bit32.band(bit32.rshift(time, 5), 0x3f),
|
minute = bit32.band(bit32.rshift(time, 5), 0x3f),
|
||||||
second = bit32.band(time, 0x1f) * 2,
|
second = bit32.band(time, 0x1f) * 2,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function dateFuzzyEq(date1: string, date2: string, thresholdDays: number): boolean
|
function dateFuzzyEq(date1: string, date2: string, thresholdDays: number): boolean
|
||||||
-- Convert the date strings to Lua date tables
|
-- Convert the date strings to Lua date tables
|
||||||
local year1, month1, day1 = date1:match("(%d+)-(%d+)-(%d+)")
|
local year1, month1, day1 = date1:match("(%d+)-(%d+)-(%d+)")
|
||||||
local year2, month2, day2 = date2:match("(%d+)-(%d+)-(%d+)")
|
local year2, month2, day2 = date2:match("(%d+)-(%d+)-(%d+)")
|
||||||
|
|
||||||
-- Create date tables
|
-- Create date tables
|
||||||
local dt1 =
|
local dt1 =
|
||||||
os.time({ year = assert(tonumber(year1)), month = assert(tonumber(month1)), day = assert(tonumber(day1)) })
|
os.time({ year = assert(tonumber(year1)), month = assert(tonumber(month1)), day = assert(tonumber(day1)) })
|
||||||
local dt2 =
|
local dt2 =
|
||||||
os.time({ year = assert(tonumber(year2)), month = assert(tonumber(month2)), day = assert(tonumber(day2)) })
|
os.time({ year = assert(tonumber(year2)), month = assert(tonumber(month2)), day = assert(tonumber(day2)) })
|
||||||
|
|
||||||
-- Calculate the difference in seconds
|
-- Calculate the difference in seconds
|
||||||
local difference = math.abs(dt1 - dt2)
|
local difference = math.abs(dt1 - dt2)
|
||||||
|
|
||||||
-- Calculate the threshold in seconds
|
-- Calculate the threshold in seconds
|
||||||
local threshold_seconds = thresholdDays * 86400 -- 86400 seconds in a day
|
local threshold_seconds = thresholdDays * 86400 -- 86400 seconds in a day
|
||||||
|
|
||||||
-- Check if the difference is within the threshold
|
-- Check if the difference is within the threshold
|
||||||
return difference <= threshold_seconds
|
return difference <= threshold_seconds
|
||||||
end
|
end
|
||||||
|
|
||||||
function timeFuzzyEq(time1: string, time2: string, thresholdSeconds: number): boolean
|
function timeFuzzyEq(time1: string, time2: string, thresholdSeconds: number): boolean
|
||||||
-- Convert the time strings to hours, minutes, and seconds
|
-- Convert the time strings to hours, minutes, and seconds
|
||||||
local hour1, minute1 = time1:match("(%d+):(%d+)")
|
local hour1, minute1 = time1:match("(%d+):(%d+)")
|
||||||
local hour2, minute2 = time2:match("(%d+):(%d+)")
|
local hour2, minute2 = time2:match("(%d+):(%d+)")
|
||||||
|
|
||||||
-- Create time tables and convert to seconds
|
-- Create time tables and convert to seconds
|
||||||
local totalSeconds1 = (assert(tonumber(hour1)) * 3600) + (assert(tonumber(minute1)) * 60)
|
local totalSeconds1 = (assert(tonumber(hour1)) * 3600) + (assert(tonumber(minute1)) * 60)
|
||||||
local totalSeconds2 = (assert(tonumber(hour2)) * 3600) + (assert(tonumber(minute2)) * 60)
|
local totalSeconds2 = (assert(tonumber(hour2)) * 3600) + (assert(tonumber(minute2)) * 60)
|
||||||
|
|
||||||
-- Calculate the difference in seconds
|
-- Calculate the difference in seconds
|
||||||
local difference = math.abs(totalSeconds1 - totalSeconds2)
|
local difference = math.abs(totalSeconds1 - totalSeconds2)
|
||||||
|
|
||||||
-- Check if the difference is within the threshold
|
-- Check if the difference is within the threshold
|
||||||
return difference <= thresholdSeconds
|
return difference <= thresholdSeconds
|
||||||
end
|
end
|
||||||
|
|
||||||
return function(test: typeof(frktest.test))
|
return function(test: typeof(frktest.test))
|
||||||
test.suite("ZIP metadata tests", function()
|
test.suite("ZIP metadata tests", function()
|
||||||
for _, file in ZIPS do
|
for _, file in ZIPS do
|
||||||
if not string.match(file, "%.zip$") then
|
if not string.match(file, "%.zip$") then
|
||||||
-- Not a zip file, skip
|
-- Not a zip file, skip
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
local checkErr:(((...any) -> any?) -> nil) = if table.find(FALLIBLES, file) then check.should_error else check.should_not_error
|
local checkErr: ((...any) -> any?) -> nil = if table.find(FALLIBLES, file)
|
||||||
test.case(`Parsed metadata matches unzip output - {file}`, function()
|
then check.should_error
|
||||||
checkErr(function(...)
|
else check.should_not_error
|
||||||
file = "tests/data/" .. file
|
test.case(`Parsed metadata matches unzip output - {file}`, function()
|
||||||
local data = fs.readFile(file)
|
checkErr(function(...)
|
||||||
local zip = unzip.load(buffer.fromstring(data))
|
file = "tests/data/" .. file
|
||||||
|
local data = fs.readFile(file)
|
||||||
-- Get sizes from unzip command
|
local zip = unzip.load(buffer.fromstring(data))
|
||||||
local result = process.spawn("unzip", { "-v", file })
|
|
||||||
-- HACK: We use assert here since we don't know if we expect false or true
|
-- Get sizes from unzip command
|
||||||
assert(result.ok)
|
local result = process.spawn("unzip", { "-v", file })
|
||||||
|
-- HACK: We use assert here since we don't know if we expect false or true
|
||||||
-- Parse unzip output
|
assert(result.ok)
|
||||||
for line in string.gmatch(result.stdout, "[^\r\n]+") do
|
|
||||||
if
|
-- Parse unzip output
|
||||||
not string.match(line, "^Archive:")
|
for line in string.gmatch(result.stdout, "[^\r\n]+") do
|
||||||
and not string.match(line, "^%s+Length")
|
if
|
||||||
and not string.match(line, "^%s*%-%-%-%-")
|
not string.match(line, "^Archive:")
|
||||||
and not string.match(line, "files?$")
|
and not string.match(line, "^%s+Length")
|
||||||
and #line > 0
|
and not string.match(line, "^%s*%-%-%-%-")
|
||||||
then
|
and not string.match(line, "files?$")
|
||||||
-- TODO: Expose information about size, and compression ratio in API
|
and #line > 0
|
||||||
local length, method, _size, _cmpr, expectedDate, expectedTime, crc32, name = string.match(
|
then
|
||||||
line,
|
-- TODO: Expose information about size, and compression ratio in API
|
||||||
"^%s*(%d+)%s+(%S+)%s+(%d+)%s+([+-]?%d*%%?)%s+(%d%d%d%d%-%d%d%-%d%d)%s+(%d%d:%d%d)%s+(%x+)%s+(.+)$"
|
local length, method, _size, _cmpr, expectedDate, expectedTime, crc32, name = string.match(
|
||||||
)
|
line,
|
||||||
|
"^%s*(%d+)%s+(%S+)%s+(%d+)%s+([+-]?%d*%%?)%s+(%d%d%d%d%-%d%d%-%d%d)%s+(%d%d:%d%d)%s+(%x+)%s+(.+)$"
|
||||||
local entry = assert(zip:findEntry(assert(name)))
|
)
|
||||||
|
|
||||||
local gotDateTime = DateTime.fromLocalTime(
|
local entry = assert(zip:findEntry(assert(name)))
|
||||||
timestampToValues(entry.timestamp) :: DateTime.DateTimeValueArguments
|
|
||||||
)
|
local gotDateTime = DateTime.fromLocalTime(
|
||||||
|
timestampToValues(entry.timestamp) :: DateTime.DateTimeValueArguments
|
||||||
check.equal(tonumber(length), entry.size)
|
)
|
||||||
check.equal(METHOD_NAME_TRANSFORMATIONS[method :: string], entry.method)
|
|
||||||
check.is_true(dateFuzzyEq(gotDateTime:formatLocalTime("%Y-%m-%d"), expectedDate :: string, 1))
|
check.equal(tonumber(length), entry.size)
|
||||||
|
check.equal(METHOD_NAME_TRANSFORMATIONS[method :: string], entry.method)
|
||||||
-- TODO: Use extra datetime field
|
check.is_true(
|
||||||
check.is_true(
|
dateFuzzyEq(gotDateTime:formatLocalTime("%Y-%m-%d"), expectedDate :: string, 1)
|
||||||
-- Allow a threshold of 26 hours, which is the largest possible gap between any two
|
)
|
||||||
-- timezones
|
|
||||||
timeFuzzyEq(gotDateTime:formatLocalTime("%H:%M"), expectedTime :: string, 26 * 3600)
|
-- TODO: Use extra datetime field
|
||||||
)
|
check.is_true(
|
||||||
|
-- Allow a threshold of 26 hours, which is the largest possible gap between any two
|
||||||
check.equal(string.format("%08x", entry.crc), crc32)
|
-- timezones
|
||||||
end
|
timeFuzzyEq(gotDateTime:formatLocalTime("%H:%M"), expectedTime :: string, 26 * 3600)
|
||||||
end
|
)
|
||||||
|
|
||||||
return
|
check.equal(string.format("%08x", entry.crc), crc32)
|
||||||
end)
|
end
|
||||||
end)
|
end
|
||||||
end
|
|
||||||
end)
|
return
|
||||||
end
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
176
tests/walk.luau
176
tests/walk.luau
|
@ -1,88 +1,88 @@
|
||||||
local fs = require("@lune/fs")
|
local fs = require("@lune/fs")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
local frktest = require("../lune_packages/frktest")
|
local frktest = require("../lune_packages/frktest")
|
||||||
local check = frktest.assert.check
|
local check = frktest.assert.check
|
||||||
|
|
||||||
local ZipReader = require("../lib")
|
local ZipReader = require("../lib")
|
||||||
|
|
||||||
local ZIPS = {
|
local ZIPS = {
|
||||||
"tests/data/files_and_dirs.zip",
|
"tests/data/files_and_dirs.zip",
|
||||||
"tests/data/symlink.zip",
|
"tests/data/symlink.zip",
|
||||||
"tests/data/extended_timestamp.zip",
|
"tests/data/extended_timestamp.zip",
|
||||||
}
|
}
|
||||||
|
|
||||||
return function(test: typeof(frktest.test))
|
return function(test: typeof(frktest.test))
|
||||||
test.suite("ZIP walking tests", function()
|
test.suite("ZIP walking tests", function()
|
||||||
test.case("Walks all entries recursively", function()
|
test.case("Walks all entries recursively", function()
|
||||||
for _, file in ZIPS do
|
for _, file in ZIPS do
|
||||||
local data = fs.readFile(file)
|
local data = fs.readFile(file)
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
-- Get entries from our implementation
|
-- Get entries from our implementation
|
||||||
local entries = {}
|
local entries = {}
|
||||||
zip:walk(function(entry)
|
zip:walk(function(entry)
|
||||||
if entry.name ~= "/" then
|
if entry.name ~= "/" then
|
||||||
table.insert(entries, entry:getPath())
|
table.insert(entries, entry:getPath())
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
table.sort(entries)
|
table.sort(entries)
|
||||||
|
|
||||||
-- Get entries from unzip command
|
-- Get entries from unzip command
|
||||||
local result = process.spawn("unzip", { "-l", file })
|
local result = process.spawn("unzip", { "-l", file })
|
||||||
check.is_true(result.ok)
|
check.is_true(result.ok)
|
||||||
|
|
||||||
-- Parse unzip output into sorted array
|
-- Parse unzip output into sorted array
|
||||||
local unzipEntries = {}
|
local unzipEntries = {}
|
||||||
for line in string.gmatch(result.stdout, "[^\r\n]+") do
|
for line in string.gmatch(result.stdout, "[^\r\n]+") do
|
||||||
-- Skip header/footer lines and empty lines
|
-- Skip header/footer lines and empty lines
|
||||||
if
|
if
|
||||||
not string.match(line, "^Archive:")
|
not string.match(line, "^Archive:")
|
||||||
and not string.match(line, "^%s+Length")
|
and not string.match(line, "^%s+Length")
|
||||||
and not string.match(line, "^%s*%-%-%-%-")
|
and not string.match(line, "^%s*%-%-%-%-")
|
||||||
and not string.match(line, "^%s+%d+%s+%d+ files?$")
|
and not string.match(line, "^%s+%d+%s+%d+ files?$")
|
||||||
and #line > 0
|
and #line > 0
|
||||||
then
|
then
|
||||||
-- Extract filename from unzip output format
|
-- Extract filename from unzip output format
|
||||||
local name = string.match(line, "%S+$")
|
local name = string.match(line, "%S+$")
|
||||||
if name then
|
if name then
|
||||||
table.insert(unzipEntries, name)
|
table.insert(unzipEntries, name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(unzipEntries)
|
table.sort(unzipEntries)
|
||||||
|
|
||||||
-- Compare results
|
-- Compare results
|
||||||
check.table.equal(entries, unzipEntries)
|
check.table.equal(entries, unzipEntries)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test.case("Walks with correct depth values", function()
|
test.case("Walks with correct depth values", function()
|
||||||
for _, file in ZIPS do
|
for _, file in ZIPS do
|
||||||
local data = fs.readFile(file)
|
local data = fs.readFile(file)
|
||||||
local zip = ZipReader.load(buffer.fromstring(data))
|
local zip = ZipReader.load(buffer.fromstring(data))
|
||||||
|
|
||||||
-- Verify depth values increase correctly
|
-- Verify depth values increase correctly
|
||||||
local rootSeen = false
|
local rootSeen = false
|
||||||
|
|
||||||
zip:walk(function(entry, depth)
|
zip:walk(function(entry, depth)
|
||||||
if entry:getPath() == "/" then
|
if entry:getPath() == "/" then
|
||||||
check.equal(depth, 0)
|
check.equal(depth, 0)
|
||||||
rootSeen = true
|
rootSeen = true
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Count path separators to verify depth, starting at 1 for `/`
|
-- Count path separators to verify depth, starting at 1 for `/`
|
||||||
local expectedDepth = 1
|
local expectedDepth = 1
|
||||||
for _ in string.gmatch(entry:getPath():gsub("/$", ""), "/") do
|
for _ in string.gmatch(entry:getPath():gsub("/$", ""), "/") do
|
||||||
expectedDepth += 1
|
expectedDepth += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
check.equal(depth, expectedDepth)
|
check.equal(depth, expectedDepth)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
check.is_true(rootSeen)
|
check.is_true(rootSeen)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue