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)
+ }
+ }
+}