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 }