code wiki / (root) / nx_timefmt.nx

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}