Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
121 changes: 121 additions & 0 deletions bytesconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,130 @@ 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 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, time.UTC)
// 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 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
switch a {
case 'j':
if b == 'a' && c == 'n' {
return time.January, true
}
if b == 'u' && c == 'n' {
return time.June, true
}
if b == 'u' && c == 'l' {
return time.July, true
}
case 'f':
if b == 'e' && c == 'b' {
return time.February, true
}
case 'm':
if b == 'a' && c == 'r' {
return time.March, true
}
if b == 'a' && c == 'y' {
return time.May, true
}
case 'a':
if b == 'p' && c == 'r' {
return time.April, true
}
if b == 'u' && c == 'g' {
return time.August, true
}
case 's':
if b == 'e' && c == 'p' {
return time.September, true
}
case 'o':
if b == 'c' && c == 't' {
return time.October, true
}
case 'n':
if b == 'o' && c == 'v' {
return time.November, true
}
case 'd':
if b == 'e' && c == '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
41 changes: 41 additions & 0 deletions bytesconv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,47 @@ func TestAppendHTTPDate(t *testing.T) {
}
}

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

valid := []string{
"Tue, 10 Nov 2009 23:00:00 GMT",
"Mon, 29 Feb 2016 12:34:56 GMT", // leap year
"Fri, 31 Dec 1999 23:59:59 GMT",
"Tue, 10 Nov 2009 23:00:00 UTC", // fallback path should match time.Parse behavior
}
for _, s := range valid {
got, err := ParseHTTPDate([]byte(s))
if err != nil {
t.Fatalf("unexpected error parsing %q: %v", s, err)
}
want, err := time.Parse(time.RFC1123, s)
if err != nil {
t.Fatalf("unexpected reference parse error for %q: %v", s, err)
}
if !got.Equal(want) {
t.Fatalf("unexpected parsed time for %q: got=%v want=%v", s, got, want)
}
}
}

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

invalid := []string{
"Tue, 31 Feb 2009 23:00:00 GMT", // invalid day-of-month
"Tue, 10 Foo 2009 23:00:00 GMT", // invalid month
"Tue 10 Nov 2009 23:00:00 GMT", // missing comma separator
"Tue, 10 Nov 2009 23-00-00 GMT", // invalid time separators
"Tue, 29 Feb 2019 23:00:00 GMT", // non-leap-year date
}
for _, s := range invalid {
if _, err := ParseHTTPDate([]byte(s)); err == nil {
t.Fatalf("expected error for invalid http date %q", s)
}
}
}

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

Expand Down
30 changes: 30 additions & 0 deletions bytesconv_timing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"html"
"net"
"testing"
"time"

"github.com/valyala/bytebufferpool"
)
Expand Down Expand Up @@ -187,3 +188,32 @@ func BenchmarkParseUfloat(b *testing.B) {
}
})
}

func BenchmarkAppendHTTPDate(b *testing.B) {
ts := time.Date(2026, time.February, 6, 22, 0, 0, 0, time.UTC)
want := "Fri, 06 Feb 2026 22:00:00 GMT"
b.RunParallel(func(pb *testing.PB) {
var buf []byte
for pb.Next() {
buf = AppendHTTPDate(buf[:0], ts)
if string(buf) != want {
b.Fatalf("unexpected date: %q. Expecting %q", buf, want)
}
}
})
}

func BenchmarkAppendHTTPDateLocal(b *testing.B) {
loc := time.FixedZone("UTC+3", 3*60*60)
ts := time.Date(2026, time.February, 7, 1, 0, 0, 0, loc)
want := "Fri, 06 Feb 2026 22:00:00 GMT"
b.RunParallel(func(pb *testing.PB) {
var buf []byte
for pb.Next() {
buf = AppendHTTPDate(buf[:0], ts)
if string(buf) != want {
b.Fatalf("unexpected date: %q. Expecting %q", buf, want)
}
}
})
}
Loading