Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.20.x]
go-version: [1.24.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
env:
Expand Down
126 changes: 126 additions & 0 deletions bytesconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func AppendIPv4(dst []byte, ip net.IP) []byte {

var errEmptyIPStr = errors.New("empty ip address string")

var httpDateGMT = time.FixedZone("GMT", 0)

// ParseIPv4 parses ip address from ipStr into dst and returns the extended dst.
func ParseIPv4(dst net.IP, ipStr []byte) (net.IP, error) {
if len(ipStr) == 0 {
Expand Down Expand Up @@ -117,9 +119,133 @@ func AppendHTTPDate(dst []byte, date time.Time) []byte {

// ParseHTTPDate parses HTTP-compliant (RFC1123) date.
func ParseHTTPDate(date []byte) (time.Time, error) {
if t, ok := parseRFC1123DateGMT(date); ok {
return t, nil
}
return time.Parse(time.RFC1123, b2s(date))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to http.TimeFormat.

net/http also only accepts GMT as timezone.

}

func parseRFC1123DateGMT(b []byte) (time.Time, bool) {
// Expects "Mon, 02 Jan 2006 15:04:05 GMT".
if len(b) != 29 {
return time.Time{}, false
}
if !isWeekday3(b[0], b[1], b[2]) {
return time.Time{}, false
}
if b[3] != ',' || b[4] != ' ' || b[7] != ' ' || b[11] != ' ' ||
b[16] != ' ' || b[19] != ':' || b[22] != ':' || b[25] != ' ' {
return time.Time{}, false
}
if (b[26]|0x20) != 'g' || (b[27]|0x20) != 'm' || (b[28]|0x20) != 't' {
return time.Time{}, false
}

day, ok := parse2Digits(b[5], b[6])
if !ok || day < 1 || day > 31 {
return time.Time{}, false
}
month, ok := parseMonth3(b[8], b[9], b[10])
if !ok {
return time.Time{}, false
}
year, ok := parse4Digits(b[12], b[13], b[14], b[15])
if !ok {
return time.Time{}, false
}
hour, ok := parse2Digits(b[17], b[18])
if !ok || hour > 23 {
return time.Time{}, false
}
minute, ok := parse2Digits(b[20], b[21])
if !ok || minute > 59 {
return time.Time{}, false
}
second, ok := parse2Digits(b[23], b[24])
if !ok || second > 59 {
return time.Time{}, false
}

t := time.Date(year, month, day, hour, minute, second, 0, httpDateGMT)
// Reject calendar-invalid dates like "31 Feb", which time.Date normalizes.
if t.Year() != year || t.Month() != month || t.Day() != day {
return time.Time{}, false
}
return t, true
}

func isWeekday3(a, b, c byte) bool {
a |= 0x20
b |= 0x20
c |= 0x20
k := uint32(a)<<16 | uint32(b)<<8 | uint32(c)
switch k {
case uint32('m')<<16 | uint32('o')<<8 | uint32('n'),
uint32('t')<<16 | uint32('u')<<8 | uint32('e'),
uint32('w')<<16 | uint32('e')<<8 | uint32('d'),
uint32('t')<<16 | uint32('h')<<8 | uint32('u'),
uint32('f')<<16 | uint32('r')<<8 | uint32('i'),
uint32('s')<<16 | uint32('a')<<8 | uint32('t'),
uint32('s')<<16 | uint32('u')<<8 | uint32('n'):
return true
default:
return false
}
}

func parse2Digits(a, b byte) (int, bool) {
if a < '0' || a > '9' || b < '0' || b > '9' {
return 0, false
}
return int(a-'0')*10 + int(b-'0'), true
}

func parse4Digits(a, b, c, d byte) (int, bool) {
v1, ok := parse2Digits(a, b)
if !ok {
return 0, false
}
v2, ok := parse2Digits(c, d)
if !ok {
return 0, false
}
return v1*100 + v2, true
}

func parseMonth3(a, b, c byte) (time.Month, bool) {
a |= 0x20
b |= 0x20
c |= 0x20
k := uint32(a)<<16 | uint32(b)<<8 | uint32(c)
switch k {
case uint32('j')<<16 | uint32('a')<<8 | uint32('n'):
return time.January, true
case uint32('f')<<16 | uint32('e')<<8 | uint32('b'):
return time.February, true
case uint32('m')<<16 | uint32('a')<<8 | uint32('r'):
return time.March, true
case uint32('a')<<16 | uint32('p')<<8 | uint32('r'):
return time.April, true
case uint32('m')<<16 | uint32('a')<<8 | uint32('y'):
return time.May, true
case uint32('j')<<16 | uint32('u')<<8 | uint32('n'):
return time.June, true
case uint32('j')<<16 | uint32('u')<<8 | uint32('l'):
return time.July, true
case uint32('a')<<16 | uint32('u')<<8 | uint32('g'):
return time.August, true
case uint32('s')<<16 | uint32('e')<<8 | uint32('p'):
return time.September, true
case uint32('o')<<16 | uint32('c')<<8 | uint32('t'):
return time.October, true
case uint32('n')<<16 | uint32('o')<<8 | uint32('v'):
return time.November, true
case uint32('d')<<16 | uint32('e')<<8 | uint32('c'):
return time.December, true
}
return 0, false
}

// AppendUint appends n to dst and returns the extended dst.
func AppendUint(dst []byte, n int) []byte {
if n < 0 {
Expand Down
53 changes: 53 additions & 0 deletions bytesconv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,59 @@ func TestAppendHTTPDate(t *testing.T) {
}
}

func TestParseHTTPDateCompatibility(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
value string
hasError bool
roundTrip bool
}{
{name: "gmt-fast-path", value: "Tue, 10 Nov 2009 23:00:00 GMT", roundTrip: true},
{name: "epoch", value: "Thu, 01 Jan 1970 00:00:00 GMT", roundTrip: true},
{name: "year-boundary", value: "Fri, 31 Dec 1999 23:59:59 GMT", roundTrip: true},
{name: "leap-year", value: "Mon, 29 Feb 2016 12:34:56 GMT", roundTrip: true},
{name: "utc-fallback", value: "Tue, 10 Nov 2009 23:00:00 UTC"},
{name: "mixedcase-weekday-month", value: "tUe, 10 nOv 2009 23:00:00 GMT"},
{name: "day-zero", value: "Tue, 00 Nov 2009 23:00:00 GMT", hasError: true},
{name: "invalid-day", value: "Tue, 31 Feb 2009 23:00:00 GMT", hasError: true},
{name: "invalid-weekday", value: "Xxx, 10 Nov 2009 23:00:00 GMT", hasError: true},
{name: "invalid-month", value: "Tue, 10 Foo 2009 23:00:00 GMT", hasError: true},
{name: "invalid-hour", value: "Tue, 10 Nov 2009 24:00:00 GMT", hasError: true},
{name: "invalid-minute", value: "Tue, 10 Nov 2009 23:60:00 GMT", hasError: true},
{name: "invalid-second", value: "Tue, 10 Nov 2009 23:00:60 GMT", hasError: true},
{name: "invalid-separator", value: "Tue 10 Nov 2009 23:00:00 GMT", hasError: true},
{name: "invalid-time-separator", value: "Tue, 10 Nov 2009 23-00-00 GMT", hasError: true},
{name: "non-leap-year", value: "Tue, 29 Feb 2019 23:00:00 GMT", hasError: true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got, gotErr := ParseHTTPDate([]byte(tc.value))
want, wantErr := time.ParseInLocation(time.RFC1123, tc.value, time.UTC)

if (gotErr != nil) != (wantErr != nil) {
t.Fatalf("error mismatch for %q: ParseHTTPDate err=%v, ParseInLocation err=%v", tc.value, gotErr, wantErr)
}
if tc.hasError != (gotErr != nil) {
t.Fatalf("unexpected error state for %q: gotErr=%v, expectedError=%v", tc.value, gotErr, tc.hasError)
}
if gotErr != nil {
return
}
if !got.Equal(want) {
t.Fatalf("parsed time mismatch for %q: got=%v want=%v", tc.value, got, want)
}
if tc.roundTrip && got.Format(time.RFC1123) != tc.value {
t.Fatalf("unexpected formatted date %q. Expecting %q", got.Format(time.RFC1123), tc.value)
}
})
}
}

func TestParseUintError(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading