//! HTTP-date parsing and formatting used by archive conditional requests. //! Go's net/http accepts IMF-fixdate plus the obsolete RFC850 and asctime //! forms, so this module deliberately implements the same three shapes. const std = @import("std"); const weekdays = [_][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; const months = [_][]const u8{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; pub fn format(buf: *[40]u8, unix_seconds: i64) []const u8 { const days = @divFloor(unix_seconds, std.time.s_per_day); const day_seconds: u32 = @intCast(@mod(unix_seconds, std.time.s_per_day)); const civil = civilFromDays(days); const weekday: usize = @intCast(@mod(days + 4, 7)); return std.fmt.bufPrint(buf, "{s}, {d:0>2} {s} {d:0>4} {d:0>2}:{d:0>2}:{d:0>2} GMT", .{ weekdays[weekday], @as(u8, @intCast(civil.day)), months[civil.month - 1], @as(u32, @intCast(civil.year)), day_seconds / 3600, day_seconds / 60 % 60, day_seconds % 60, }) catch unreachable; } pub fn parse(raw: []const u8) ?i64 { if (parseImf(raw)) |value| return value; if (parseRfc850(raw)) |value| return value; return parseAsctime(raw); } fn parseImf(raw: []const u8) ?i64 { if (raw.len < 29 or raw[3] != ',' or raw[4] != ' ' or raw[7] != ' ' or raw[11] != ' ' or raw[16] != ' ' or raw[19] != ':' or raw[22] != ':' or !std.mem.eql(u8, afterFraction(raw[25..]) orelse return null, " GMT") or weekdayShort(raw[0..3]) == null) return null; return timestamp( parseDigits(raw[12..16]) orelse return null, monthNumber(raw[8..11]) orelse return null, parseDigits(raw[5..7]) orelse return null, parseDigits(raw[17..19]) orelse return null, parseDigits(raw[20..22]) orelse return null, parseDigits(raw[23..25]) orelse return null, ); } fn parseRfc850(raw: []const u8) ?i64 { const comma = std.mem.indexOfScalar(u8, raw, ',') orelse return null; if (weekdayLong(raw[0..comma]) == null) return null; const tail = raw[comma + 1 ..]; if (tail.len < 23 or tail[0] != ' ' or tail[3] != '-' or tail[7] != '-' or tail[10] != ' ' or tail[13] != ':' or tail[16] != ':' or !std.mem.eql(u8, afterFraction(tail[19..]) orelse return null, " GMT")) return null; const short_year = parseDigits(tail[8..10]) orelse return null; const year: u32 = if (short_year >= 69) 1900 + short_year else 2000 + short_year; return timestamp( year, monthNumber(tail[4..7]) orelse return null, parseDigits(tail[1..3]) orelse return null, parseDigits(tail[11..13]) orelse return null, parseDigits(tail[14..16]) orelse return null, parseDigits(tail[17..19]) orelse return null, ); } fn parseAsctime(raw: []const u8) ?i64 { if (raw.len < 24 or raw[3] != ' ' or raw[7] != ' ' or raw[10] != ' ' or raw[13] != ':' or raw[16] != ':' or weekdayShort(raw[0..3]) == null) return null; const year_suffix = afterFraction(raw[19..]) orelse return null; if (year_suffix.len != 5 or year_suffix[0] != ' ') return null; const day: u32 = if (raw[8] == ' ') parseDigits(raw[9..10]) orelse return null else parseDigits(raw[8..10]) orelse return null; return timestamp( parseDigits(year_suffix[1..]) orelse return null, monthNumber(raw[4..7]) orelse return null, day, parseDigits(raw[11..13]) orelse return null, parseDigits(raw[14..16]) orelse return null, parseDigits(raw[17..19]) orelse return null, ); } /// time.Parse accepts a fractional second even when the selected layout does /// not contain one. http.ParseTime inherits that behavior for all three date /// layouts; consume one to nine digits but leave the integer Unix second. fn afterFraction(raw: []const u8) ?[]const u8 { if (raw.len == 0 or (raw[0] != '.' and raw[0] != ',')) return raw; var i: usize = 1; while (i < raw.len and raw[i] >= '0' and raw[i] <= '9' and i <= 9) : (i += 1) {} if (i == 1) return null; return raw[i..]; } fn timestamp(year: u32, month: u32, day: u32, hour: u32, minute: u32, second: u32) ?i64 { if (hour > 23 or minute > 59 or second > 59) return null; const days = daysFromCivil(@intCast(year), @intCast(month), @intCast(day)) orelse return null; return days * std.time.s_per_day + @as(i64, hour) * 3600 + @as(i64, minute) * 60 + second; } fn parseDigits(raw: []const u8) ?u32 { if (raw.len == 0) return null; var value: u32 = 0; for (raw) |c| { if (c < '0' or c > '9') return null; value = value * 10 + c - '0'; } return value; } fn monthNumber(raw: []const u8) ?u32 { for (months, 1..) |month, i| if (std.mem.eql(u8, raw, month)) return @intCast(i); return null; } fn weekdayShort(raw: []const u8) ?usize { for (weekdays, 0..) |weekday, i| if (std.mem.eql(u8, raw, weekday)) return i; return null; } fn weekdayLong(raw: []const u8) ?usize { const long = [_][]const u8{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; for (long, 0..) |weekday, i| if (std.mem.eql(u8, raw, weekday)) return i; return null; } fn leap(year: i64) bool { return @mod(year, 4) == 0 and (@mod(year, 100) != 0 or @mod(year, 400) == 0); } fn daysFromCivil(year_raw: i64, month: u8, day: u8) ?i64 { if (month < 1 or month > 12 or day < 1) return null; const lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; const limit: u8 = lengths[month - 1] + @as(u8, if (month == 2 and leap(year_raw)) 1 else 0); if (day > limit) return null; const year = year_raw - @intFromBool(month <= 2); const era = @divFloor(year, 400); const yoe = year - era * 400; const shifted_month: i64 = @as(i64, month) + @as(i64, if (month > 2) -3 else 9); const doy = @divFloor(153 * shifted_month + 2, 5) + day - 1; const doe = yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy; return era * 146097 + doe - 719468; } const Civil = struct { year: i64, month: usize, day: i64 }; fn civilFromDays(days: i64) Civil { const z = days + 719468; const era = @divFloor(z, 146097); const doe = z - era * 146097; const yoe = @divFloor(doe - @divFloor(doe, 1460) + @divFloor(doe, 36524) - @divFloor(doe, 146096), 365); var year = yoe + era * 400; const doy = doe - (365 * yoe + @divFloor(yoe, 4) - @divFloor(yoe, 100)); const mp = @divFloor(5 * doy + 2, 153); const day = doy - @divFloor(153 * mp + 2, 5) + 1; const month: usize = @intCast(mp + @as(i64, if (mp < 10) 3 else -9)); year += @intFromBool(month <= 2); return .{ .year = year, .month = month, .day = day }; } test "formats and parses Go HTTP date forms" { var buf: [40]u8 = undefined; try std.testing.expectEqualStrings("Mon, 02 Jan 2006 15:04:05 GMT", format(&buf, 1_136_214_245)); try std.testing.expectEqual(@as(?i64, 1_136_214_245), parse("Mon, 02 Jan 2006 15:04:05 GMT")); try std.testing.expectEqual(@as(?i64, 1_136_214_245), parse("Monday, 02-Jan-06 15:04:05 GMT")); try std.testing.expectEqual(@as(?i64, 1_136_214_245), parse("Mon Jan 2 15:04:05 2006")); try std.testing.expectEqual(@as(?i64, 0), parse("Thu, 01 Jan 1970 00:00:00 GMT")); try std.testing.expectEqual(@as(?i64, -1), parse("Wed, 31 Dec 1969 23:59:59 GMT")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sun, 06 Nov 1994 08:49:37 GMT")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sunday, 06-Nov-94 08:49:37 GMT")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sun Nov 6 08:49:37 1994")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sun, 06 Nov 1994 08:49:37.123456789 GMT")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sunday, 06-Nov-94 08:49:37,5 GMT")); try std.testing.expectEqual(@as(?i64, 784_111_777), parse("Sun Nov 6 08:49:37.01 1994")); try std.testing.expect(parse("Mon, 31 Feb 2006 15:04:05 GMT") == null); try std.testing.expect(parse("Mon, 02 Jan 2006 15:04:60 GMT") == null); }