summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2022-06-11 13:52:02 -0600
committerLuke Shumaker <lukeshu@lukeshu.com>2022-06-11 22:31:33 -0600
commit468eca4684d9b51d00e18cb129bebf528a844035 (patch)
tree85191e6ff392833d737370a20a9b119e6139e04f /pkg
parent5cecda729a22bcca9b1fa8234dd03140c0380720 (diff)
Improve formatting of CSums and UUIDs
Diffstat (limited to 'pkg')
-rw-r--r--pkg/btrfs/crc32c.go9
-rw-r--r--pkg/btrfs/crc32c_test.go35
-rw-r--r--pkg/btrfs/internal/uuid.go46
-rw-r--r--pkg/btrfs/internal/uuid_test.go63
-rw-r--r--pkg/util/fmt.go67
-rw-r--r--pkg/util/fmt_test.go99
6 files changed, 318 insertions, 1 deletions
diff --git a/pkg/btrfs/crc32c.go b/pkg/btrfs/crc32c.go
index ab2d6e9..52058e8 100644
--- a/pkg/btrfs/crc32c.go
+++ b/pkg/btrfs/crc32c.go
@@ -2,14 +2,21 @@ package btrfs
import (
"encoding/binary"
+ "encoding/hex"
"fmt"
"hash/crc32"
+
+ "lukeshu.com/btrfs-tools/pkg/util"
)
type CSum [0x20]byte
func (csum CSum) String() string {
- return fmt.Sprintf("%x", [0x20]byte(csum))
+ return hex.EncodeToString(csum[:])
+}
+
+func (csum CSum) Format(f fmt.State, verb rune) {
+ util.FormatByteArrayStringer(csum, csum[:], f, verb)
}
func CRC32c(data []byte) CSum {
diff --git a/pkg/btrfs/crc32c_test.go b/pkg/btrfs/crc32c_test.go
new file mode 100644
index 0000000..335b3cc
--- /dev/null
+++ b/pkg/btrfs/crc32c_test.go
@@ -0,0 +1,35 @@
+package btrfs_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "lukeshu.com/btrfs-tools/pkg/btrfs"
+)
+
+func TestCSumFormat(t *testing.T) {
+ t.Parallel()
+ type TestCase struct {
+ InputSum btrfs.CSum
+ InputFmt string
+ Output string
+ }
+ csum := btrfs.CSum{0xbd, 0x7b, 0x41, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
+ testcases := map[string]TestCase{
+ "s": TestCase{InputSum: csum, InputFmt: "%s", Output: "bd7b41f400000000000000000000000000000000000000000000000000000000"},
+ "x": TestCase{InputSum: csum, InputFmt: "%x", Output: "bd7b41f400000000000000000000000000000000000000000000000000000000"},
+ "v": TestCase{InputSum: csum, InputFmt: "%v", Output: "bd7b41f400000000000000000000000000000000000000000000000000000000"},
+ "70s": TestCase{InputSum: csum, InputFmt: "|% 70s", Output: "| bd7b41f400000000000000000000000000000000000000000000000000000000"},
+ "#180v": TestCase{InputSum: csum, InputFmt: "%#180v", Output: " btrfs.CSum{0xbd, 0x7b, 0x41, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}"},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ actual := fmt.Sprintf(tc.InputFmt, tc.InputSum)
+ assert.Equal(t, tc.Output, actual)
+ })
+ }
+}
diff --git a/pkg/btrfs/internal/uuid.go b/pkg/btrfs/internal/uuid.go
index 6169638..cb2c800 100644
--- a/pkg/btrfs/internal/uuid.go
+++ b/pkg/btrfs/internal/uuid.go
@@ -2,7 +2,10 @@ package internal
import (
"encoding/hex"
+ "fmt"
"strings"
+
+ "lukeshu.com/btrfs-tools/pkg/util"
)
type UUID [16]byte
@@ -17,3 +20,46 @@ func (uuid UUID) String() string {
str[20:32],
}, "-")
}
+
+func (uuid UUID) Format(f fmt.State, verb rune) {
+ util.FormatByteArrayStringer(uuid, uuid[:], f, verb)
+}
+
+func ParseUUID(str string) (UUID, error) {
+ var ret UUID
+ j := 0
+ for i := 0; i < len(str); i++ {
+ if j >= len(ret)*2 {
+ return UUID{}, fmt.Errorf("too long to be a UUID: %q|%q", str[:i], str[i:])
+ }
+ c := str[i]
+ var v byte
+ switch {
+ case '0' <= c && c <= '9':
+ v = c - '0'
+ case 'a' <= c && c <= 'f':
+ v = c - 'a' + 10
+ case 'A' <= c && c <= 'F':
+ v = c - 'A' + 10
+ case c == '-':
+ continue
+ default:
+ return UUID{}, fmt.Errorf("illegal byte in UUID: %q|%q|%q", str[:i], str[i:i+1], str[i+1:])
+ }
+ if j%2 == 0 {
+ ret[j/2] = v << 4
+ } else {
+ ret[j/2] = (ret[j/2] & 0xf0) | (v & 0x0f)
+ }
+ j++
+ }
+ return ret, nil
+}
+
+func MustParseUUID(str string) UUID {
+ ret, err := ParseUUID(str)
+ if err != nil {
+ panic(err)
+ }
+ return ret
+}
diff --git a/pkg/btrfs/internal/uuid_test.go b/pkg/btrfs/internal/uuid_test.go
new file mode 100644
index 0000000..8c78570
--- /dev/null
+++ b/pkg/btrfs/internal/uuid_test.go
@@ -0,0 +1,63 @@
+package internal_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "lukeshu.com/btrfs-tools/pkg/btrfs/internal"
+)
+
+func TestParseUUID(t *testing.T) {
+ t.Parallel()
+ type TestCase struct {
+ Input string
+ OutputVal internal.UUID
+ OutputErr string
+ }
+ testcases := map[string]TestCase{
+ "basic": TestCase{Input: "a0dd94ed-e60c-42e8-8632-64e8d4765a43", OutputVal: internal.UUID{0xa0, 0xdd, 0x94, 0xed, 0xe6, 0x0c, 0x42, 0xe8, 0x86, 0x32, 0x64, 0xe8, 0xd4, 0x76, 0x5a, 0x43}},
+ "too-long": TestCase{Input: "a0dd94ed-e60c-42e8-8632-64e8d4765a43a", OutputErr: `too long to be a UUID: "a0dd94ed-e60c-42e8-8632-64e8d4765a43"|"a"`},
+ "bad char": TestCase{Input: "a0dd94ej-e60c-42e8-8632-64e8d4765a43a", OutputErr: `illegal byte in UUID: "a0dd94e"|"j"|"-e60c-42e8-8632-64e8d4765a43a"`},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ val, err := internal.ParseUUID(tc.Input)
+ assert.Equal(t, tc.OutputVal, val)
+ if tc.OutputErr == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.OutputErr)
+ }
+ })
+ }
+}
+
+func TestUUIDFormat(t *testing.T) {
+ t.Parallel()
+ type TestCase struct {
+ InputUUID internal.UUID
+ InputFmt string
+ Output string
+ }
+ uuid := internal.MustParseUUID("a0dd94ed-e60c-42e8-8632-64e8d4765a43")
+ testcases := map[string]TestCase{
+ "s": TestCase{InputUUID: uuid, InputFmt: "%s", Output: "a0dd94ed-e60c-42e8-8632-64e8d4765a43"},
+ "x": TestCase{InputUUID: uuid, InputFmt: "%x", Output: "a0dd94ede60c42e8863264e8d4765a43"},
+ "X": TestCase{InputUUID: uuid, InputFmt: "%X", Output: "A0DD94EDE60C42E8863264E8D4765A43"},
+ "v": TestCase{InputUUID: uuid, InputFmt: "%v", Output: "a0dd94ed-e60c-42e8-8632-64e8d4765a43"},
+ "40s": TestCase{InputUUID: uuid, InputFmt: "|% 40s", Output: "| a0dd94ed-e60c-42e8-8632-64e8d4765a43"},
+ "#115v": TestCase{InputUUID: uuid, InputFmt: "|%#115v", Output: "| internal.UUID{0xa0, 0xdd, 0x94, 0xed, 0xe6, 0xc, 0x42, 0xe8, 0x86, 0x32, 0x64, 0xe8, 0xd4, 0x76, 0x5a, 0x43}"},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ actual := fmt.Sprintf(tc.InputFmt, tc.InputUUID)
+ assert.Equal(t, tc.Output, actual)
+ })
+ }
+}
diff --git a/pkg/util/fmt.go b/pkg/util/fmt.go
new file mode 100644
index 0000000..c0ff596
--- /dev/null
+++ b/pkg/util/fmt.go
@@ -0,0 +1,67 @@
+package util
+
+import (
+ "fmt"
+ "strings"
+)
+
+// FmtStateString returns the fmt.Printf string that produced a given
+// fmt.State and verb.
+func FmtStateString(st fmt.State, verb rune) string {
+ var ret strings.Builder
+ ret.WriteByte('%')
+ for _, flag := range []int{'-', '+', '#', ' ', '0'} {
+ if st.Flag(flag) {
+ ret.WriteByte(byte(flag))
+ }
+ }
+ if width, ok := st.Width(); ok {
+ fmt.Fprintf(&ret, "%d", width)
+ }
+ if prec, ok := st.Precision(); ok {
+ if prec == 0 {
+ ret.WriteByte('.')
+ } else {
+ fmt.Fprintf(&ret, ".%d", prec)
+ }
+ }
+ ret.WriteRune(verb)
+ return ret.String()
+}
+
+// FormatByteArrayStringer is function for helping to implement
+// fmt.Formatter for []byte or [n]byte types that have a custom string
+// representation. Use it like:
+//
+// type MyType [16]byte
+//
+// func (val MyType) String() string {
+// …
+// }
+//
+// func (val MyType) Format(f fmt.State, verb rune) {
+// util.FormatByteArrayStringer(val, val[:], f, verb)
+// }
+func FormatByteArrayStringer(
+ obj interface {
+ fmt.Stringer
+ fmt.Formatter
+ },
+ objBytes []byte,
+ f fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ if !f.Flag('#') {
+ FormatByteArrayStringer(obj, objBytes, f, 's') // as a string
+ } else {
+ byteStr := fmt.Sprintf("%#v", objBytes)
+ objType := fmt.Sprintf("%T", obj)
+ objStr := objType + strings.TrimPrefix(byteStr, "[]byte")
+ fmt.Fprintf(f, FmtStateString(f, 's'), objStr)
+ }
+ case 's', 'q': // string
+ fmt.Fprintf(f, FmtStateString(f, verb), obj.String())
+ default:
+ fmt.Fprintf(f, FmtStateString(f, verb), objBytes)
+ }
+}
diff --git a/pkg/util/fmt_test.go b/pkg/util/fmt_test.go
new file mode 100644
index 0000000..d2579d0
--- /dev/null
+++ b/pkg/util/fmt_test.go
@@ -0,0 +1,99 @@
+package util_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "lukeshu.com/btrfs-tools/pkg/util"
+)
+
+type FmtState struct {
+ MWidth int
+ MPrec int
+ MFlagMinus bool
+ MFlagPlus bool
+ MFlagSharp bool
+ MFlagSpace bool
+ MFlagZero bool
+}
+
+func (st FmtState) Width() (int, bool) {
+ if st.MWidth < 1 {
+ return 0, false
+ }
+ return st.MWidth, true
+}
+
+func (st FmtState) Precision() (int, bool) {
+ if st.MPrec < 1 {
+ return 0, false
+ }
+ return st.MPrec, true
+}
+
+func (st FmtState) Flag(b int) bool {
+ switch b {
+ case '-':
+ return st.MFlagMinus
+ case '+':
+ return st.MFlagPlus
+ case '#':
+ return st.MFlagSharp
+ case ' ':
+ return st.MFlagSpace
+ case '0':
+ return st.MFlagZero
+ }
+ return false
+}
+
+func (st FmtState) Write([]byte) (int, error) {
+ panic("not implemented")
+}
+
+func (dst *FmtState) Format(src fmt.State, verb rune) {
+ if width, ok := src.Width(); ok {
+ dst.MWidth = width
+ }
+ if prec, ok := src.Precision(); ok {
+ dst.MPrec = prec
+ }
+ dst.MFlagMinus = src.Flag('-')
+ dst.MFlagPlus = src.Flag('+')
+ dst.MFlagSharp = src.Flag('#')
+ dst.MFlagSpace = src.Flag(' ')
+ dst.MFlagZero = src.Flag('0')
+}
+
+// letters only? No 'p', 'T', or 'w'.
+const verbs = "abcdefghijklmnoqrstuvxyzABCDEFGHIJKLMNOPQRSUVWXYZ"
+
+func FuzzFmtStateString(f *testing.F) {
+ f.Fuzz(func(t *testing.T,
+ width, prec uint8,
+ flagMinus, flagPlus, flagSharp, flagSpace, flagZero bool,
+ verbIdx uint8,
+ ) {
+ if flagMinus {
+ flagZero = false
+ }
+ input := FmtState{
+ MWidth: int(width),
+ MPrec: int(prec),
+ MFlagMinus: flagMinus,
+ MFlagPlus: flagPlus,
+ MFlagSharp: flagSharp,
+ MFlagSpace: flagSpace,
+ MFlagZero: flagZero,
+ }
+ verb := rune(verbs[int(verbIdx)%len(verbs)])
+
+ t.Logf("(%#v, %c) => %q", input, verb, util.FmtStateString(input, verb))
+
+ var output FmtState
+ assert.Equal(t, "", fmt.Sprintf(util.FmtStateString(input, verb), &output))
+ assert.Equal(t, input, output)
+ })
+}