You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
6.8 KiB
358 lines
6.8 KiB
4 weeks ago
|
package bytesize
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
)
|
||
|
|
||
|
type Size int64
|
||
|
|
||
|
// Byte size prefix.
|
||
|
const (
|
||
|
Byte Size = 1 // byte
|
||
|
Kibibyte Size = 1 << (10 * iota) // kibibyte
|
||
|
Mebibyte // mebibyte
|
||
|
Gibibyte // gibibyte
|
||
|
Tebibyte // tebibyte
|
||
|
Pebibyte // pebibyte
|
||
|
|
||
|
Kilobyte Size = 1000 * Byte // kilobyte
|
||
|
Megabyte Size = 1000 * Kilobyte // megabyte
|
||
|
Gigabyte Size = 1000 * Megabyte // gigabyte
|
||
|
Terabyte Size = 1000 * Gigabyte // terabyte
|
||
|
Petabyte Size = 1000 * Terabyte // petabyte
|
||
|
)
|
||
|
|
||
|
var unitMap = map[string]uint64{
|
||
|
"B": uint64(Byte),
|
||
|
"KiB": uint64(Kibibyte),
|
||
|
"MiB": uint64(Mebibyte),
|
||
|
"GiB": uint64(Gibibyte),
|
||
|
"TiB": uint64(Tebibyte),
|
||
|
"PiB": uint64(Pebibyte),
|
||
|
|
||
|
"kB": uint64(Kilobyte),
|
||
|
"KB": uint64(Kilobyte),
|
||
|
"MB": uint64(Megabyte),
|
||
|
"GB": uint64(Gigabyte),
|
||
|
"TB": uint64(Terabyte),
|
||
|
"PB": uint64(Petabyte),
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
ErrInvalidSize = errors.New("bytesize: invalid size")
|
||
|
ErrMissingUnit = errors.New("bytesize: missing unit")
|
||
|
ErrUnknownUnit = errors.New("bytesize: unknown unit")
|
||
|
)
|
||
|
|
||
|
// Parse parses a size string.
|
||
|
// A byte size string is a possibly signed sequence of
|
||
|
// decimal numbers, each with optional fraction and a unit suffix,
|
||
|
// such as "300kB", "-1.5GiB" or "2GB45MB".
|
||
|
// Valid units are "B", "KiB", "MiB", "GiB", "TiB", "PiB" for binary size.
|
||
|
// Valid units are "B", "kB", "MB", "GB", "TB", "PB" for human size.
|
||
|
//
|
||
|
// nolint: funlen,gocognit,gocyclo,cyclop
|
||
|
func Parse(in string) (Size, error) {
|
||
|
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
|
||
|
orig := in
|
||
|
neg := false
|
||
|
|
||
|
var res uint64
|
||
|
|
||
|
// Consume [-+]?
|
||
|
if in != "" {
|
||
|
c := in[0]
|
||
|
if c == '-' || c == '+' {
|
||
|
neg = c == '-'
|
||
|
in = in[1:]
|
||
|
}
|
||
|
}
|
||
|
// Special case: if all that is left is "0", this is zero.
|
||
|
if in == "0" {
|
||
|
return 0, nil
|
||
|
}
|
||
|
|
||
|
if in == "" {
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
for in != "" {
|
||
|
var (
|
||
|
val, fVal uint64 // integers before, after decimal point
|
||
|
scale float64 = 1 // value = v + f/scale
|
||
|
)
|
||
|
|
||
|
var err error
|
||
|
|
||
|
// The next character must be [0-9.]
|
||
|
if !(in[0] == '.' || '0' <= in[0] && in[0] <= '9') {
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
// Consume [0-9]*
|
||
|
pl := len(in)
|
||
|
|
||
|
val, in, err = leadingInt(in)
|
||
|
if err != nil {
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
pre := pl != len(in) // whether we consumed anything before a period
|
||
|
|
||
|
// Consume (\.[0-9]*)?
|
||
|
post := false
|
||
|
|
||
|
if in != "" && in[0] == '.' {
|
||
|
in = in[1:]
|
||
|
pl := len(in)
|
||
|
fVal, scale, in = leadingFraction(in)
|
||
|
post = pl != len(in)
|
||
|
}
|
||
|
|
||
|
if !pre && !post {
|
||
|
// no digits (e.g. ".s" or "-.s")
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
// Consume unit.
|
||
|
i := 0
|
||
|
for ; i < len(in); i++ {
|
||
|
c := in[i]
|
||
|
if c == '.' || '0' <= c && c <= '9' {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if i == 0 {
|
||
|
return 0, fmt.Errorf("%w %q", ErrMissingUnit, orig)
|
||
|
}
|
||
|
|
||
|
u := in[:i]
|
||
|
in = in[i:]
|
||
|
unit, ok := unitMap[u]
|
||
|
|
||
|
if !ok {
|
||
|
return 0, fmt.Errorf("%w %s in size %q", ErrMissingUnit, u, orig)
|
||
|
}
|
||
|
|
||
|
if val > 1<<63/unit {
|
||
|
// overflow
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
val *= unit
|
||
|
if fVal > 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)
|
||
|
val += uint64(float64(fVal) * (float64(unit) / scale))
|
||
|
if val > 1<<63 {
|
||
|
// overflow
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
res += val
|
||
|
if res > 1<<63 {
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if neg {
|
||
|
return -Size(res), nil
|
||
|
}
|
||
|
|
||
|
if res > 1<<63-1 {
|
||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||
|
}
|
||
|
|
||
|
return Size(res), 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(in string) (uint64, float64, string) {
|
||
|
i := 0
|
||
|
scale := float64(1)
|
||
|
overflow := false
|
||
|
|
||
|
var res uint64
|
||
|
|
||
|
for ; i < len(in); i++ {
|
||
|
cur := in[i]
|
||
|
if cur < '0' || cur > '9' {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if overflow {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if res > (1<<63-1)/10 {
|
||
|
// It's possible for overflow to give a positive number, so take care.
|
||
|
overflow = true
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
pres := res*10 + uint64(cur) - '0'
|
||
|
if pres > 1<<63 {
|
||
|
overflow = true
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
res = pres
|
||
|
scale *= 10
|
||
|
}
|
||
|
|
||
|
return res, scale, in[i:]
|
||
|
}
|
||
|
|
||
|
var errLeadingInt = errors.New("bytesize: bad [0-9]*")
|
||
|
|
||
|
// leadingInt consumes the leading [0-9]* from in.
|
||
|
func leadingInt[bytes []byte | string](in bytes) (uint64, bytes, error) {
|
||
|
i := 0
|
||
|
|
||
|
var (
|
||
|
res uint64
|
||
|
rem bytes
|
||
|
)
|
||
|
|
||
|
for ; i < len(in); i++ {
|
||
|
curr := in[i]
|
||
|
|
||
|
if curr < '0' || curr > '9' {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if res > 1<<63/10 {
|
||
|
// overflow
|
||
|
return 0, rem, errLeadingInt
|
||
|
}
|
||
|
|
||
|
res = res*10 + uint64(curr) - '0'
|
||
|
if res > 1<<63 {
|
||
|
// overflow
|
||
|
return 0, rem, errLeadingInt
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return res, in[i:], nil
|
||
|
}
|
||
|
|
||
|
func (s Size) String() string {
|
||
|
var arr [32]byte
|
||
|
n := s.format(&arr)
|
||
|
|
||
|
return string(arr[n:])
|
||
|
}
|
||
|
|
||
|
// format formats the representation of d into the end of buf and
|
||
|
// returns the offset of the first character.
|
||
|
// nolint: gosec
|
||
|
func (s Size) format(buf *[32]byte) int {
|
||
|
wLen := len(buf)
|
||
|
|
||
|
data := uint64(s)
|
||
|
neg := s < 0
|
||
|
|
||
|
if neg {
|
||
|
data = -data
|
||
|
}
|
||
|
|
||
|
prec := 3
|
||
|
wLen--
|
||
|
buf[wLen] = 'B'
|
||
|
wLen--
|
||
|
|
||
|
switch {
|
||
|
case data == 0:
|
||
|
buf[wLen] = '0'
|
||
|
|
||
|
return wLen
|
||
|
case data < uint64(Kilobyte):
|
||
|
// print bytes
|
||
|
wLen++
|
||
|
prec = 0
|
||
|
case data < uint64(Megabyte):
|
||
|
// print kipobytes
|
||
|
buf[wLen] = 'k'
|
||
|
case data < uint64(Gigabyte):
|
||
|
// print megabytes
|
||
|
prec = 6
|
||
|
buf[wLen] = 'M'
|
||
|
case data < uint64(Terabyte):
|
||
|
// print gigabytes
|
||
|
prec = 9
|
||
|
buf[wLen] = 'G'
|
||
|
case data < uint64(Petabyte):
|
||
|
// print terabytes
|
||
|
prec = 12
|
||
|
buf[wLen] = 'T'
|
||
|
default:
|
||
|
// print petobytes
|
||
|
prec = 15
|
||
|
buf[wLen] = 'P'
|
||
|
}
|
||
|
|
||
|
wLen, data = fmtFrac(buf[:wLen], data, prec)
|
||
|
wLen = fmtInt(buf[:wLen], data)
|
||
|
|
||
|
if neg {
|
||
|
wLen--
|
||
|
buf[wLen] = '-'
|
||
|
}
|
||
|
|
||
|
return wLen
|
||
|
}
|
||
|
|
||
|
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
||
|
// tail of buf, omitting trailing zeros. It omits the decimal
|
||
|
// point too when the fraction is 0. It returns the index where the
|
||
|
// output bytes begin and the value v/10**prec.
|
||
|
func fmtFrac(buf []byte, val uint64, prec int) (int, uint64) {
|
||
|
// Omit trailing zeros up to and including decimal point.
|
||
|
wLen := len(buf)
|
||
|
show := false
|
||
|
|
||
|
for range prec {
|
||
|
digit := val % 10
|
||
|
show = show || digit != 0
|
||
|
|
||
|
if show {
|
||
|
wLen--
|
||
|
buf[wLen] = byte(digit) + '0'
|
||
|
}
|
||
|
|
||
|
val /= 10
|
||
|
}
|
||
|
|
||
|
if show {
|
||
|
wLen--
|
||
|
buf[wLen] = '.'
|
||
|
}
|
||
|
|
||
|
return wLen, val
|
||
|
}
|
||
|
|
||
|
// fmtInt formats v into the tail of buf.
|
||
|
// It returns the index where the output begins.
|
||
|
func fmtInt(buf []byte, val uint64) int {
|
||
|
wLen := len(buf)
|
||
|
if val == 0 {
|
||
|
wLen--
|
||
|
buf[wLen] = '0'
|
||
|
} else {
|
||
|
for val > 0 {
|
||
|
wLen--
|
||
|
buf[wLen] = byte(val%10) + '0'
|
||
|
val /= 10
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return wLen
|
||
|
}
|