From 0a5c83ece140ab81a8111756cb6a80a4687036ca Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 7 Jan 2020 19:11:34 +0000 Subject: [PATCH] lib/encoder: add string rendering and parsing for encodings --- lib/encoder/encoder.go | 110 ++++++++++++++++++++++++++++++++++++ lib/encoder/encoder_test.go | 56 ++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/lib/encoder/encoder.go b/lib/encoder/encoder.go index 8d84ac2eb..ecb2f8c8d 100644 --- a/lib/encoder/encoder.go +++ b/lib/encoder/encoder.go @@ -16,6 +16,7 @@ import ( "bytes" "fmt" "io" + "sort" "strconv" "strings" "unicode/utf8" @@ -32,6 +33,7 @@ const ( ) // NB keep the tests in fstests/fstests/fstests.go FsEncoding up to date with this +// NB keep the aliases up to date below also // Possible flags for the MultiEncoder const ( @@ -99,6 +101,114 @@ type Encoder interface { // character classes type MultiEncoder uint +// Aliases maps encodings to names and vice versa +var ( + encodingToName = map[MultiEncoder]string{} + nameToEncoding = map[string]MultiEncoder{} +) + +// alias adds an alias for MultiEncoder.String() and MultiEncoder.Set() +func alias(name string, mask MultiEncoder) { + nameToEncoding[name] = mask + // don't overwrite existing reverse translations + if _, ok := encodingToName[mask]; !ok { + encodingToName[mask] = name + } +} + +func init() { + alias("None", EncodeZero) + alias("Slash", EncodeSlash) + alias("LtGt", EncodeLtGt) + alias("DoubleQuote", EncodeDoubleQuote) + alias("SingleQuote", EncodeSingleQuote) + alias("BackQuote", EncodeBackQuote) + alias("Dollar", EncodeDollar) + alias("Colon", EncodeColon) + alias("Question", EncodeQuestion) + alias("Asterisk", EncodeAsterisk) + alias("Pipe", EncodePipe) + alias("Hash", EncodeHash) + alias("Percent", EncodePercent) + alias("BackSlash", EncodeBackSlash) + alias("CrLf", EncodeCrLf) + alias("Del", EncodeDel) + alias("Ctl", EncodeCtl) + alias("LeftSpace", EncodeLeftSpace) + alias("LeftPeriod", EncodeLeftPeriod) + alias("LeftTilde", EncodeLeftTilde) + alias("LeftCrLfHtVt", EncodeLeftCrLfHtVt) + alias("RightSpace", EncodeRightSpace) + alias("RightPeriod", EncodeRightPeriod) + alias("RightCrLfHtVt", EncodeRightCrLfHtVt) + alias("InvalidUtf8", EncodeInvalidUtf8) + alias("Dot", EncodeDot) +} + +// validStrings returns all the valid MultiEncoder strings +func validStrings() string { + var out []string + for k := range nameToEncoding { + out = append(out, k) + } + sort.Strings(out) + return strings.Join(out, ", ") +} + +// String converts the MultiEncoder into text +func (mask MultiEncoder) String() string { + // See if there is an exact translation - if so return that + if name, ok := encodingToName[mask]; ok { + return name + } + var out []string + // Otherwise decompose bit by bit + for bit := MultiEncoder(1); bit != 0; bit *= 2 { + if (mask & bit) != 0 { + if name, ok := encodingToName[bit]; ok { + out = append(out, name) + } else { + out = append(out, fmt.Sprintf("0x%X", uint(bit))) + } + } + } + return strings.Join(out, ",") +} + +// Set converts a string into a MultiEncoder +func (mask *MultiEncoder) Set(in string) error { + var out MultiEncoder + parts := strings.Split(in, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if bits, ok := nameToEncoding[part]; ok { + out |= bits + } else { + i, err := strconv.ParseInt(part, 0, 64) + if err != nil { + return fmt.Errorf("bad encoding %q: possible values are: %s", part, validStrings()) + } + out |= MultiEncoder(i) + } + } + *mask = out + return nil +} + +// Type returns a textual type of the MultiEncoder to satsify the pflag.Value interface +func (mask MultiEncoder) Type() string { + return "Encoding" +} + +// Scan implements the fmt.Scanner interface +func (mask *MultiEncoder) Scan(s fmt.ScanState, ch rune) error { + token, err := s.Token(true, nil) + if err != nil { + return err + } + return mask.Set(string(token)) +} + // Encode takes a raw name and substitutes any reserved characters and // patterns in it func (mask MultiEncoder) Encode(in string) string { diff --git a/lib/encoder/encoder_test.go b/lib/encoder/encoder_test.go index 487398ab9..b041aaca4 100644 --- a/lib/encoder/encoder_test.go +++ b/lib/encoder/encoder_test.go @@ -1,12 +1,68 @@ package encoder import ( + "fmt" "regexp" "strconv" "strings" "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" ) +// Check it satisfies the interfaces +var ( + _ pflag.Value = (*MultiEncoder)(nil) + _ fmt.Scanner = (*MultiEncoder)(nil) +) + +func TestEncodeString(t *testing.T) { + for _, test := range []struct { + mask uint + want string + }{ + {0, "None"}, + {EncodeZero, "None"}, + {EncodeDoubleQuote, "DoubleQuote"}, + {EncodeDot, "Dot"}, + {EncodeWin, "LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe"}, + {EncodeHashPercent, "Hash,Percent"}, + {EncodeSlash | EncodeDollar | EncodeColon, "Slash,Dollar,Colon"}, + {EncodeSlash | (1 << 31), "Slash,0x80000000"}, + } { + got := MultiEncoder(test.mask).String() + assert.Equal(t, test.want, got) + } + +} + +func TestEncodeSet(t *testing.T) { + for _, test := range []struct { + in string + want uint + wantErr bool + }{ + {"", 0, true}, + {"None", 0, false}, + {"None", EncodeZero, false}, + {"DoubleQuote", EncodeDoubleQuote, false}, + {"Dot", EncodeDot, false}, + {"LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe", EncodeWin, false}, + {"Hash,Percent", EncodeHashPercent, false}, + {"Slash,Dollar,Colon", EncodeSlash | EncodeDollar | EncodeColon, false}, + {"Slash,0x80000000", EncodeSlash | (1 << 31), false}, + {"Blerp", 0, true}, + {"0xFGFFF", 0, true}, + } { + var got MultiEncoder + err := got.Set(test.in) + assert.Equal(t, test.wantErr, err != nil, err) + assert.Equal(t, MultiEncoder(test.want), got, test.in) + } + +} + type testCase struct { mask uint in string