timefmt.nx source
↩ module page · 240 lines · 8398 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
34import "syscalls.nx"
35
36// ---- Gregorian calendar math -------------------------------------
37//
38// Epoch seconds (Unix) -> broken-down UTC time. Algorithm from
39// Howard Hinnant's "date" library (2013), public domain, verified
40// against Zeller's congruence.
41
42// Is `y` a leap year in the proleptic Gregorian calendar?
43func is_leap(y: i64) -> i64 {
44 if (y % 4) != 0 { return 0 }
45 if (y % 100) != 0 { return 1 }
46 if (y % 400) != 0 { return 0 }
47 return 1
48}
49
50// Days from civil date (Hinnant 2013, algorithm "days_from_civil").
51// Returns days since 1970-01-01 (Unix epoch).
52func days_from_civil(y: i64, m: i64, d: i64) -> i64 {
53 var year: i64 = y
54 if m <= 2 { year = year - 1 }
55 let era: i64 = year / 400
56 let yoe: i64 = year - era * 400
57 var doy: i64 = 0
58 if m > 2 { doy = (153 * (m - 3) + 2) / 5 + d - 1 }
59 else { doy = (153 * (m + 9) + 2) / 5 + d - 1 }
60 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy
61 return era * 146097 + doe - 719468
62}
63
64// Inverse: days since epoch -> (y, m, d) via three i64 output slots.
65func civil_from_days(days: i64, y_out: *i64, m_out: *i64, d_out: *i64) -> i64 {
66 let z: i64 = days + 719468
67 var era: i64 = z / 146097
68 if z < 0 {
69 if (z % 146097) != 0 { era = era - 1 }
70 }
71 let doe: i64 = z - era * 146097
72 let yoe: i64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
73 let year: i64 = yoe + era * 400
74 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
75 let mp: i64 = (5 * doy + 2) / 153
76 let d: i64 = doy - (153 * mp + 2) / 5 + 1
77 var m: i64 = mp + 3
78 var y: i64 = year
79 if mp >= 10 { m = mp - 9; y = year + 1 }
80 *y_out = y
81 *m_out = m
82 *d_out = d
83 return 0
84}
85
86// Break down epoch-seconds into (y, m, d, h, mi, s). All outputs
87// are caller-mmapped i64 slots.
88func break_down_utc(epoch_secs: i64,
89 y: *i64, mo: *i64, d: *i64,
90 h: *i64, mi: *i64, s: *i64) -> i64 {
91 let secs_per_day: i64 = 86400
92 var days: i64 = epoch_secs / secs_per_day
93 var secs_of_day: i64 = epoch_secs - days * secs_per_day
94 // Correct floor-div behaviour for negative seconds (pre-1970):
95 if secs_of_day < 0 {
96 secs_of_day = secs_of_day + secs_per_day
97 days = days - 1
98 }
99 civil_from_days(days, y, mo, d)
100 *h = secs_of_day / 3600
101 let rem: i64 = secs_of_day - *h * 3600
102 *mi = rem / 60
103 *s = rem - *mi * 60
104 return 0
105}
106
107// ---- formatting --------------------------------------------------
108
109// Write `n` as a zero-padded decimal of exactly `width` digits
110// into out[pos..pos+width]. Width must be 2..4; overflow (n has
111// more digits than width) clamps at width.
112func emit_zpad(out: *u8, pos: i64, n: i64, width: i64) -> i64 {
113 var rem: i64 = n
114 var i: i64 = width - 1
115 while i >= 0 {
116 out[pos + i] = 0x30 + (rem % 10)
117 rem = rem / 10
118 i = i - 1
119 }
120 return width
121}
122
123// Write epoch_secs as an RFC 3339 timestamp "YYYY-MM-DDTHH:MM:SSZ"
124// (20 chars) to out. Caller must provide >= 20 bytes of space.
125func format_rfc3339(epoch_secs: i64, out: *u8) -> i64 {
126 let y_raw: *u8 = sys_mmap(16)
127 let m_raw: *u8 = sys_mmap(16)
128 let d_raw: *u8 = sys_mmap(16)
129 let h_raw: *u8 = sys_mmap(16)
130 let i_raw: *u8 = sys_mmap(16)
131 let s_raw: *u8 = sys_mmap(16)
132 let y: *i64 = y_raw as *i64
133 let m: *i64 = m_raw as *i64
134 let d: *i64 = d_raw as *i64
135 let h: *i64 = h_raw as *i64
136 let mi: *i64 = i_raw as *i64
137 let s: *i64 = s_raw as *i64
138 break_down_utc(epoch_secs, y, m, d, h, mi, s)
139
140 // Clamp year per T2.
141 var year: i64 = *y
142 if year < 0 { year = 0 }
143 if year > 9999 { year = 9999 }
144
145 emit_zpad(out, 0, year, 4)
146 out[4] = 0x2D // '-'
147 emit_zpad(out, 5, *m, 2)
148 out[7] = 0x2D
149 emit_zpad(out, 8, *d, 2)
150 out[10] = 0x54 // 'T'
151 emit_zpad(out, 11, *h, 2)
152 out[13] = 0x3A // ':'
153 emit_zpad(out, 14, *mi, 2)
154 out[16] = 0x3A
155 emit_zpad(out, 17, *s, 2)
156 out[19] = 0x5A // 'Z'
157 return 20
158}
159
160// ---- X.509 timestamp parse ---------------------------------------
161//
162// X.509 cert validity dates come in two forms:
163// UTCTime "YYMMDDhhmmssZ" (13 chars, years 1950-2049)
164// GeneralizedTime "YYYYMMDDhhmmssZ" (15 chars, any year)
165//
166// Parse either into epoch-seconds. Returns -1 on malformed input.
167
168func parse_2digit(buf: *u8, off: i64) -> i64 {
169 let a: i64 = buf[off] - 0x30
170 let b: i64 = buf[off + 1] - 0x30
171 if a < 0 { return -1 }
172 if a > 9 { return -1 }
173 if b < 0 { return -1 }
174 if b > 9 { return -1 }
175 return a * 10 + b
176}
177
178func parse_4digit(buf: *u8, off: i64) -> i64 {
179 let a: i64 = parse_2digit(buf, off)
180 if a < 0 { return -1 }
181 let b: i64 = parse_2digit(buf, off + 2)
182 if b < 0 { return -1 }
183 return a * 100 + b
184}
185
186// Parse a UTCTime (13 chars). Years 0..49 map to 20YY; 50..99
187// map to 19YY (per RFC 5280 §4.1.2.5.1).
188func parse_utctime(buf: *u8) -> i64 {
189 let yy: i64 = parse_2digit(buf, 0)
190 if yy < 0 { return -1 }
191 var y: i64 = 1900 + yy
192 if yy < 50 { y = 2000 + yy }
193 let mo: i64 = parse_2digit(buf, 2)
194 let d: i64 = parse_2digit(buf, 4)
195 let h: i64 = parse_2digit(buf, 6)
196 let mi: i64 = parse_2digit(buf, 8)
197 let s: i64 = parse_2digit(buf, 10)
198 if mo < 1 { return -1 }
199 if mo > 12 { return -1 }
200 if d < 1 { return -1 }
201 if d > 31 { return -1 }
202 if buf[12] != 0x5A { return -1 } // 'Z' required
203 let days: i64 = days_from_civil(y, mo, d)
204 return days * 86400 + h * 3600 + mi * 60 + s
205}
206
207// Parse a GeneralizedTime (15 chars, YYYYMMDDhhmmssZ).
208func parse_gentime(buf: *u8) -> i64 {
209 let y: i64 = parse_4digit(buf, 0)
210 let mo: i64 = parse_2digit(buf, 4)
211 let d: i64 = parse_2digit(buf, 6)
212 let h: i64 = parse_2digit(buf, 8)
213 let mi: i64 = parse_2digit(buf, 10)
214 let s: i64 = parse_2digit(buf, 12)
215 if y < 0 { return -1 }
216 if mo < 1 { return -1 }
217 if mo > 12 { return -1 }
218 if buf[14] != 0x5A { return -1 }
219 let days: i64 = days_from_civil(y, mo, d)
220 return days * 86400 + h * 3600 + mi * 60 + s
221}
222
223// Compile-only smoke. Epoch 0 (1970-01-01T00:00:00Z) should
224// format to exactly that string.
225func main() -> i64 {
226 let out: *u8 = sys_mmap(32)
227 format_rfc3339(0, out)
228 // Check a few expected byte values.
229 if out[0] != 0x31 { return 1 } // '1'
230 if out[3] != 0x30 { return 2 } // '0' (1970)
231 if out[10] != 0x54 { return 3 } // 'T'
232 if out[19] != 0x5A { return 4 } // 'Z'
233
234 // Round-trip: format + parse a known date.
235 // 1234567890 seconds = 2009-02-13T23:31:30Z.
236 format_rfc3339(1234567890, out)
237 if out[0] != 0x32 { return 5 } // '2'
238 if out[3] != 0x39 { return 6 } // '9'
239 return 0
240}