tar_header.nx source
↩ module page · 181 lines · 6506 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
35import "syscalls.nx"
36
37const TAR_BLOCK: i64 = 512
38
39const TAR_ERR_FORMAT: i64 = -1
40const TAR_ERR_SHORT: i64 = -2
41const TAR_ERR_OCTAL: i64 = -3
42
43struct TarHeader {
44 name_off: i64, name_len: i64,
45 prefix_off: i64, prefix_len: i64,
46 link_off: i64, link_len: i64,
47 mode: i64,
48 uid: i64,
49 gid: i64,
50 size: i64, // file payload size
51 mtime: i64,
52 checksum: i64,
53 typeflag: i64, // raw byte
54 // Total record size including payload + padding, so caller can
55 // advance to the next header.
56 record_len: i64,
57}
58
59// Parse an octal ASCII number field of given length. Stops at
60// NUL, space, or end. Returns value or TAR_ERR_OCTAL on digit
61// out of range.
62func tar_parse_octal(buf: *u8, off: i64, n: i64) -> i64 {
63 var v: i64 = 0
64 var i: i64 = 0
65 while i < n {
66 let c: i64 = buf[off + i]
67 if c == 0 { return v }
68 if c == 0x20 { return v } // space = end
69 if c < 0x30 { return TAR_ERR_OCTAL }
70 if c > 0x37 { return TAR_ERR_OCTAL }
71 v = v * 8 + (c - 0x30)
72 i = i + 1
73 }
74 return v
75}
76
77// Scan a NUL terminator within a bounded window. Returns length
78// or the window size if no NUL found (tar name field without
79// terminator means "full 100 bytes").
80func tar_scan_nul(buf: *u8, off: i64, max: i64) -> i64 {
81 var i: i64 = 0
82 while i < max {
83 if buf[off + i] == 0 { return i }
84 i = i + 1
85 }
86 return max
87}
88
89// Check for the end-of-archive marker: a block of 512 zero bytes.
90// Returns 1 if this block is all zero, 0 otherwise.
91func tar_is_zero_block(buf: *u8, off: i64) -> i64 {
92 var i: i64 = 0
93 while i < TAR_BLOCK {
94 if buf[off + i] != 0 { return 0 }
95 i = i + 1
96 }
97 return 1
98}
99
100// Parse one tar header at offset. Returns 0 on success and fills
101// h; returns TAR_ERR_* on failure.
102func tar_parse_header(buf: *u8, n: i64, off: i64, h: *TarHeader) -> i64 {
103 if n < off + TAR_BLOCK { return TAR_ERR_SHORT }
104
105 // Magic "ustar" at offset 257.
106 if buf[off + 257] != 0x75 { return TAR_ERR_FORMAT } // 'u'
107 if buf[off + 258] != 0x73 { return TAR_ERR_FORMAT } // 's'
108 if buf[off + 259] != 0x74 { return TAR_ERR_FORMAT } // 't'
109 if buf[off + 260] != 0x61 { return TAR_ERR_FORMAT } // 'a'
110 if buf[off + 261] != 0x72 { return TAR_ERR_FORMAT } // 'r'
111
112 h.name_off = off + 0
113 h.name_len = tar_scan_nul(buf, off + 0, 100)
114 h.prefix_off = off + 345
115 h.prefix_len = tar_scan_nul(buf, off + 345, 155)
116 h.link_off = off + 157
117 h.link_len = tar_scan_nul(buf, off + 157, 100)
118
119 h.mode = tar_parse_octal(buf, off + 100, 8)
120 h.uid = tar_parse_octal(buf, off + 108, 8)
121 h.gid = tar_parse_octal(buf, off + 116, 8)
122 h.size = tar_parse_octal(buf, off + 124, 12)
123 h.mtime = tar_parse_octal(buf, off + 136, 12)
124 h.checksum = tar_parse_octal(buf, off + 148, 8)
125 h.typeflag = buf[off + 156]
126
127 if h.size < 0 { return TAR_ERR_OCTAL }
128
129 // Data payload rounds up to 512. record_len = header + padded data.
130 var padded: i64 = h.size
131 let rem: i64 = h.size % TAR_BLOCK
132 if rem != 0 { padded = padded + (TAR_BLOCK - rem) }
133 h.record_len = TAR_BLOCK + padded
134 return 0
135}
136
137// Compile-only smoke -- forge a minimal ustar header.
138func main() -> i64 {
139 let raw: *u8 = sys_mmap(TAR_BLOCK + 512)
140 var i: i64 = 0
141 while i < TAR_BLOCK + 512 { raw[i] = 0; i = i + 1 }
142
143 // name = "hello.txt"
144 raw[0] = 0x68; raw[1] = 0x65; raw[2] = 0x6C; raw[3] = 0x6C
145 raw[4] = 0x6F; raw[5] = 0x2E; raw[6] = 0x74; raw[7] = 0x78; raw[8] = 0x74
146
147 // mode "000644\0 " occupies offset 100..107, NUL-terminated.
148 raw[100] = 0x30; raw[101] = 0x30; raw[102] = 0x30
149 raw[103] = 0x36; raw[104] = 0x34; raw[105] = 0x34
150
151 // size "13\0" at offset 124 (11 bytes then NUL). Octal 13 = dec 11.
152 raw[124] = 0x31; raw[125] = 0x33
153
154 // typeflag '0' = regular file
155 raw[156] = 0x30
156
157 // magic "ustar" at offset 257 + NUL + version "00"
158 raw[257] = 0x75; raw[258] = 0x73; raw[259] = 0x74
159 raw[260] = 0x61; raw[261] = 0x72
160 raw[263] = 0x30; raw[264] = 0x30
161
162 let h_raw: *u8 = sys_mmap(256)
163 let h: *TarHeader = h_raw as *TarHeader
164 if tar_parse_header(raw, 1024, 0, h) != 0 { return 1 }
165
166 if h.name_len != 9 { return 2 }
167 if h.mode != 420 { return 3 } // 0o644 = 420 decimal
168 if h.size != 11 { return 4 }
169 if h.typeflag != 0x30 { return 5 }
170
171 // Record length = 512 header + 512 padded data (11 rounds up).
172 if h.record_len != 1024 { return 6 }
173
174 // End-of-archive: the (unused) second block should be all zero.
175 if tar_is_zero_block(raw, 512) != 1 { return 7 }
176
177 // Bad magic -> TAR_ERR_FORMAT.
178 raw[257] = 0
179 if tar_parse_header(raw, 1024, 0, h) != TAR_ERR_FORMAT { return 8 }
180 return 0
181}