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

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
}