local fs = require("@lune/fs") local process = require("@lune/process") local DateTime = require("@lune/datetime") local frktest = require("../lune_packages/frktest") local check = frktest.assert.check local ZipReader = require("../lib") local ZIPS = fs.readDir("tests/data") local FALLIBLES = { "invalid_cde_number_of_files_allocation_greater_offset.zip", "invalid_cde_number_of_files_allocation_smaller_offset.zip", "invalid_offset.zip", "invalid_offset2.zip", "misaligned_comment.zip", "comment_garbage.zip", "chinese.zip" -- FIXME: Support encoding other than UTF-8 and ASCII using OS APIs after FFI } local function timestampToValues(dosTimestamp: number): DateTime.DateTimeValues local time = bit32.band(dosTimestamp, 0xFFFF) local date = bit32.band(bit32.rshift(dosTimestamp, 16), 0xFFFF) return { year = bit32.band(bit32.rshift(date, 9), 0x7f) + 1980, month = bit32.band(bit32.rshift(date, 5), 0x0f), day = bit32.band(date, 0x1f), hour = bit32.band(bit32.rshift(time, 11), 0x1f), minute = bit32.band(bit32.rshift(time, 5), 0x3f), second = bit32.band(time, 0x1f) * 2, } end function dateFuzzyEq(date1: string, date2: string, thresholdDays: number): boolean -- Convert the date strings to Lua date tables local year1, month1, day1 = date1:match("(%d+)-(%d+)-(%d+)") local year2, month2, day2 = date2:match("(%d+)-(%d+)-(%d+)") -- Create date tables local dt1 = os.time({ year = assert(tonumber(year1)), month = assert(tonumber(month1)), day = assert(tonumber(day1)) }) local dt2 = os.time({ year = assert(tonumber(year2)), month = assert(tonumber(month2)), day = assert(tonumber(day2)) }) -- Calculate the difference in seconds local difference = math.abs(dt1 - dt2) -- Calculate the threshold in seconds local threshold_seconds = thresholdDays * 86400 -- 86400 seconds in a day -- Check if the difference is within the threshold return difference <= threshold_seconds end function timeFuzzyEq(time1: string, time2: string, thresholdSeconds: number): boolean -- Convert the time strings to hours, minutes, and seconds local hour1, minute1 = time1:match("(%d+):(%d+)") local hour2, minute2 = time2:match("(%d+):(%d+)") -- Create time tables and convert to seconds local totalSeconds1 = (assert(tonumber(hour1)) * 3600) + (assert(tonumber(minute1)) * 60) local totalSeconds2 = (assert(tonumber(hour2)) * 3600) + (assert(tonumber(minute2)) * 60) -- Calculate the difference in seconds local difference = math.abs(totalSeconds1 - totalSeconds2) -- Check if the difference is within the threshold return difference <= thresholdSeconds end return function(test: typeof(frktest.test)) test.suite("ZIP metadata tests", function() for _, file in ZIPS do if not string.match(file, "%.zip$") then -- Not a zip file, skip continue end local checkErr:(((...any) -> any?) -> nil) = if table.find(FALLIBLES, file) then check.should_error else check.should_not_error test.case(`Parsed metadata matches unzip output - {file}`, function() checkErr(function(...) file = "tests/data/" .. file local data = fs.readFile(file) local zip = ZipReader.load(buffer.fromstring(data)) -- Get sizes from unzip command local result = process.spawn("unzip", { "-v", file }) -- HACK: We use assert here since we don't know if we expect false or true assert(result.ok) -- Parse unzip output for line in string.gmatch(result.stdout, "[^\r\n]+") do if not string.match(line, "^Archive:") and not string.match(line, "^%s+Length") and not string.match(line, "^%s*%-%-%-%-") and not string.match(line, "files?$") and #line > 0 then -- TODO: Expose information about method, size, and compression ratio in API 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( timestampToValues(entry.timestamp) :: DateTime.DateTimeValueArguments ) check.equal(tonumber(length), entry.size) check.is_true(dateFuzzyEq(gotDateTime:formatLocalTime("%Y-%m-%d"), expectedDate :: string, 1)) check.is_true( -- 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) ) check.equal(string.format("%08x", entry.crc), crc32) end end return end) end) end end) end