module zip_test @test; import std::io; import std::compression::zip; fn void test_zip_store() { @pool() { // Create archive with uncompressed file ZipArchive zip = zip::open(mem, "unittest_store.zip", "w")!!; zip.write_file("test.txt", "Hello, World!", STORE)!!; (void)zip.close(); defer (void)file::delete("unittest_store.zip"); // Read and verify ZipArchive read_zip = zip::open(mem, "unittest_store.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 1, "Expected 1 entry"); ZipEntry entry = read_zip.stat("test.txt")!!; assert(entry.method == STORE, "Expected STORE method"); assert(entry.uncompressed_size == 13, "Expected 13 bytes"); char[] data = read_zip.read_file_all(mem, "test.txt")!!; defer free(data); assert((String)data == "Hello, World!", "Data mismatch"); }; } fn void test_zip_deflate() { @pool() { // Create archive with compressed file ZipArchive zip = zip::open(mem, "unittest_deflate.zip", "w")!!; ZipEntryWriter writer = zip.open_writer("compressed.txt", DEFLATE)!!; String data = "This is a test. "; for (int i = 0; i < 100; i++) { writer.write((char[])data)!!; } writer.close()!!; (void)zip.close(); defer (void)file::delete("unittest_deflate.zip"); // Read and verify ZipArchive read_zip = zip::open(mem, "unittest_deflate.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 1, "Expected 1 entry"); ZipEntry entry = read_zip.stat("compressed.txt")!!; assert(entry.method == DEFLATE, "Expected DEFLATE method"); assert(entry.uncompressed_size == 1600, "Expected 1600 bytes"); char[] decompressed = read_zip.read_file_all(mem, "compressed.txt")!!; defer free(decompressed); assert(decompressed.len == 1600, "Decompressed size mismatch"); }; } fn void test_zip_directory() { @pool() { // Create archive with directory ZipArchive zip = zip::open(mem, "unittest_dir.zip", "w")!!; zip.add_directory("docs")!!; zip.write_file("docs/readme.txt", "README")!!; (void)zip.close(); defer (void)file::delete("unittest_dir.zip"); // Read and verify ZipArchive read_zip = zip::open(mem, "unittest_dir.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 2, "Expected 2 entries"); ZipEntry dir_entry = read_zip.stat("docs/")!!; assert(dir_entry.is_directory, "Expected directory"); assert(dir_entry.uncompressed_size == 0, "Directory should have 0 size"); ZipEntry file_entry = read_zip.stat("docs/readme.txt")!!; assert(!file_entry.is_directory, "Expected file"); char[] data = read_zip.read_file_all(mem, "docs/readme.txt")!!; defer free(data); assert((String)data == "README", "Data mismatch"); }; } fn void test_zip_crc32_verification() { @pool() { // Create archive ZipArchive zip = zip::open(mem, "unittest_crc.zip", "w")!!; zip.write_file("data.txt", "Test data for CRC32")!!; (void)zip.close(); defer (void)file::delete("unittest_crc.zip"); ZipArchive read_zip = zip::open(mem, "unittest_crc.zip", "r")!!; defer (void)read_zip.close(); char[] data = read_zip.read_file_all(mem, "data.txt")!!; defer free(data); assert((String)data == "Test data for CRC32", "Data mismatch"); }; } fn void test_zip_multiple_files() { @pool() { // Create archive with multiple files ZipArchive zip = zip::open(mem, "unittest_multi.zip", "w")!!; zip.write_file("file1.txt", "First file")!!; zip.write_file("file2.txt", "Second file")!!; zip.write_file("file3.txt", "Third file")!!; (void)zip.close(); defer (void)file::delete("unittest_multi.zip"); // Read and verify ZipArchive read_zip = zip::open(mem, "unittest_multi.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 3, "Expected 3 entries"); for (usz i = 0; i < read_zip.count(); i++) { ZipEntry entry = read_zip.stat_at(i)!!; assert(!entry.is_directory, "Expected files only"); } char[] data1 = read_zip.read_file_all(mem, "file1.txt")!!; defer free(data1); assert((String)data1 == "First file", "File1 mismatch"); char[] data2 = read_zip.read_file_all(mem, "file2.txt")!!; defer free(data2); assert((String)data2 == "Second file", "File2 mismatch"); char[] data3 = read_zip.read_file_all(mem, "file3.txt")!!; defer free(data3); assert((String)data3 == "Third file", "File3 mismatch"); }; } fn void test_zip_streaming() { @pool() { // Test streaming write ZipArchive zip = zip::open(mem, "unittest_stream.zip", "w")!!; ZipEntryWriter writer = zip.open_writer("stream.txt", DEFLATE)!!; writer.write("Part 1. ")!!; writer.write("Part 2. ")!!; writer.write("Part 3.")!!; writer.close()!!; (void)zip.close(); defer (void)file::delete("unittest_stream.zip"); // Read and verify ZipArchive read_zip = zip::open(mem, "unittest_stream.zip", "r")!!; defer (void)read_zip.close(); char[] data = read_zip.read_file_all(mem, "stream.txt")!!; defer free(data); assert((String)data == "Part 1. Part 2. Part 3.", "Streaming write failed"); }; } fn void test_zip_invalid_access() { @pool() { // Test non-existent archive ZipArchive? opt = zip::open(mem, "non_existent.zip", "r"); assert(!@ok(opt), "Expected error when opening non-existent file"); // Test non-existent entry ZipArchive zip = zip::open(mem, "unittest_edge.zip", "w")!!; zip.write_file("exists.txt", "data")!!; (void)zip.close(); defer (void)file::delete("unittest_edge.zip"); ZipArchive read_zip = zip::open(mem, "unittest_edge.zip", "r")!!; defer (void)read_zip.close(); ZipEntry? entry_opt = read_zip.stat("does_not_exist.txt"); assert(!@ok(entry_opt), "Expected ENTRY_NOT_FOUND"); char[]? data_opt = read_zip.read_file_all(mem, "does_not_exist.txt"); assert(!@ok(data_opt), "Expected error when reading non-existent file"); assert(!@ok(data_opt), "Expected error when reading non-existent file"); }; } fn void test_zip_empty_archive() { @pool() { // Create empty archive ZipArchive zip = zip::open(mem, "unittest_empty.zip", "w")!!; (void)zip.close(); defer (void)file::delete("unittest_empty.zip"); // Read empty archive ZipArchive read_zip = zip::open(mem, "unittest_empty.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 0, "Expected 0 entries"); }; } fn void test_zip_recovery() { @pool() { String path = "unittest_embedded_broken.zip"; // Create a "broken" ZIP (LFH + Data, but no Central Directory) // Filename: "a", Data: "bc" char[] broken_zip = {0x50,0x4B,0x03,0x04,0x14,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x38,0x2B,0xA9,0xC2,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x61,0x62,0x63}; file::save(path, broken_zip)!!; defer (void)file::delete(path); ZipArchive? normal = zip::open(mem, path, "r"); assert(!@ok(normal), "Normal open should fail on broken ZIP"); ZipArchive recovered = zip::recover(mem, path)!!; defer (void)recovered.close(); assert(recovered.count() == 1, "Should have recovered 1 file"); char[] data = recovered.read_file_all(mem, "a")!!; defer free(data); assert((String)data == "bc", "Recovered data mismatch"); }; } fn void test_zip_cp437() { @pool() { String path = "unittest_embedded_cp437.zip"; // Create a ZIP with CP437 encoding (Bit 11 NOT set) // Filename: 0x80 (Ç in CP437), Data: "x" char[] cp437_zip = {0x50,0x4B,0x03,0x04,0x14,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x83,0x16,0xDC,0x8C,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x80,0x78}; file::save(path, cp437_zip)!!; defer (void)file::delete(path); ZipArchive recovered = zip::recover(mem, path)!!; defer (void)recovered.close(); ZipEntry entry = recovered.stat_at(0)!!; assert(entry.name == "Ç", "CP437 decoding failed"); char[] data = recovered.read_file_all(mem, "Ç")!!; defer free(data); assert((String)data == "x", "Data mismatch in CP437 test"); }; } fn void test_zip_with_comment() { @pool() { // Create a ZIP file with a comment ZipArchive zip = zip::open(mem, "unittest_comment.zip", "w")!!; zip.write_file("test.txt", "Hello, World!")!!; (void)zip.close(); defer (void)file::delete("unittest_comment.zip"); char[] zip_data = file::load(mem, "unittest_comment.zip")!!; defer free(zip_data); isz eocd_pos = -1; for (isz i = (isz)zip_data.len - 22; i >= 0; i--) { uint sig = mem::load((uint*)&zip_data[i], 1); if (sig == 0x06054b50) { eocd_pos = i; break; } } assert(eocd_pos >= 0, "EOCD not found"); String comment = "This is a test comment!"; mem::store((ushort*)&zip_data[eocd_pos + 20], (ushort)comment.len, 1); char[] new_zip = mem::new_array(char, zip_data.len + comment.len); defer free(new_zip); mem::copy(new_zip.ptr, zip_data.ptr, zip_data.len); mem::copy(new_zip.ptr + zip_data.len, comment.ptr, comment.len); file::save("unittest_comment.zip", new_zip[:zip_data.len + comment.len])!!; // Try to open it ZipArchive read_zip = zip::open(mem, "unittest_comment.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.count() == 1, "Expected 1 entry"); assert(read_zip.comment == comment, "Comment mismatch"); char[] data = read_zip.read_file_all(mem, "test.txt")!!; defer free(data); assert((String)data == "Hello, World!", "Data mismatch with comment"); }; } fn void test_zip_write_comment() { @pool() { ZipArchive zip = zip::open(mem, "unittest_write_comment.zip", "w")!!; zip.comment = String.copy("Created by C3 ZIP library", zip.allocator); zip.write_file("test.txt", "Hello!")!!; (void)zip.close(); defer (void)file::delete("unittest_write_comment.zip"); ZipArchive read_zip = zip::open(mem, "unittest_write_comment.zip", "r")!!; defer (void)read_zip.close(); assert(read_zip.comment == "Created by C3 ZIP library", "Comment not preserved"); assert(read_zip.count() == 1, "Expected 1 entry"); }; } fn void test_zip64_headers() { @pool() { String filename = "unittest_zip64.zip"; ZipArchive zip = zip::open(mem, filename, "w")!!; ZipEntryWriter writer = zip.open_writer("large.txt", STORE)!!; writer.write("data")!!; // Manually set the size to > 4GB to trigger ZIP64 headers in the Central Directory. // This tests the fix for ZIP64 extra field serialization (ensuring no byte truncation). writer.entry.uncompressed_size = 0x100000001; writer.entry.compressed_size = 0x100000001; writer.close()!!; (void)zip.close(); defer (void)file::delete(filename); ZipArchive read_zip = zip::open(mem, filename, "r")!!; defer (void)read_zip.close(); ZipEntry entry = read_zip.stat("large.txt")!!; assert(entry.uncompressed_size == 0x100000001, "Failed to read ZIP64 uncompressed size"); assert(entry.compressed_size == 0x100000001, "Failed to read ZIP64 compressed size"); }; } fn void test_zip_utf8() { @pool() { String filename = "unittest_utf8.zip"; String utf8_name = "测试_🚀.txt"; ZipArchive zip = zip::open(mem, filename, "w")!!; zip.write_file(utf8_name, "content")!!; (void)zip.close(); defer (void)file::delete(filename); ZipArchive read_zip = zip::open(mem, filename, "r")!!; defer (void)read_zip.close(); ZipEntry entry = read_zip.stat(utf8_name)!!; assert(entry.name == utf8_name, "UTF-8 filename mismatch"); }; } fn void test_zip_zero_length() { @pool() { String filename = "unittest_zero.zip"; ZipArchive zip = zip::open(mem, filename, "w")!!; zip.write_file("empty.txt", "")!!; (void)zip.close(); defer (void)file::delete(filename); ZipArchive read_zip = zip::open(mem, filename, "r")!!; defer (void)read_zip.close(); ZipEntry entry = read_zip.stat("empty.txt")!!; assert(entry.uncompressed_size == 0, "Size should be 0"); char[] data = read_zip.read_file_all(mem, "empty.txt")!!; defer free(data); assert(data.len == 0, "Read data should be empty"); }; } fn void test_zip64_offset() { @pool() { String filename = "unittest_zip64_offset.zip"; ZipArchive zip = zip::open(mem, filename, "w")!!; ZipEntryWriter writer = zip.open_writer("offset_test.txt", STORE)!!; writer.write("data")!!; // Manually set offset to > 4GB to trigger ZIP64 headers in the Central Directory writer.entry.offset = 0x100000005; writer.close()!!; (void)zip.close(); defer (void)file::delete(filename); ZipArchive read_zip = zip::open(mem, filename, "r")!!; defer (void)read_zip.close(); ZipEntry entry = read_zip.stat("offset_test.txt")!!; assert(entry.offset == 0x100000005, "Failed to read ZIP64 offset"); }; } fn void test_zip_reader_pos_and_seek() { @pool() { String path = "unittest_reader.zip"; ZipArchive zip = zip::open(mem, path, "w")!!; zip.write_file("test.txt", "0123456789", STORE)!!; (void)zip.close(); defer (void)file::delete(path); ZipArchive read_zip = zip::open(mem, path, "r")!!; defer (void)read_zip.close(); ZipEntryReader reader = read_zip.open_reader("test.txt")!!; defer (void)reader.close(); assert(reader.len() == 10, "Expected length 10"); assert(reader.available()!! == 10, "Expected 10 bytes available"); assert(reader.pos == 0, "Expected pos 0"); char[3] buf; assert(reader.read(buf[..])!! == 3); assert((String)buf[..] == "012", "Expected '012'"); assert(reader.pos == 3, "Expected pos 3"); assert(reader.available()!! == 7, "Expected 7 bytes available"); assert(reader.seek(2, Seek.CURSOR)!! == 5, "Expected seek to 5"); assert(reader.pos == 5, "Expected pos 5 after seek"); assert(reader.available()!! == 5, "Expected 5 bytes available after seek"); assert(reader.read(buf[..])!! == 3); assert((String)buf[..] == "567", "Expected '567'"); assert(reader.seek(1, Seek.SET)!! == 1, "Expected seek to 1"); assert(reader.read(buf[..])!! == 3); assert((String)buf[..] == "123", "Expected '123'"); assert(reader.seek(-2, Seek.END)!! == 8, "Expected seek to 8"); assert(reader.read(buf[..])!! == 2); assert((String)buf[:2] == "89", "Expected '89'"); assert(reader.available()!! == 0, "Expected 0 bytes available at end"); // Edge case: Negative seek SET assert(!@ok(reader.seek(-1, Seek.SET)), "Negative seek SET should fail"); // Edge case: Seek past end assert(reader.seek(100, Seek.SET)!! == 10, "Seek past end should cap at size"); assert(reader.pos == 10, "Pos should be 10"); }; } fn void test_zip_comment_boundary() { @pool() { String filename = "unittest_comment_limit.zip"; // 1. Test exactly 65535 bytes (Should pass) { ZipArchive zip = zip::open(mem, filename, "w")!!; char[] huge_comment = allocator::malloc(tmem, 65535)[:65535]; mem::set(huge_comment.ptr, (char)'C', 65535); zip.comment = String.copy((String)huge_comment, zip.allocator); zip.write_file("t.txt", "d")!!; (void)zip.close(); ZipArchive read_zip = zip::open(mem, filename, "r")!!; assert(read_zip.comment.len == 65535, "Comment length mismatch at 65535"); (void)read_zip.close(); (void)file::delete(filename); } // 2. Test 65536 bytes (Should fail with INVALID_ARGUMENT) { ZipArchive zip = zip::open(mem, filename, "w")!!; char[] too_huge = allocator::malloc(tmem, 65536)[:65536]; mem::set(too_huge.ptr, (char)'X', 65536); zip.comment = String.copy((String)too_huge, zip.allocator); zip.write_file("t.txt", "d")!!; fault res = @catch(zip.close()); assert(res == zip::INVALID_ARGUMENT, "Expected INVALID_ARGUMENT for 64k+1 comment"); (void)file::delete(filename); } }; } fn void test_zip_reader_available_capping() { @pool() { // We manually construct a reader to test the capping logic for huge entry sizes // that might exist on 32-bit systems (where usz < 64-bit). ZipEntryReader reader; mem::set(&reader, 0, ZipEntryReader.sizeof); reader.size = 0xFFFFFFFFFFFFFFFF; reader.pos = 0; usz avail = reader.available()!!; assert(avail == usz.max, "Expected available to be capped at usz.max"); reader.pos = 100; avail = reader.available()!!; if (usz.max < 0xFFFFFFFFFFFFFFFF) { // triggers on 32-bit assert(avail == usz.max, "Expected available to still be capped at usz.max"); } else { // on 64-bit assert(avail == usz.max - (usz)100, "Expected available size to be correct on 64-bit"); } }; }