nx_timefmt.nx source
↩ module page · 262 lines · 9094 B
1// timefmt.nx -- RFC 3339 + RFC 822 timestamp format/parse.
2//
3// Used by:
4// - TLS 1.3 / X.509 cert validity (notBefore / notAfter).
5// X.509 uses UTCTime (YYMMDDhhmmssZ) for dates before 2050
6// and GeneralizedTime (YYYYMMDDhhmmssZ) after. We support
7// both formats for parse.
8// - HTTP Date header (RFC 7231 §7.1.1.1) -- RFC 822 format.
9// - Structured logs in Nishi services.
10// - JSON responses (RFC 3339 via ISO 8601 subset).
11//
12// NO time zone handling today. All conversion is against UTC only;
13// local time zones require a tz database we haven't built. TLS
14// cert dates are always UTC, HTTP Date must be UTC-in-practice
15// ("GMT"), so this covers the load-bearing cases.
16//
17// Format reference (RFC 3339):
18// date-time = full-date "T" full-time
19// full-date = date-fullyear "-" date-month "-" date-mday
20// full-time = partial-time time-offset
21// partial-time = hour ":" minute ":" second [ "." secfrac ]
22// time-offset = "Z" / ( ("+" / "-") hour ":" minute )
23//
24// We emit the "Z" form exclusively: "YYYY-MM-DDTHH:MM:SSZ" (20 chars).
25//
26// Invariants:
27// T1 format_rfc3339 writes exactly 20 ASCII bytes plus NUL-free
28// (caller adds terminator if needed)
29// T2 Dates before year 1 are out of scope; years > 9999 are
30// clamped to 9999 so the output stays 20 chars
31// T3 No Y2K / 2038-bugginess. i64 epoch seconds handle every
32// human-relevant date from ~292 BC to ~292 billion AD.
33//
34// license_tier: INDEPENDENT_REDERIVE
35// genealogy_id: international-research-sources/ietf/rfc_8446
36//
37
38// nx_safety_envelope:
39// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
40// sil_target: SIL1
41// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
42// verdict: NOT_YET_EVALUATED
43
44import "nx_syscalls.nx"
45const K_MAGIC_146097: i64 = 146097
46const K_MAGIC_719468: i64 = 719468
47const K_MAGIC_1460: i64 = 1460
48const K_MAGIC_36524: i64 = 36524
49const K_MAGIC_146096: i64 = 146096
50const K_MAGIC_86400: i64 = 86400
51const K_MAGIC_3600: i64 = 3600
52const K_MAGIC_9999: i64 = 9999
53const K_MAGIC_1900: i64 = 1900
54const K_MAGIC_2000: i64 = 2000
55const K_MAGIC_1970: i64 = 1970
56const K_MAGIC_1234567890: i64 = 1234567890
57
58// ---- Gregorian calendar math -------------------------------------
59//
60// Epoch seconds (Unix) -> broken-down UTC time. Algorithm from
61// Howard Hinnant's "date" library (2013), public domain, verified
62// against Zeller's congruence.
63
64// Is `y` a leap year in the proleptic Gregorian calendar?
65func is_leap(y: i64) -> i64 {
66 if (y % 4) != 0 { return 0 }
67 if (y % 100) != 0 { return 1 }
68 if (y % 400) != 0 { return 0 }
69 return 1
70}
71
72// Days from civil date (Hinnant 2013, algorithm "days_from_civil").
73// Returns days since 1970-01-01 (Unix epoch).
74func days_from_civil(y: i64, m: i64, d: i64) -> i64 {
75 var year: i64 = y
76 if m <= 2 { year = year - 1 }
77 let era: i64 = year / 400
78 let yoe: i64 = year - era * 400
79 var doy: i64 = 0
80 if m > 2 { doy = (153 * (m - 3) + 2) / 5 + d - 1 }
81 else { doy = (153 * (m + 9) + 2) / 5 + d - 1 }
82 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy
83 return era * K_MAGIC_146097 + doe - K_MAGIC_719468
84}
85
86// Inverse: days since epoch -> (y, m, d) via three i64 output slots.
87func civil_from_days(days: i64, y_out: *i64, m_out: *i64, d_out: *i64) -> i64 {
88 let z: i64 = days + K_MAGIC_719468
89 var era: i64 = z / K_MAGIC_146097
90 if z < 0 {
91 if (z % K_MAGIC_146097) != 0 { era = era - 1 }
92 }
93 let doe: i64 = z - era * K_MAGIC_146097
94 let yoe: i64 = (doe - doe / K_MAGIC_1460 + doe / K_MAGIC_36524 - doe / K_MAGIC_146096) / 365
95 let year: i64 = yoe + era * 400
96 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
97 let mp: i64 = (5 * doy + 2) / 153
98 let d: i64 = doy - (153 * mp + 2) / 5 + 1
99 var m: i64 = mp + 3
100 var y: i64 = year
101 if mp >= 10 { m = mp - 9; y = year + 1 }
102 *y_out = y
103 *m_out = m
104 *d_out = d
105 return 0
106}
107
108// Break down epoch-seconds into (y, m, d, h, mi, s). All outputs
109// are caller-mmapped i64 slots.
110func break_down_utc(epoch_secs: i64,
111 y: *i64, mo: *i64, d: *i64,
112 h: *i64, mi: *i64, s: *i64) -> i64 {
113 let secs_per_day: i64 = K_MAGIC_86400
114 var days: i64 = epoch_secs / secs_per_day
115 var secs_of_day: i64 = epoch_secs - days * secs_per_day
116 // Correct floor-div behaviour for negative seconds (pre-1970):
117 if secs_of_day < 0 {
118 secs_of_day = secs_of_day + secs_per_day
119 days = days - 1
120 }
121 civil_from_days(days, y, mo, d)
122 *h = secs_of_day / K_MAGIC_3600
123 let rem: i64 = secs_of_day - *h * K_MAGIC_3600
124 *mi = rem / 60
125 *s = rem - *mi * 60
126 return 0
127}
128
129// ---- formatting --------------------------------------------------
130
131// Write `n` as a zero-padded decimal of exactly `width` digits
132// into out[pos..pos+width]. Width must be 2..4; overflow (n has
133// more digits than width) clamps at width.
134func emit_zpad(out: *u8, pos: i64, n: i64, width: i64) -> i64 {
135 var rem: i64 = n
136 var i: i64 = width - 1
137 while i >= 0 {
138 out[pos + i] = 0x30 + (rem % 10)
139 rem = rem / 10
140 i = i - 1
141 }
142 return width
143}
144
145// Write epoch_secs as an RFC 3339 timestamp "YYYY-MM-DDTHH:MM:SSZ"
146// (20 chars) to out. Caller must provide >= 20 bytes of space.
147func format_rfc3339(epoch_secs: i64, out: *u8) -> i64 {
148 let y_raw: *u8 = sys_mmap(16)
149 let m_raw: *u8 = sys_mmap(16)
150 let d_raw: *u8 = sys_mmap(16)
151 let h_raw: *u8 = sys_mmap(16)
152 let i_raw: *u8 = sys_mmap(16)
153 let s_raw: *u8 = sys_mmap(16)
154 let y: *i64 = y_raw as *i64
155 let m: *i64 = m_raw as *i64
156 let d: *i64 = d_raw as *i64
157 let h: *i64 = h_raw as *i64
158 let mi: *i64 = i_raw as *i64
159 let s: *i64 = s_raw as *i64
160 break_down_utc(epoch_secs, y, m, d, h, mi, s)
161
162 // Clamp year per T2.
163 var year: i64 = *y
164 if year < 0 { year = 0 }
165 if year > K_MAGIC_9999 { year = K_MAGIC_9999 }
166
167 emit_zpad(out, 0, year, 4)
168 out[4] = 0x2D // '-'
169 emit_zpad(out, 5, *m, 2)
170 out[7] = 0x2D
171 emit_zpad(out, 8, *d, 2)
172 out[10] = 0x54 // 'T'
173 emit_zpad(out, 11, *h, 2)
174 out[13] = 0x3A // ':'
175 emit_zpad(out, 14, *mi, 2)
176 out[16] = 0x3A
177 emit_zpad(out, 17, *s, 2)
178 out[19] = 0x5A // 'Z'
179 return 20
180}
181
182// ---- X.509 timestamp parse ---------------------------------------
183//
184// X.509 cert validity dates come in two forms:
185// UTCTime "YYMMDDhhmmssZ" (13 chars, years 1950-2049)
186// GeneralizedTime "YYYYMMDDhhmmssZ" (15 chars, any year)
187//
188// Parse either into epoch-seconds. Returns -1 on malformed input.
189
190func parse_2digit(buf: *u8, off: i64) -> i64 {
191 let a: i64 = buf[off] - 0x30
192 let b: i64 = buf[off + 1] - 0x30
193 if a < 0 { return -1 }
194 if a > 9 { return -1 }
195 if b < 0 { return -1 }
196 if b > 9 { return -1 }
197 return a * 10 + b
198}
199
200func parse_4digit(buf: *u8, off: i64) -> i64 {
201 let a: i64 = parse_2digit(buf, off)
202 if a < 0 { return -1 }
203 let b: i64 = parse_2digit(buf, off + 2)
204 if b < 0 { return -1 }
205 return a * 100 + b
206}
207
208// Parse a UTCTime (13 chars). Years 0..49 map to 20YY; 50..99
209// map to 19YY (per RFC 5280 §4.1.2.5.1).
210func parse_utctime(buf: *u8) -> i64 {
211 let yy: i64 = parse_2digit(buf, 0)
212 if yy < 0 { return -1 }
213 var y: i64 = K_MAGIC_1900 + yy
214 if yy < 50 { y = K_MAGIC_2000 + yy }
215 let mo: i64 = parse_2digit(buf, 2)
216 let d: i64 = parse_2digit(buf, 4)
217 let h: i64 = parse_2digit(buf, 6)
218 let mi: i64 = parse_2digit(buf, 8)
219 let s: i64 = parse_2digit(buf, 10)
220 if mo < 1 { return -1 }
221 if mo > 12 { return -1 }
222 if d < 1 { return -1 }
223 if d > 31 { return -1 }
224 if buf[12] != 0x5A { return -1 } // 'Z' required
225 let days: i64 = days_from_civil(y, mo, d)
226 return days * K_MAGIC_86400 + h * K_MAGIC_3600 + mi * 60 + s
227}
228
229// Parse a GeneralizedTime (15 chars, YYYYMMDDhhmmssZ).
230func parse_gentime(buf: *u8) -> i64 {
231 let y: i64 = parse_4digit(buf, 0)
232 let mo: i64 = parse_2digit(buf, 4)
233 let d: i64 = parse_2digit(buf, 6)
234 let h: i64 = parse_2digit(buf, 8)
235 let mi: i64 = parse_2digit(buf, 10)
236 let s: i64 = parse_2digit(buf, 12)
237 if y < 0 { return -1 }
238 if mo < 1 { return -1 }
239 if mo > 12 { return -1 }
240 if buf[14] != 0x5A { return -1 }
241 let days: i64 = days_from_civil(y, mo, d)
242 return days * K_MAGIC_86400 + h * K_MAGIC_3600 + mi * 60 + s
243}
244
245// Compile-only smoke. Epoch 0 (1970-01-01T00:00:00Z) should
246// format to exactly that string.
247func main() -> i64 {
248 let out: *u8 = sys_mmap(32)
249 format_rfc3339(0, out)
250 // Check a few expected byte values.
251 if out[0] != 0x31 { return 1 } // '1'
252 if out[3] != 0x30 { return 2 } // '0' (K_MAGIC_1970)
253 if out[10] != 0x54 { return 3 } // 'T'
254 if out[19] != 0x5A { return 4 } // 'Z'
255
256 // Round-trip: format + parse a known date.
257 // 1234567890 seconds = 2009-02-13T23:31:30Z.
258 format_rfc3339(K_MAGIC_1234567890, out)
259 if out[0] != 0x32 { return 5 } // '2'
260 if out[3] != 0x39 { return 6 } // '9'
261 return 0
262}