nx_http_date.nx source
↩ module page · 195 lines · 6497 B
1// http_date.nx -- format + parse RFC 7231 IMF-fixdate.
2//
3// HTTP's native date format:
4// Sun, 06 Nov 1994 08:49:37 GMT
5// ^^^ ^^ ^^^ ^^^^ ^^ ^^ ^^ ^^^
6// wkd dd mon yyyy hh mm ss tz
7//
8// Used in: Date, Last-Modified, Expires, If-Modified-Since,
9// If-Unmodified-Since headers. RFC 7231 §7.1.1 strongly prefers
10// this form over the older rfc850 / asctime variants; we only
11// produce fixdate and accept fixdate on parse.
12//
13// Always emitted with UTC / "GMT" suffix.
14//
15// Composes civil_date.nx. Built from the same primitives as
16// iso8601.nx but with English weekday + month names.
17//
18// Invariants:
19// HD1 Output is exactly 29 bytes.
20// HD2 Parser rejects non-fixdate forms (no rfc850
21// "Sun-day, 06-Nov-94" or asctime "Sun Nov 6 ..." --
22// per RFC 7231 those are deprecated; we log instead of
23// silently accept).
24// HD3 Round-trip exact for the fixdate form we emit.
25
26// nx_safety_envelope:
27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
28// sil_target: SIL1
29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
30// verdict: NOT_YET_EVALUATED
31
32import "nx_syscalls.nx"
33import "nx_civil_date.nx"
34const HD_MAGIC_1994: i64 = 1994
35
36const HD_ERR_FORMAT: i64 = -1
37const HD_ERR_SHORT: i64 = -2
38
39// 3-letter weekday names -- Sunday..Saturday. 9 bytes each slot
40// padded to 4, stored as a flat byte array so index = day * 4.
41func hd_weekday_bytes() -> *u8 {
42 return "Sun Mon Tue Wed Thu Fri Sat "
43}
44
45// 3-letter month names -- month 1..12; index 0 unused.
46func hd_month_bytes() -> *u8 {
47 return " Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec "
48}
49
50// Copy 3 bytes from tbl at index * 4 to out[off].
51func hd_put3(out: *u8, off: i64, tbl: *u8, idx: i64) -> i64 {
52 out[off] = tbl[idx * 4]
53 out[off + 1] = tbl[idx * 4 + 1]
54 out[off + 2] = tbl[idx * 4 + 2]
55 return off + 3
56}
57
58// 2-digit zero-padded integer.
59func hd_put_2d(out: *u8, off: i64, v: i64) -> i64 {
60 out[off] = 0x30 + ((v / 10) % 10)
61 out[off + 1] = 0x30 + (v % 10)
62 return off + 2
63}
64
65// 4-digit zero-padded year.
66func hd_put_4d(out: *u8, off: i64, v: i64) -> i64 {
67 out[off] = 0x30 + ((v / 1000) % 10)
68 out[off + 1] = 0x30 + ((v / 100) % 10)
69 out[off + 2] = 0x30 + ((v / 10) % 10)
70 out[off + 3] = 0x30 + (v % 10)
71 return off + 4
72}
73
74// Format a CivilDate as IMF-fixdate. Output length = 29 bytes.
75func http_date_format(out: *u8, cap: i64, cd: *CivilDate) -> i64 {
76 if cap < 29 { return HD_ERR_SHORT }
77 var off: i64 = 0
78 off = hd_put3(out, off, hd_weekday_bytes(), cd.weekday)
79 out[off] = 0x2C; off = off + 1 // ','
80 out[off] = 0x20; off = off + 1 // ' '
81 off = hd_put_2d(out, off, cd.day)
82 out[off] = 0x20; off = off + 1
83 off = hd_put3(out, off, hd_month_bytes(), cd.month)
84 out[off] = 0x20; off = off + 1
85 off = hd_put_4d(out, off, cd.year)
86 out[off] = 0x20; off = off + 1
87 off = hd_put_2d(out, off, cd.hour)
88 out[off] = 0x3A; off = off + 1 // ':'
89 off = hd_put_2d(out, off, cd.minute)
90 out[off] = 0x3A; off = off + 1
91 off = hd_put_2d(out, off, cd.second)
92 out[off] = 0x20; off = off + 1
93 out[off] = 0x47 // 'G'
94 out[off + 1] = 0x4D // 'M'
95 out[off + 2] = 0x54 // 'T'
96 return 29
97}
98
99// Convenience: format unix seconds directly.
100func http_date_format_unix(out: *u8, cap: i64, unix_sec: i64) -> i64 {
101 let cd_raw: *u8 = sys_mmap(128)
102 let cd: *CivilDate = cd_raw as *CivilDate
103 civil_from_unix(unix_sec, cd)
104 return http_date_format(out, cap, cd)
105}
106
107// Parse 2 decimal digits.
108func hd_parse_2d(buf: *u8, off: i64) -> i64 {
109 let a: i64 = buf[off]
110 let b: i64 = buf[off + 1]
111 if a < 0x30 { return -1 }
112 if a > 0x39 { return -1 }
113 if b < 0x30 { return -1 }
114 if b > 0x39 { return -1 }
115 return (a - 0x30) * 10 + (b - 0x30)
116}
117
118// Parse 4 decimal digits.
119func hd_parse_4d(buf: *u8, off: i64) -> i64 {
120 let hi: i64 = hd_parse_2d(buf, off)
121 if hi < 0 { return -1 }
122 let lo: i64 = hd_parse_2d(buf, off + 2)
123 if lo < 0 { return -1 }
124 return hi * 100 + lo
125}
126
127// Match a 3-byte month name at buf[off]. Returns 1..12 or -1.
128func hd_parse_month(buf: *u8, off: i64) -> i64 {
129 let tbl: *u8 = hd_month_bytes()
130 var i: i64 = 1
131 while i <= 12 {
132 if buf[off] == tbl[i*4] {
133 if buf[off + 1] == tbl[i*4+1] {
134 if buf[off + 2] == tbl[i*4+2] {
135 return i
136 }
137 }
138 }
139 i = i + 1
140 }
141 return -1
142}
143
144// Parse an IMF-fixdate string into *unix_sec_out.
145func http_date_parse(buf: *u8, n: i64, unix_sec_out: *i64) -> i64 {
146 if n < 29 { return HD_ERR_SHORT }
147 // Skip weekday + ", " (5 bytes).
148 let d: i64 = hd_parse_2d(buf, 5)
149 if d < 0 { return HD_ERR_FORMAT }
150 if buf[7] != 0x20 { return HD_ERR_FORMAT }
151 let mo: i64 = hd_parse_month(buf, 8)
152 if mo < 0 { return HD_ERR_FORMAT }
153 if buf[11] != 0x20 { return HD_ERR_FORMAT }
154 let y: i64 = hd_parse_4d(buf, 12)
155 if y < 0 { return HD_ERR_FORMAT }
156 if buf[16] != 0x20 { return HD_ERR_FORMAT }
157 let h: i64 = hd_parse_2d(buf, 17)
158 if h < 0 { return HD_ERR_FORMAT }
159 if buf[19] != 0x3A { return HD_ERR_FORMAT }
160 let mi: i64 = hd_parse_2d(buf, 20)
161 if mi < 0 { return HD_ERR_FORMAT }
162 if buf[22] != 0x3A { return HD_ERR_FORMAT }
163 let s: i64 = hd_parse_2d(buf, 23)
164 if s < 0 { return HD_ERR_FORMAT }
165 if buf[25] != 0x20 { return HD_ERR_FORMAT }
166 if buf[26] != 0x47 { return HD_ERR_FORMAT } // 'G'
167 if buf[27] != 0x4D { return HD_ERR_FORMAT } // 'M'
168 if buf[28] != 0x54 { return HD_ERR_FORMAT } // 'T'
169
170 *unix_sec_out = unix_from_civil(y, mo, d, h, mi, s)
171 return 0
172}
173
174// Compile-only smoke.
175func main() -> i64 {
176 // 1994-11-06 08:49:37 UTC = Sunday. From RFC 7231 §7.1.1.
177 let u: i64 = unix_from_civil(HD_MAGIC_1994, 11, 6, 8, 49, 37)
178
179 let out: *u8 = sys_mmap(64)
180 if http_date_format_unix(out, 64, u) != 29 { return 1 }
181 // Expected: "Sun, 06 Nov 1994 08:49:37 GMT"
182 let expected: *u8 = "Sun, 06 Nov 1994 08:49:37 GMT"
183 var i: i64 = 0
184 while i < 29 {
185 if out[i] != expected[i] { return 10 + i }
186 i = i + 1
187 }
188
189 // Round-trip.
190 let parsed: *i64 = (sys_mmap(16)) as *i64
191 if http_date_parse(out, 29, parsed) != 0 { return 2 }
192 if *parsed != u { return 3 }
193
194 return 0
195}