code wiki / (root) / nx_zip_header.nx

nx_zip_header.nx source

↩ module page · 210 lines · 7707 B

1// zip_header.nx -- parse PKZIP APPNOTE 6.3.x framing. 2// 3// Parses two key structures in a .zip archive: 4// 1. Local File Header (LFH) -- one per stored file, prefixes 5// each compressed payload. Signature 0x04034b50. 6// 2. End of Central Directory record (EOCD) -- at the tail of 7// the archive; points to the central directory. Signature 8// 0x06054b50. 9// 10// This module does NOT parse the central directory entries (CDFH) 11// or decompress payloads. With just LFH + EOCD the Nishi stack 12// can: detect zip framing, count members, read filenames, 13// identify compression method (0=stored, 8=DEFLATE, 14=LZMA, 14// 93=Zstd...), and find the offset of each file's raw bytes. 15// 16// All multi-byte values in ZIP are little-endian. 17// 18// Invariants: 19// Z1 LFH signature must be 0x04034b50. 20// Z2 EOCD signature must be 0x06054b50 (may be preceded by a 21// variable-length ZIP comment; caller scans backward). 22// Z3 Filenames are stored raw; if bit 11 of general-purpose 23// flag is set, they are UTF-8; otherwise CP437. We expose 24// offset+length and let caller decide. 25// Z4 Compressed-size / uncompressed-size may be 0xFFFFFFFF to 26// indicate Zip64 extensions; we surface that as a sentinel 27// for now. 28 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36const ZIP_MAGIC_66000: i64 = 66000 37 38const ZIP_LFH_SIG: i64 = 0x04034b50 39const ZIP_EOCD_SIG: i64 = 0x06054b50 40 41const ZIP_ERR_FORMAT: i64 = -1 42const ZIP_ERR_SHORT: i64 = -2 43 44const ZIP64_MARKER: i64 = 0xFFFFFFFF 45 46struct ZipLocalHeader { 47 version_needed: i64, 48 flag: i64, 49 method: i64, // 0 stored, 8 deflate, 14 lzma, 93 zstd 50 mod_time: i64, // MS-DOS time 51 mod_date: i64, // MS-DOS date 52 crc32: i64, 53 compressed_size: i64, 54 uncompressed_size: i64, 55 name_off: i64, name_len: i64, 56 extra_off: i64, extra_len: i64, 57 // Header length so caller knows where the compressed payload starts. 58 header_len: i64, 59} 60 61struct ZipEndOfCentralDir { 62 disk_number: i64, 63 cd_start_disk: i64, 64 entries_this_disk: i64, 65 entries_total: i64, 66 cd_size: i64, 67 cd_offset: i64, 68 comment_off: i64, 69 comment_len: i64, 70} 71 72// Little-endian reads. 73func zip_read_u16(buf: *u8, off: i64) -> i64 { 74 return buf[off] | (buf[off + 1] << 8) 75} 76 77func zip_read_u32(buf: *u8, off: i64) -> i64 { 78 let b0: i64 = buf[off] 79 let b1: i64 = buf[off + 1] 80 let b2: i64 = buf[off + 2] 81 let b3: i64 = buf[off + 3] 82 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 83} 84 85// Parse a Local File Header at buf[off]. Returns 0 on success 86// or a negative ZIP_ERR_*. 87func zip_parse_lfh(buf: *u8, n: i64, off: i64, h: *ZipLocalHeader) -> i64 { 88 if n < off + 30 { return ZIP_ERR_SHORT } 89 if zip_read_u32(buf, off) != ZIP_LFH_SIG { return ZIP_ERR_FORMAT } 90 91 h.version_needed = zip_read_u16(buf, off + 4) 92 h.flag = zip_read_u16(buf, off + 6) 93 h.method = zip_read_u16(buf, off + 8) 94 h.mod_time = zip_read_u16(buf, off + 10) 95 h.mod_date = zip_read_u16(buf, off + 12) 96 h.crc32 = zip_read_u32(buf, off + 14) 97 h.compressed_size = zip_read_u32(buf, off + 18) 98 h.uncompressed_size = zip_read_u32(buf, off + 22) 99 let name_len: i64 = zip_read_u16(buf, off + 26) 100 let extra_len: i64 = zip_read_u16(buf, off + 28) 101 102 let name_off: i64 = off + 30 103 if n < name_off + name_len + extra_len { return ZIP_ERR_SHORT } 104 h.name_off = name_off 105 h.name_len = name_len 106 h.extra_off = name_off + name_len 107 h.extra_len = extra_len 108 h.header_len = 30 + name_len + extra_len 109 return 0 110} 111 112// Scan backwards from end-of-buffer for the EOCD signature. ZIP 113// allows a trailing comment up to 65535 bytes, so we bound the 114// scan to the last 66000 bytes. Returns offset of EOCD or -1. 115func zip_find_eocd(buf: *u8, n: i64) -> i64 { 116 var limit: i64 = n - ZIP_MAGIC_66000 117 if limit < 0 { limit = 0 } 118 // EOCD is at least 22 bytes; last possible start = n-22. 119 var off: i64 = n - 22 120 while off >= limit { 121 if off + 3 < n { 122 if zip_read_u32(buf, off) == ZIP_EOCD_SIG { return off } 123 } 124 off = off - 1 125 } 126 return -1 127} 128 129// Parse EOCD at given offset. 130func zip_parse_eocd(buf: *u8, n: i64, off: i64, 131 e: *ZipEndOfCentralDir) -> i64 { 132 if n < off + 22 { return ZIP_ERR_SHORT } 133 if zip_read_u32(buf, off) != ZIP_EOCD_SIG { return ZIP_ERR_FORMAT } 134 135 e.disk_number = zip_read_u16(buf, off + 4) 136 e.cd_start_disk = zip_read_u16(buf, off + 6) 137 e.entries_this_disk = zip_read_u16(buf, off + 8) 138 e.entries_total = zip_read_u16(buf, off + 10) 139 e.cd_size = zip_read_u32(buf, off + 12) 140 e.cd_offset = zip_read_u32(buf, off + 16) 141 let comment_len: i64 = zip_read_u16(buf, off + 20) 142 if n < off + 22 + comment_len { return ZIP_ERR_SHORT } 143 e.comment_off = off + 22 144 e.comment_len = comment_len 145 return 0 146} 147 148// Helper: map compression method code to a short name length. 149// Returns 0 if unknown. Used for debug printing; callers print 150// from a static table. 151func zip_method_known(method: i64) -> i64 { 152 if method == 0 { return 1 } // stored 153 if method == 8 { return 1 } // deflate 154 if method == 14 { return 1 } // lzma 155 if method == 93 { return 1 } // zstd 156 if method == 95 { return 1 } // xz 157 if method == 98 { return 1 } // ppmd 158 return 0 159} 160 161// Compile-only smoke: forge minimal LFH + EOCD in a buffer. 162func main() -> i64 { 163 let raw: *u8 = sys_mmap(256) 164 var i: i64 = 0 165 while i < 256 { raw[i] = 0; i = i + 1 } 166 167 // LFH at offset 0: sig, version 20, flag 0, method 0 (stored), 168 // time/date 0, crc 0, csize 5, usize 5, name_len 3 "foo", 169 // extra_len 0. 170 raw[0] = 0x50; raw[1] = 0x4b; raw[2] = 0x03; raw[3] = 0x04 171 raw[4] = 20; raw[5] = 0 172 raw[6] = 0; raw[7] = 0 173 raw[8] = 0; raw[9] = 0 174 raw[18] = 5 // compressed_size low byte 175 raw[22] = 5 // uncompressed_size low byte 176 raw[26] = 3 // name_len 177 raw[30] = 0x66 // 'f' 178 raw[31] = 0x6F // 'o' 179 raw[32] = 0x6F // 'o' 180 181 // EOCD at offset 40: sig, zeros, entries 1, cd_size 0, cd_off 38, 182 // comment_len 0. 22 bytes total. 183 let eocd: i64 = 40 184 raw[eocd] = 0x50; raw[eocd+1] = 0x4b 185 raw[eocd+2] = 0x05; raw[eocd+3] = 0x06 186 raw[eocd+8] = 1 // entries_this_disk low 187 raw[eocd+10] = 1 // entries_total low 188 raw[eocd+16] = 33 // cd_offset low (just any value) 189 190 let lfh_raw: *u8 = sys_mmap(128) 191 let lfh: *ZipLocalHeader = lfh_raw as *ZipLocalHeader 192 if zip_parse_lfh(raw, 256, 0, lfh) != 0 { return 1 } 193 if lfh.method != 0 { return 2 } 194 if lfh.name_len != 3 { return 3 } 195 if lfh.compressed_size != 5 { return 4 } 196 if lfh.header_len != 33 { return 5 } 197 198 let eocd_off: i64 = zip_find_eocd(raw, eocd + 22) 199 if eocd_off != eocd { return 6 } 200 201 let ee_raw: *u8 = sys_mmap(128) 202 let ee: *ZipEndOfCentralDir = ee_raw as *ZipEndOfCentralDir 203 if zip_parse_eocd(raw, 256, eocd_off, ee) != 0 { return 7 } 204 if ee.entries_total != 1 { return 8 } 205 if ee.cd_offset != 33 { return 9 } 206 207 if zip_method_known(8) != 1 { return 10 } 208 if zip_method_known(999) != 0 { return 11 } 209 return 0 210}