package lib import ( "encoding/base64" "encoding/hex" "fmt" "os" "path/filepath" "strconv" "strings" "time" ) var progname = filepath.Base(os.Args[0]) const ( daysInYear = 365 digitWidth = 10 hoursInQuarterDay = 6 ) // ProgName returns what lib thinks the program name is, namely the // basename of argv0. // // It is similar to the Linux __progname function. func ProgName() string { return progname } // Warnx displays a formatted error message to standard error, à la // warnx(3). func Warnx(format string, a ...any) (int, error) { format = fmt.Sprintf("[%s] %s", progname, format) format += "\n" return fmt.Fprintf(os.Stderr, format, a...) } // Warn displays a formatted error message to standard output, // appending the error string, à la warn(3). func Warn(err error, format string, a ...any) (int, error) { format = fmt.Sprintf("[%s] %s", progname, format) format += ": %v\n" a = append(a, err) return fmt.Fprintf(os.Stderr, format, a...) } // Errx displays a formatted error message to standard error and exits // with the status code from `exit`, à la errx(3). func Errx(exit int, format string, a ...any) { format = fmt.Sprintf("[%s] %s", progname, format) format += "\n" fmt.Fprintf(os.Stderr, format, a...) os.Exit(exit) } // Err displays a formatting error message to standard error, // appending the error string, and exits with the status code from // `exit`, à la err(3). func Err(exit int, err error, format string, a ...any) { format = fmt.Sprintf("[%s] %s", progname, format) format += ": %v\n" a = append(a, err) fmt.Fprintf(os.Stderr, format, a...) os.Exit(exit) } // Itoa provides cheap integer to fixed-width decimal ASCII. Give a // negative width to avoid zero-padding. Adapted from the 'itoa' // function in the log/log.go file in the standard library. func Itoa(i int, wid int) string { // Assemble decimal in reverse order. var b [20]byte bp := len(b) - 1 for i >= digitWidth || wid > 1 { wid-- q := i / digitWidth b[bp] = byte('0' + i - q*digitWidth) bp-- i = q } b[bp] = byte('0' + i) return string(b[bp:]) } var ( dayDuration = 24 * time.Hour yearDuration = (daysInYear * dayDuration) + (hoursInQuarterDay * time.Hour) ) // Duration returns a prettier string for time.Durations. func Duration(d time.Duration) string { var s string if d >= yearDuration { years := int64(d / yearDuration) s += fmt.Sprintf("%dy", years) d -= time.Duration(years) * yearDuration } if d >= dayDuration { days := d / dayDuration s += fmt.Sprintf("%dd", days) } if s != "" { return s } d %= 1 * time.Second hours := int64(d / time.Hour) d -= time.Duration(hours) * time.Hour s += fmt.Sprintf("%dh%s", hours, d) return s } // IsDigit checks if a byte is a decimal digit. func IsDigit(b byte) bool { return b >= '0' && b <= '9' } // ParseDuration parses a duration string into a time.Duration. // It supports standard units (ns, us/µs, ms, s, m, h) plus extended units: // d (days, 24h), w (weeks, 7d), y (years, 365d). // Units can be combined without spaces, e.g., "1y2w3d4h5m6s". // Case-insensitive. Years and days are approximations (no leap seconds/months). // Returns an error for invalid input. func ParseDuration(s string) (time.Duration, error) { s = strings.ToLower(s) // Normalize to lowercase for case-insensitivity. if s == "" { return 0, fmt.Errorf("empty duration string") } var total time.Duration i := 0 for i < len(s) { // Parse the number part. start := i for i < len(s) && IsDigit(s[i]) { i++ } if start == i { return 0, fmt.Errorf("expected number at position %d", start) } numStr := s[start:i] num, err := strconv.ParseUint(numStr, 10, 64) if err != nil { return 0, fmt.Errorf("invalid number %q: %w", numStr, err) } // Parse the unit part. if i >= len(s) { return 0, fmt.Errorf("expected unit after number %q", numStr) } unitStart := i i++ // Consume the first char of the unit. unit := s[unitStart:i] // Handle potential two-char units like "ms". if unit == "m" && i < len(s) && s[i] == 's' { i++ // Consume the 's'. unit = "ms" } // Convert to duration based on unit. var d time.Duration switch unit { case "ns": d = time.Nanosecond * time.Duration(num) case "us", "µs": d = time.Microsecond * time.Duration(num) case "ms": d = time.Millisecond * time.Duration(num) case "s": d = time.Second * time.Duration(num) case "m": d = time.Minute * time.Duration(num) case "h": d = time.Hour * time.Duration(num) case "d": d = 24 * time.Hour * time.Duration(num) case "w": d = 7 * 24 * time.Hour * time.Duration(num) case "y": d = 365 * 24 * time.Hour * time.Duration(num) // Approximate, non-leap year. default: return 0, fmt.Errorf("unknown unit %q at position %d", s[unitStart:i], unitStart) } total += d } return total, nil } type HexEncodeMode uint8 const ( // HexEncodeLower prints the bytes as lowercase hexadecimal. HexEncodeLower HexEncodeMode = iota + 1 // HexEncodeUpper prints the bytes as uppercase hexadecimal. HexEncodeUpper // HexEncodeLowerColon prints the bytes as lowercase hexadecimal // with colons between each pair of bytes. HexEncodeLowerColon // HexEncodeUpperColon prints the bytes as uppercase hexadecimal // with colons between each pair of bytes. HexEncodeUpperColon // HexEncodeBytes prints the string as a sequence of []byte. HexEncodeBytes // HexEncodeBase64 prints the string as a base64-encoded string. HexEncodeBase64 ) func (m HexEncodeMode) String() string { switch m { case HexEncodeLower: return "lower" case HexEncodeUpper: return "upper" case HexEncodeLowerColon: return "lcolon" case HexEncodeUpperColon: return "ucolon" case HexEncodeBytes: return "bytes" case HexEncodeBase64: return "base64" default: panic("invalid hex encode mode") } } func ParseHexEncodeMode(s string) HexEncodeMode { switch strings.ToLower(s) { case "lower": return HexEncodeLower case "upper": return HexEncodeUpper case "lcolon": return HexEncodeLowerColon case "ucolon": return HexEncodeUpperColon case "bytes": return HexEncodeBytes case "base64": return HexEncodeBase64 } panic("invalid hex encode mode") } func hexColons(s string) string { if len(s)%2 != 0 { fmt.Fprintf(os.Stderr, "hex string: %s\n", s) fmt.Fprintf(os.Stderr, "hex length: %d\n", len(s)) panic("invalid hex string length") } n := len(s) if n <= 2 { return s } pairCount := n / 2 if n%2 != 0 { pairCount++ } var b strings.Builder b.Grow(n + pairCount - 1) for i := 0; i < n; i += 2 { b.WriteByte(s[i]) if i+1 < n { b.WriteByte(s[i+1]) } if i+2 < n { b.WriteByte(':') } } return b.String() } func hexEncode(b []byte) string { s := hex.EncodeToString(b) if len(s)%2 != 0 { s = "0" + s } return s } func bytesAsByteSliceString(buf []byte) string { sb := &strings.Builder{} sb.WriteString("[]byte{") for i := range buf { fmt.Fprintf(sb, "0x%02x, ", buf[i]) } sb.WriteString("}") return sb.String() } // HexEncode encodes the given bytes as a hexadecimal string. It // also supports a few other binary-encoding formats as well. func HexEncode(b []byte, mode HexEncodeMode) string { switch mode { case HexEncodeLower: return hexEncode(b) case HexEncodeUpper: return strings.ToUpper(hexEncode(b)) case HexEncodeLowerColon: return hexColons(hexEncode(b)) case HexEncodeUpperColon: return strings.ToUpper(hexColons(hexEncode(b))) case HexEncodeBytes: return bytesAsByteSliceString(b) case HexEncodeBase64: return base64.StdEncoding.EncodeToString(b) default: panic("invalid hex encode mode") } }