code wiki / (root) / timefmt.nx

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}