gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/parse.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math"
     9  	"math/big"
    10  	"os"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"gitlab.com/NebulousLabs/encoding"
    17  	"gitlab.com/NebulousLabs/errors"
    18  	"gitlab.com/SkynetLabs/skyd/build"
    19  	"go.sia.tech/siad/types"
    20  )
    21  
    22  var (
    23  	// ErrParsePeriodAmount is returned when the input is unable to be parsed
    24  	// into a period unit due to a malformed amount.
    25  	ErrParsePeriodAmount = errors.New("malformed amount")
    26  	// ErrParsePeriodUnits is returned when the input is unable to be parsed
    27  	// into a period unit due to missing units.
    28  	ErrParsePeriodUnits = errors.New("amount is missing period units")
    29  
    30  	// ErrParseRateLimitAmount is returned when the input is unable to be parsed into
    31  	// a rate limit unit due to a malformed amount.
    32  	ErrParseRateLimitAmount = errors.New("malformed amount")
    33  	// ErrParseRateLimitNoAmount is returned when the input is unable to be
    34  	// parsed into a rate limit unit due to no amount being given.
    35  	ErrParseRateLimitNoAmount = errors.New("amount is missing")
    36  	// ErrParseRateLimitUnits is returned when the input is unable to be parsed
    37  	// into a rate limit unit due to missing units.
    38  	ErrParseRateLimitUnits = errors.New("amount is missing rate limit units")
    39  
    40  	// ErrParseSizeAmount is returned when the input is unable to be parsed into
    41  	// a file size unit due to a malformed amount.
    42  	ErrParseSizeAmount = errors.New("malformed amount")
    43  	// ErrParseSizeUnits is returned when the input is unable to be parsed into
    44  	// a file size unit due to missing units.
    45  	ErrParseSizeUnits = errors.New("amount is missing filesize units")
    46  
    47  	// ErrParseTimeoutAmount is returned when the input is unable to be parsed
    48  	// into a timeout unit due to a malformed amount.
    49  	ErrParseTimeoutAmount = errors.New("malformed amount")
    50  	// ErrParseTimeoutUnits is returned when the input is unable to be parsed
    51  	// into a timeout unit due to missing units.
    52  	ErrParseTimeoutUnits = errors.New("amount is missing timeout units")
    53  )
    54  
    55  // bandwidthUnit takes bps (bits per second) as an argument and converts
    56  // them into a more human-readable string with a unit.
    57  func bandwidthUnit(bps uint64) string {
    58  	units := []string{"Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"}
    59  	mag := uint64(1)
    60  	unit := ""
    61  	for _, unit = range units {
    62  		if bps < 1e3*mag {
    63  			break
    64  		} else if unit != units[len(units)-1] {
    65  			// don't want to perform this multiply on the last iter; that
    66  			// would give us 1.235 Ybps instead of 1235 Ybps
    67  			mag *= 1e3
    68  		}
    69  	}
    70  	return fmt.Sprintf("%.2f %s", float64(bps)/float64(mag), unit)
    71  }
    72  
    73  // parseFilesize converts strings of form '10GB' or '10 gb' to a size in bytes.
    74  // Fractional sizes are truncated at the byte size.
    75  func parseFilesize(strSize string) (string, error) {
    76  	units := []struct {
    77  		suffix     string
    78  		multiplier int64
    79  	}{
    80  		{"kb", 1e3},
    81  		{"mb", 1e6},
    82  		{"gb", 1e9},
    83  		{"tb", 1e12},
    84  		{"kib", 1 << 10},
    85  		{"mib", 1 << 20},
    86  		{"gib", 1 << 30},
    87  		{"tib", 1 << 40},
    88  		{"b", 1}, // must be after others else it'll match on them all
    89  	}
    90  
    91  	strSize = strings.ToLower(strings.TrimSpace(strSize))
    92  	for _, unit := range units {
    93  		if strings.HasSuffix(strSize, unit.suffix) {
    94  			// Trim spaces after removing the suffix to allow spaces between the
    95  			// value and the unit.
    96  			value := strings.TrimSpace(strings.TrimSuffix(strSize, unit.suffix))
    97  			r, ok := new(big.Rat).SetString(value)
    98  			if !ok {
    99  				return "", ErrParseSizeAmount
   100  			}
   101  			r.Mul(r, new(big.Rat).SetInt(big.NewInt(unit.multiplier)))
   102  			if !r.IsInt() {
   103  				f, _ := r.Float64()
   104  				return fmt.Sprintf("%d", int64(f)), nil
   105  			}
   106  			return r.RatString(), nil
   107  		}
   108  	}
   109  
   110  	return "", ErrParseSizeUnits
   111  }
   112  
   113  // periodUnits turns a period in terms of blocks to a number of weeks.
   114  func periodUnits(blocks types.BlockHeight) string {
   115  	return fmt.Sprint(blocks / 1008) // 1008 blocks per week
   116  }
   117  
   118  // parsePeriod converts a duration specified in blocks, hours, or weeks to a
   119  // number of blocks.
   120  func parsePeriod(period string) (string, error) {
   121  	units := []struct {
   122  		suffix     string
   123  		multiplier float64
   124  	}{
   125  		{"b", 1},        // blocks
   126  		{"block", 1},    // blocks
   127  		{"blocks", 1},   // blocks
   128  		{"h", 6},        // hours
   129  		{"hour", 6},     // hours
   130  		{"hours", 6},    // hours
   131  		{"d", 144},      // days
   132  		{"day", 144},    // days
   133  		{"days", 144},   // days
   134  		{"w", 1008},     // weeks
   135  		{"week", 1008},  // weeks
   136  		{"weeks", 1008}, // weeks
   137  	}
   138  
   139  	period = strings.ToLower(strings.TrimSpace(period))
   140  	for _, unit := range units {
   141  		if strings.HasSuffix(period, unit.suffix) {
   142  			var base float64
   143  			_, err := fmt.Sscan(strings.TrimSuffix(period, unit.suffix), &base)
   144  			if err != nil {
   145  				return "", ErrParsePeriodAmount
   146  			}
   147  			blocks := int(base * unit.multiplier)
   148  			return fmt.Sprint(blocks), nil
   149  		}
   150  	}
   151  
   152  	return "", ErrParsePeriodUnits
   153  }
   154  
   155  // parseTimeout converts a duration specified in seconds, hours, days or weeks
   156  // to a number of seconds
   157  func parseTimeout(duration string) (string, error) {
   158  	units := []struct {
   159  		suffix     string
   160  		multiplier float64
   161  	}{
   162  		{"s", 1},          // seconds
   163  		{"second", 1},     // seconds
   164  		{"seconds", 1},    // seconds
   165  		{"h", 3600},       // hours
   166  		{"hour", 3600},    // hours
   167  		{"hours", 3600},   // hours
   168  		{"d", 86400},      // days
   169  		{"day", 86400},    // days
   170  		{"days", 86400},   // days
   171  		{"w", 604800},     // weeks
   172  		{"week", 604800},  // weeks
   173  		{"weeks", 604800}, // weeks
   174  	}
   175  
   176  	duration = strings.ToLower(strings.TrimSpace(duration))
   177  	for _, unit := range units {
   178  		if strings.HasSuffix(duration, unit.suffix) {
   179  			value := strings.TrimSpace(strings.TrimSuffix(duration, unit.suffix))
   180  			var base float64
   181  			_, err := fmt.Sscan(value, &base)
   182  			if err != nil {
   183  				return "", ErrParseTimeoutAmount
   184  			}
   185  			seconds := int(base * unit.multiplier)
   186  			return fmt.Sprint(seconds), nil
   187  		}
   188  	}
   189  
   190  	return "", ErrParseTimeoutUnits
   191  }
   192  
   193  // currencyUnits converts a types.Currency to a string with human-readable
   194  // units. The unit used will be the largest unit that results in a value
   195  // greater than 1. The value is rounded to 4 significant digits.
   196  func currencyUnits(c types.Currency) string {
   197  	pico := types.SiacoinPrecision.Div64(1e12)
   198  	if c.Cmp(pico) < 0 {
   199  		return c.String() + " H"
   200  	}
   201  
   202  	// iterate until we find a unit greater than c
   203  	mag := pico
   204  	unit := ""
   205  	for _, unit = range []string{"pS", "nS", "uS", "mS", "SC", "KS", "MS", "GS", "TS"} {
   206  		if c.Cmp(mag.Mul64(1e3)) < 0 {
   207  			break
   208  		} else if unit != "TS" {
   209  			// don't want to perform this multiply on the last iter; that
   210  			// would give us 1.235 TS instead of 1235 TS
   211  			mag = mag.Mul64(1e3)
   212  		}
   213  	}
   214  
   215  	num := new(big.Rat).SetInt(c.Big())
   216  	denom := new(big.Rat).SetInt(mag.Big())
   217  	res, _ := new(big.Rat).Mul(num, denom.Inv(denom)).Float64()
   218  
   219  	return fmt.Sprintf("%.4g %s", res, unit)
   220  }
   221  
   222  // bigIntToCurrencyUnitsWithExchangeRate will transform a big.Int into a
   223  // currency with sign indicator, and then format the currency in the same way as
   224  // currencyUnits. If a non-nil exchange rate is provided, it will additionally
   225  // provide the result of applying the rate to the amount.
   226  func bigIntToCurrencyUnitsWithExchangeRate(b *big.Int, rate *types.ExchangeRate) string {
   227  	var sign string
   228  	if b.Sign() == -1 {
   229  		sign = "-"
   230  	}
   231  
   232  	c := types.NewCurrency(b.Abs(b))
   233  	cString := currencyUnits(c)
   234  	if rate == nil {
   235  		return fmt.Sprintf("%s%s", sign, cString)
   236  	}
   237  
   238  	return fmt.Sprintf("%s%s (%s)", sign, cString, rate.ApplyAndFormat(c))
   239  }
   240  
   241  // currencyUnitsWithExchangeRate will format a types.Currency in the same way as
   242  // currencyUnits. If a non-nil exchange rate is provided, it will additionally
   243  // provide the result of applying the rate to the amount.
   244  func currencyUnitsWithExchangeRate(c types.Currency, rate *types.ExchangeRate) string {
   245  	cString := currencyUnits(c)
   246  	if rate == nil {
   247  		return cString
   248  	}
   249  
   250  	return fmt.Sprintf("%s (%s)", cString, rate.ApplyAndFormat(c))
   251  }
   252  
   253  // parseRatelimit converts a ratelimit input string of to an int64 representing
   254  // the bytes per second ratelimit.
   255  func parseRatelimit(rateLimitStr string) (int64, error) {
   256  	// Check for 0 values signifying that the no limit is being set
   257  	if rateLimitStr == "0" {
   258  		return 0, nil
   259  	}
   260  	// Create struct of rates. Have to start at the high end so that B/s is
   261  	// checked last, otherwise it would return false positives
   262  	rates := []struct {
   263  		unit   string
   264  		factor float64
   265  	}{
   266  		{"TB/s", 1e12},
   267  		{"GB/s", 1e9},
   268  		{"MB/s", 1e6},
   269  		{"KB/s", 1e3},
   270  		{"B/s", 1e0},
   271  		{"Tbps", 1e12 / 8},
   272  		{"Gbps", 1e9 / 8},
   273  		{"Mbps", 1e6 / 8},
   274  		{"Kbps", 1e3 / 8},
   275  		{"Bps", 1e0 / 8},
   276  	}
   277  	rateLimitStr = strings.TrimSpace(rateLimitStr)
   278  	for _, rate := range rates {
   279  		if !strings.HasSuffix(rateLimitStr, rate.unit) {
   280  			continue
   281  		}
   282  
   283  		// trim units and spaces
   284  		rateLimitStr = strings.TrimSuffix(rateLimitStr, rate.unit)
   285  		rateLimitStr = strings.TrimSpace(rateLimitStr)
   286  
   287  		// Check for empty string meaning only the units were provided
   288  		if rateLimitStr == "" {
   289  			return 0, ErrParseRateLimitNoAmount
   290  		}
   291  
   292  		// convert string to float for exponation
   293  		rateLimitFloat, err := strconv.ParseFloat(rateLimitStr, 64)
   294  		if err != nil {
   295  			return 0, errors.Compose(ErrParseRateLimitAmount, err)
   296  		}
   297  		// Check for Bps to make sure it is greater than 8 Bps meaning that it is at
   298  		// least 1 B/s
   299  		if rateLimitFloat < 8 && rate.unit == "Bps" {
   300  			return 0, errors.AddContext(ErrParseRateLimitAmount, "Bps rate limit cannot be < 8 Bps")
   301  		}
   302  
   303  		// Determine factor and convert to int64 for bps
   304  		rateLimit := int64(rateLimitFloat * rate.factor)
   305  
   306  		return rateLimit, nil
   307  	}
   308  
   309  	return 0, ErrParseRateLimitUnits
   310  }
   311  
   312  // ratelimitUnits converts an int64 to a string with human-readable ratelimit
   313  // units. The unit used will be the largest unit that results in a value greater
   314  // than 1. The value is rounded to 4 significant digits.
   315  func ratelimitUnits(ratelimit int64) string {
   316  	// Check for bps
   317  	if ratelimit < 1e3 {
   318  		return fmt.Sprintf("%v %s", ratelimit, "B/s")
   319  	}
   320  	// iterate until we find a unit greater than c
   321  	mag := 1e3
   322  	unit := ""
   323  	for _, unit = range []string{"KB/s", "MB/s", "GB/s", "TB/s"} {
   324  		if float64(ratelimit) < mag*1e3 {
   325  			break
   326  		} else if unit != "TB/s" {
   327  			// don't want to perform this multiply on the last iter; that
   328  			// would give us 1.235 tbps instead of 1235 tbps
   329  			mag = mag * 1e3
   330  		}
   331  	}
   332  
   333  	return fmt.Sprintf("%.4g %s", float64(ratelimit)/mag, unit)
   334  }
   335  
   336  // yesNo returns "Yes" if b is true, and "No" if b is false.
   337  func yesNo(b bool) string {
   338  	if b {
   339  		return "Yes"
   340  	}
   341  	return "No"
   342  }
   343  
   344  // parseTxn decodes a transaction from s, which can be JSON, base64, or a path
   345  // to a file containing either encoding.
   346  func parseTxn(s string) (types.Transaction, error) {
   347  	// first assume s is a file
   348  	txnBytes, err := ioutil.ReadFile(s)
   349  	if os.IsNotExist(err) {
   350  		// assume s is a literal encoding
   351  		txnBytes = []byte(s)
   352  	} else if err != nil {
   353  		return types.Transaction{}, errors.New("could not read transaction file: " + err.Error())
   354  	}
   355  	// txnBytes now contains either s or the contents of the file, so it is
   356  	// either JSON or base64
   357  	var txn types.Transaction
   358  	if json.Valid(txnBytes) {
   359  		if err := json.Unmarshal(txnBytes, &txn); err != nil {
   360  			return types.Transaction{}, errors.New("could not decode JSON transaction: " + err.Error())
   361  		}
   362  	} else {
   363  		bin, err := base64.StdEncoding.DecodeString(string(txnBytes))
   364  		if err != nil {
   365  			return types.Transaction{}, errors.New("argument is not valid JSON, base64, or filepath")
   366  		}
   367  		if err := encoding.Unmarshal(bin, &txn); err != nil {
   368  			return types.Transaction{}, errors.New("could not decode binary transaction: " + err.Error())
   369  		}
   370  	}
   371  	return txn, nil
   372  }
   373  
   374  // fmtDuration converts a time.Duration into a days,hours,minutes string
   375  func fmtDuration(dur time.Duration) string {
   376  	dur = dur.Round(time.Minute)
   377  	d := dur / time.Hour / 24
   378  	dur -= d * time.Hour * 24
   379  	h := dur / time.Hour
   380  	dur -= h * time.Hour
   381  	m := dur / time.Minute
   382  	return fmt.Sprintf("%02d days %02d hours %02d minutes", d, h, m)
   383  }
   384  
   385  // parsePercentages takes a range of floats and returns them rounded to
   386  // percentages that add up to 100. They will be returned in the same order that
   387  // they were provided
   388  func parsePercentages(values []float64) []float64 {
   389  	// Create a slice of percentInfo to track information of the values in the
   390  	// slice and calculate the subTotal of the floor values
   391  	type percentInfo struct {
   392  		index       int
   393  		floorVal    float64
   394  		originalVal float64
   395  	}
   396  	var percentages []*percentInfo
   397  	var subTotal float64
   398  	for i, v := range values {
   399  		fv := math.Floor(v)
   400  		percentages = append(percentages, &percentInfo{
   401  			index:       i,
   402  			floorVal:    fv,
   403  			originalVal: v,
   404  		})
   405  		subTotal += fv
   406  	}
   407  
   408  	// Sanity check and regression check that all values were added. Fine to
   409  	// continue through in production as result will only be a minor UX
   410  	// descrepency
   411  	if len(percentages) != len(values) {
   412  		build.Critical("Not all values added to percentage slice; potential duplicate value error")
   413  	}
   414  
   415  	// Determine the difference to 100 from the subTotal of the floor values
   416  	diff := 100 - subTotal
   417  
   418  	// Diff should always be smaller than the number of values. Sanity check for
   419  	// developers, fine to continue through in production as result will only be
   420  	// a minor UX descrepency
   421  	if int(diff) > len(values) {
   422  		build.Critical(fmt.Errorf("Unexpected diff value %v, number of values %v", diff, len(values)))
   423  	}
   424  
   425  	// Sort the slice based on the size of the decimal value
   426  	sort.Slice(percentages, func(i, j int) bool {
   427  		_, a := math.Modf(percentages[i].originalVal)
   428  		_, b := math.Modf(percentages[j].originalVal)
   429  		return a > b
   430  	})
   431  
   432  	// Divide the diff amongst the floor values from largest decimal value to
   433  	// the smallest to decide which values get rounded up.
   434  	for _, pi := range percentages {
   435  		if diff <= 0 {
   436  			break
   437  		}
   438  		pi.floorVal++
   439  		diff--
   440  	}
   441  
   442  	// Reorder the slice and return
   443  	for _, pi := range percentages {
   444  		values[pi.index] = pi.floorVal
   445  	}
   446  
   447  	return values
   448  }
   449  
   450  // sizeString converts the uint64 size to a string with appropriate units and
   451  // truncates to 4 significant digits.
   452  func sizeString(size uint64) string {
   453  	sizes := []struct {
   454  		unit   string
   455  		factor float64
   456  	}{
   457  		{"EB", 1e18},
   458  		{"PB", 1e15},
   459  		{"TB", 1e12},
   460  		{"GB", 1e9},
   461  		{"MB", 1e6},
   462  		{"KB", 1e3},
   463  		{"B", 1e0},
   464  	}
   465  
   466  	// Convert size to a float
   467  	for i, s := range sizes {
   468  		// Check to see if we are at the right order of magnitude.
   469  		res := float64(size) / s.factor
   470  		if res < 1 {
   471  			continue
   472  		}
   473  		// Create the string
   474  		str := fmt.Sprintf("%.4g %s", res, s.unit)
   475  		// Check for rounding to three 0s
   476  		if !strings.Contains(str, "000") {
   477  			return str
   478  		}
   479  		// If we are at the max unit then there is no trimming to do
   480  		if i == 0 {
   481  			build.Critical("input uint64 overflows uint64, shouldn't be possible")
   482  			return str
   483  		}
   484  		// Trim the trailing three 0s and round to the next unit size
   485  		return fmt.Sprintf("1 %s", sizes[i-1].unit)
   486  	}
   487  	return "0 B"
   488  }