From 468eca4684d9b51d00e18cb129bebf528a844035 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sat, 11 Jun 2022 13:52:02 -0600 Subject: Improve formatting of CSums and UUIDs --- pkg/btrfs/crc32c.go | 9 +++- pkg/btrfs/crc32c_test.go | 35 +++++++++++++++ pkg/btrfs/internal/uuid.go | 46 +++++++++++++++++++ pkg/btrfs/internal/uuid_test.go | 63 ++++++++++++++++++++++++++ pkg/util/fmt.go | 67 ++++++++++++++++++++++++++++ pkg/util/fmt_test.go | 99 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 pkg/btrfs/crc32c_test.go create mode 100644 pkg/btrfs/internal/uuid_test.go create mode 100644 pkg/util/fmt.go create mode 100644 pkg/util/fmt_test.go (limited to 'pkg') 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) + }) +} -- cgit v1.2.3-2-g168b