code wiki / (root) / nx_tar_header.nx

nx_tar_header.nx source

↩ module page · 188 lines · 6642 B

1// tar_header.nx -- parse POSIX ustar tar headers (IEEE 1003.1). 2// 3// Each tar entry is a 512-byte header followed by its file data 4// padded to a 512-byte boundary. The archive ends with at least 5// two zero-filled 512-byte blocks. 6// 7// Header layout: 8// offset size field 9// 0 100 name (NUL-terminated if shorter) 10// 100 8 mode (octal ASCII) 11// 108 8 uid (octal ASCII) 12// 116 8 gid (octal ASCII) 13// 124 12 size (octal ASCII, bytes of file data) 14// 136 12 mtime (octal ASCII seconds since epoch) 15// 148 8 header checksum (octal ASCII) 16// 156 1 typeflag ('0'=regular, '5'=dir, '2'=symlink, 17// '1'=hardlink, 'x'=pax extended, 'g'=pax global) 18// 157 100 linkname 19// 257 6 magic "ustar\0" (POSIX) or "ustar " (GNU) 20// 263 2 version "00" (POSIX) or " \0" (GNU) 21// 265 32 uname (owner name) 22// 297 32 gname (group name) 23// 329 8 devmajor (octal) 24// 337 8 devminor (octal) 25// 345 155 prefix (prepended to name for long paths) 26// 27// Invariants: 28// T1 Magic must be "ustar" -- the first 5 bytes at offset 257. 29// T2 Size fields are parsed as big-endian octal ASCII. GNU tar 30// optionally encodes large sizes as binary with MSB=0x80; 31// we reject those (caller can detect and extend). 32// T3 Checksum verification not performed here (future work); 33// we parse the field value but don't validate. 34 35// nx_safety_envelope: 36// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 37// sil_target: SIL1 38// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 39// verdict: NOT_YET_EVALUATED 40 41import "nx_syscalls.nx" 42const TAR_MAGIC_1024: i64 = 1024 43 44const TAR_BLOCK: i64 = 512 45 46const TAR_ERR_FORMAT: i64 = -1 47const TAR_ERR_SHORT: i64 = -2 48const TAR_ERR_OCTAL: i64 = -3 49 50struct TarHeader { 51 name_off: i64, name_len: i64, 52 prefix_off: i64, prefix_len: i64, 53 link_off: i64, link_len: i64, 54 mode: i64, 55 uid: i64, 56 gid: i64, 57 size: i64, // file payload size 58 mtime: i64, 59 checksum: i64, 60 typeflag: i64, // raw byte 61 // Total record size including payload + padding, so caller can 62 // advance to the next header. 63 record_len: i64, 64} 65 66// Parse an octal ASCII number field of given length. Stops at 67// NUL, space, or end. Returns value or TAR_ERR_OCTAL on digit 68// out of range. 69func tar_parse_octal(buf: *u8, off: i64, n: i64) -> i64 { 70 var v: i64 = 0 71 var i: i64 = 0 72 while i < n { 73 let c: i64 = buf[off + i] 74 if c == 0 { return v } 75 if c == 0x20 { return v } // space = end 76 if c < 0x30 { return TAR_ERR_OCTAL } 77 if c > 0x37 { return TAR_ERR_OCTAL } 78 v = v * 8 + (c - 0x30) 79 i = i + 1 80 } 81 return v 82} 83 84// Scan a NUL terminator within a bounded window. Returns length 85// or the window size if no NUL found (tar name field without 86// terminator means "full 100 bytes"). 87func tar_scan_nul(buf: *u8, off: i64, max: i64) -> i64 { 88 var i: i64 = 0 89 while i < max { 90 if buf[off + i] == 0 { return i } 91 i = i + 1 92 } 93 return max 94} 95 96// Check for the end-of-archive marker: a block of 512 zero bytes. 97// Returns 1 if this block is all zero, 0 otherwise. 98func tar_is_zero_block(buf: *u8, off: i64) -> i64 { 99 var i: i64 = 0 100 while i < TAR_BLOCK { 101 if buf[off + i] != 0 { return 0 } 102 i = i + 1 103 } 104 return 1 105} 106 107// Parse one tar header at offset. Returns 0 on success and fills 108// h; returns TAR_ERR_* on failure. 109func tar_parse_header(buf: *u8, n: i64, off: i64, h: *TarHeader) -> i64 { 110 if n < off + TAR_BLOCK { return TAR_ERR_SHORT } 111 112 // Magic "ustar" at offset 257. 113 if buf[off + 257] != 0x75 { return TAR_ERR_FORMAT } // 'u' 114 if buf[off + 258] != 0x73 { return TAR_ERR_FORMAT } // 's' 115 if buf[off + 259] != 0x74 { return TAR_ERR_FORMAT } // 't' 116 if buf[off + 260] != 0x61 { return TAR_ERR_FORMAT } // 'a' 117 if buf[off + 261] != 0x72 { return TAR_ERR_FORMAT } // 'r' 118 119 h.name_off = off + 0 120 h.name_len = tar_scan_nul(buf, off + 0, 100) 121 h.prefix_off = off + 345 122 h.prefix_len = tar_scan_nul(buf, off + 345, 155) 123 h.link_off = off + 157 124 h.link_len = tar_scan_nul(buf, off + 157, 100) 125 126 h.mode = tar_parse_octal(buf, off + 100, 8) 127 h.uid = tar_parse_octal(buf, off + 108, 8) 128 h.gid = tar_parse_octal(buf, off + 116, 8) 129 h.size = tar_parse_octal(buf, off + 124, 12) 130 h.mtime = tar_parse_octal(buf, off + 136, 12) 131 h.checksum = tar_parse_octal(buf, off + 148, 8) 132 h.typeflag = buf[off + 156] 133 134 if h.size < 0 { return TAR_ERR_OCTAL } 135 136 // Data payload rounds up to 512. record_len = header + padded data. 137 var padded: i64 = h.size 138 let rem: i64 = h.size % TAR_BLOCK 139 if rem != 0 { padded = padded + (TAR_BLOCK - rem) } 140 h.record_len = TAR_BLOCK + padded 141 return 0 142} 143 144// Compile-only smoke -- forge a minimal ustar header. 145func main() -> i64 { 146 let raw: *u8 = sys_mmap(TAR_BLOCK + 512) 147 var i: i64 = 0 148 while i < TAR_BLOCK + 512 { raw[i] = 0; i = i + 1 } 149 150 // name = "hello.txt" 151 raw[0] = 0x68; raw[1] = 0x65; raw[2] = 0x6C; raw[3] = 0x6C 152 raw[4] = 0x6F; raw[5] = 0x2E; raw[6] = 0x74; raw[7] = 0x78; raw[8] = 0x74 153 154 // mode "000644\0 " occupies offset 100..107, NUL-terminated. 155 raw[100] = 0x30; raw[101] = 0x30; raw[102] = 0x30 156 raw[103] = 0x36; raw[104] = 0x34; raw[105] = 0x34 157 158 // size "13\0" at offset 124 (11 bytes then NUL). Octal 13 = dec 11. 159 raw[124] = 0x31; raw[125] = 0x33 160 161 // typeflag '0' = regular file 162 raw[156] = 0x30 163 164 // magic "ustar" at offset 257 + NUL + version "00" 165 raw[257] = 0x75; raw[258] = 0x73; raw[259] = 0x74 166 raw[260] = 0x61; raw[261] = 0x72 167 raw[263] = 0x30; raw[264] = 0x30 168 169 let h_raw: *u8 = sys_mmap(256) 170 let h: *TarHeader = h_raw as *TarHeader 171 if tar_parse_header(raw, TAR_MAGIC_1024, 0, h) != 0 { return 1 } 172 173 if h.name_len != 9 { return 2 } 174 if h.mode != 420 { return 3 } // 0o644 = 420 decimal 175 if h.size != 11 { return 4 } 176 if h.typeflag != 0x30 { return 5 } 177 178 // Record length = 512 header + 512 padded data (11 rounds up). 179 if h.record_len != TAR_MAGIC_1024 { return 6 } 180 181 // End-of-archive: the (unused) second block should be all zero. 182 if tar_is_zero_block(raw, 512) != 1 { return 7 } 183 184 // Bad magic -> TAR_ERR_FORMAT. 185 raw[257] = 0 186 if tar_parse_header(raw, TAR_MAGIC_1024, 0, h) != TAR_ERR_FORMAT { return 8 } 187 return 0 188}