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}