diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index f552470..9c5ca5b 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ 1.22.4 ] + go-version: [ 1.22.5 ] steps: - name: Check out code into the Go module directory uses: actions/checkout@v3 diff --git a/xtime/time.go b/xtime/time.go new file mode 100644 index 0000000..3dfbf0a --- /dev/null +++ b/xtime/time.go @@ -0,0 +1,55 @@ +// Copyright (c) 2015-2024 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package xtime + +import ( + "time" +) + +// Additional durations, a day is considered to be 24 hours +const ( + Day time.Duration = time.Hour * 24 + Week = Day * 7 +) + +var unitMap = map[string]int64{ + "ns": int64(time.Nanosecond), + "us": int64(time.Microsecond), + "µs": int64(time.Microsecond), // U+00B5 = micro symbol + "μs": int64(time.Microsecond), // U+03BC = Greek letter mu + "ms": int64(time.Millisecond), + "s": int64(time.Second), + "m": int64(time.Minute), + "h": int64(time.Hour), + "d": int64(Day), + "w": int64(Week), +} + +// ParseDuration parses a duration string. +// The following code is borrowed from time.ParseDuration +// https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/time/format.go;l=1589 +// This function extends this function by allowing support for days and weeks. +// This function must only be used when days and weeks are necessary inputs +// in all other cases it is preferred that a user uses Go's time.ParseDuration +func ParseDuration(s string) (time.Duration, error) { + dur, err := time.ParseDuration(s) // Parse via standard Go, if success return right away. + if err == nil { + return dur, nil + } + return parseDuration(s) +} diff --git a/xtime/time_contrib.go b/xtime/time_contrib.go new file mode 100644 index 0000000..bbf6e45 --- /dev/null +++ b/xtime/time_contrib.go @@ -0,0 +1,165 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the go.dev/LICENSE file. + +package xtime + +import ( + "errors" + "strconv" + "time" +) + +// function borrowed from https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/time/format.go;l=1589 +// supports days and weeks such as '1d1ms', '1w1ms' +func parseDuration(s string) (time.Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d int64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + for s != "" { + var ( + v, f int64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + if i == 0 { + return 0, errors.New("missing unit in duration " + strconv.Quote(orig)) + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("unknown unit " + strconv.Quote(u) + " in duration " + strconv.Quote(orig)) + } + if v > (1<<63-1)/unit { + // overflow + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + v *= unit + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += int64(float64(f) * (float64(unit) / scale)) + if v < 0 { + // overflow + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + } + d += v + if d < 0 { + // overflow + return 0, errors.New("invalid duration " + strconv.Quote(orig)) + } + } + + if neg { + d = -d + } + return time.Duration(d), nil +} + +var errLeadingInt = errors.New("bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt(s string) (x int64, rem string, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > (1<<63-1)/10 { + // overflow + return 0, "", errLeadingInt + } + x = x*10 + int64(c) - '0' + if x < 0 { + // overflow + return 0, "", errLeadingInt + } + } + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x int64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + int64(c) - '0' + if y < 0 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} diff --git a/xtime/time_test.go b/xtime/time_test.go new file mode 100644 index 0000000..04878de --- /dev/null +++ b/xtime/time_test.go @@ -0,0 +1,136 @@ +// Copyright (c) 2015-2024 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package xtime + +import ( + "fmt" + "strings" + "testing" + "time" +) + +var parseDurationTests = []struct { + in string + want time.Duration +}{ + // simple + {"0", 0}, + {"5s", 5 * time.Second}, + {"30s", 30 * time.Second}, + {"1478s", 1478 * time.Second}, + // sign + {"-5s", -5 * time.Second}, + {"+5s", 5 * time.Second}, + {"-0", 0}, + {"+0", 0}, + // decimal + {"5.0s", 5 * time.Second}, + {"5.6s", 5*time.Second + 600*time.Millisecond}, + {"5.s", 5 * time.Second}, + {".5s", 500 * time.Millisecond}, + {"1.0s", 1 * time.Second}, + {"1.00s", 1 * time.Second}, + {"1.004s", 1*time.Second + 4*time.Millisecond}, + {"1.0040s", 1*time.Second + 4*time.Millisecond}, + {"100.00100s", 100*time.Second + 1*time.Millisecond}, + // different units + {"10ns", 10 * time.Nanosecond}, + {"11us", 11 * time.Microsecond}, + {"12µs", 12 * time.Microsecond}, // U+00B5 + {"12μs", 12 * time.Microsecond}, // U+03BC + {"13ms", 13 * time.Millisecond}, + {"14s", 14 * time.Second}, + {"15m", 15 * time.Minute}, + {"16h", 16 * time.Hour}, + // composite durations + {"3h30m", 3*time.Hour + 30*time.Minute}, + {"10.5s4m", 4*time.Minute + 10*time.Second + 500*time.Millisecond}, + {"-2m3.4s", -(2*time.Minute + 3*time.Second + 400*time.Millisecond)}, + {"1h2m3s4ms5us6ns", 1*time.Hour + 2*time.Minute + 3*time.Second + 4*time.Millisecond + 5*time.Microsecond + 6*time.Nanosecond}, + {"39h9m14.425s", 39*time.Hour + 9*time.Minute + 14*time.Second + 425*time.Millisecond}, + // large value + {"52763797000ns", 52763797000 * time.Nanosecond}, + // more than 9 digits after decimal point, see https://golang.org/issue/6617 + {"0.3333333333333333333h", 20 * time.Minute}, + // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 + {"9007199254740993ns", (1<<53 + 1) * time.Nanosecond}, + // largest duration that can be represented by int64 in nanoseconds + {"9223372036854775807ns", (1<<63 - 1) * time.Nanosecond}, + {"9223372036854775.807us", (1<<63 - 1) * time.Nanosecond}, + {"9223372036s854ms775us807ns", (1<<63 - 1) * time.Nanosecond}, + {"-9223372036854775808ns", -1 << 63 * time.Nanosecond}, + {"-9223372036854775.808us", -1 << 63 * time.Nanosecond}, + {"-9223372036s854ms775us808ns", -1 << 63 * time.Nanosecond}, + // largest negative value + {"-9223372036854775808ns", -1 << 63 * time.Nanosecond}, + // largest negative round trip value, see https://golang.org/issue/48629 + {"-2562047h47m16.854775808s", -1 << 63 * time.Nanosecond}, + // huge string; issue 15011. + {"0.100000000000000000000h", 6 * time.Minute}, + // This value tests the first overflow check in leadingFraction. + {"0.830103483285477580700h", 49*time.Minute + 48*time.Second + 372539827*time.Nanosecond}, +} + +func TestParseDuration(t *testing.T) { + for _, tc := range parseDurationTests { + d, err := ParseDuration(tc.in) + if err != nil || d != tc.want { + t.Errorf("ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) + } + } +} + +var parseDurationErrorTests = []struct { + in string + expect string +}{ + // invalid + {"", `""`}, + {"3", `"3"`}, + {"-", `"-"`}, + {"s", `"s"`}, + {".", `"."`}, + {"-.", `"-."`}, + {".s", `".s"`}, + {"+.s", `"+.s"`}, + {"\x85\x85", `"\x85\x85"`}, + {"\xffff", `"\xffff"`}, + {"hello \xffff world", `"hello \xffff world"`}, + {"\uFFFD", `"�"`}, // utf8.RuneError + {"\uFFFD hello \uFFFD world", `"� hello � world"`}, // utf8.RuneError + // overflow + {"9223372036854775810ns", `"9223372036854775810ns"`}, + {"9223372036854775808ns", `"9223372036854775808ns"`}, + {"-9223372036854775809ns", `"-9223372036854775809ns"`}, + {"9223372036854776us", `"9223372036854776us"`}, + {"3000000h", `"3000000h"`}, + {"9223372036854775.808us", `"9223372036854775.808us"`}, + {"9223372036854ms775us808ns", `"9223372036854ms775us808ns"`}, +} + +func TestParseDurationErrors(t *testing.T) { + for _, tc := range parseDurationErrorTests { + _, err := ParseDuration(tc.in) + if err == nil { + t.Errorf("ParseDuration(%q) = _, nil, want _, non-nil", tc.in) + } else if !strings.Contains(err.Error(), tc.expect) { + fmt.Println(err) + t.Errorf("ParseDuration(%q) = _, %q, error does not contain %q", tc.in, err, tc.expect) + } + } +}