diff options
Diffstat (limited to 'pkg/types/ascii')
-rw-r--r-- | pkg/types/ascii/ascii.go | 173 | ||||
-rw-r--r-- | pkg/types/ascii/ascii_test.go | 290 |
2 files changed, 463 insertions, 0 deletions
diff --git a/pkg/types/ascii/ascii.go b/pkg/types/ascii/ascii.go new file mode 100644 index 0000000..92ead4b --- /dev/null +++ b/pkg/types/ascii/ascii.go @@ -0,0 +1,173 @@ +// package ascii provides ASCII key-value (de)serialization, see ยง3: +// +// https://git.sigsum.org/sigsum/plain/doc/api.md +// +// Write key-value pairs to a buffer using the WritePair() method. +// +// Read key-value pairs from a buffer using the ReadPairs() method. It takes as +// input a function that parses the buffer using a map's Dequeue*() methods. +// +// XXX: add a usage example, until then see TestReadPairs(). +// +package ascii + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strconv" + + "git.sigsum.org/sigsum-go/pkg/hex" +) + +const ( + EndOfKey = "=" + EndOfValue = "\n" +) + +var ( + endOfKey = []byte(EndOfKey) + endOfValue = []byte(EndOfValue) +) + +// WritePair writes a key-value pair +func WritePair(w io.Writer, key, value string) error { + _, err := w.Write(bytes.Join([][]byte{[]byte(key), endOfKey, []byte(value), endOfValue}, nil)) + return err +} + +// ReadPairs parses key-value pairs strictly using the provided parse function +func ReadPairs(r io.Reader, parse func(*Map) error) error { + m, err := newMap(r) + if err != nil { + return err + } + if err := parse(&m); err != nil { + return err + } + return m.done() +} + +// Map is a map of ASCII key-value pairs. An ASCII key has a list of ASCII +// values. A value can be dequeued for a key as a certain type. Call Done() +// after dequeing all expected values to be strict about no redundant values. +type Map map[string][]string + +// NumValues returns the number of values for a given key. If the key does not +// exist, the number of values is per definition zero. +func (m *Map) NumValues(key string) uint64 { + values, ok := (*m)[key] + if !ok { + return 0 + } + return uint64(len(values)) +} + +// DequeueString dequeues a string value for a given key. +func (m *Map) DequeueString(key string, str *string) (err error) { + *str, err = m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + return nil +} + +// DequeueUint64 dequeues an uint64 value for a given key. +func (m *Map) DequeueUint64(key string, num *uint64) error { + v, err := m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + *num, err = strconv.ParseUint(v, 10, 64) + if err != nil { + return fmt.Errorf("invalid uint64: %w", err) + } + return nil +} + +// DequeueArray dequeues an array value for a given key +func (m *Map) DequeueArray(key string, arr []byte) error { + v, err := m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + b, err := hex.Deserialize(v) + if err != nil { + return fmt.Errorf("invalid array: %w", err) + } + if n := len(b); n != len(arr) { + return fmt.Errorf("invalid array size %d", n) + } + copy(arr, b) + return nil +} + +// dequeue dequeues a value for a given key +func (m *Map) dequeue(key string) (string, error) { + _, ok := (*m)[key] + if !ok { + return "", fmt.Errorf("missing key %q", key) + } + if len((*m)[key]) == 0 { + return "", fmt.Errorf("missing value for key %q", key) + } + + value := (*m)[key][0] + (*m)[key] = (*m)[key][1:] + return value, nil +} + +// done checks that there are no keys with remaining values +func (m *Map) done() error { + for k, v := range *m { + if len(v) != 0 { + return fmt.Errorf("remaining values for key %q", k) + } + } + return nil +} + +// newMap parses ASCII-encoded key-value pairs into a map +func newMap(r io.Reader) (m Map, err error) { + buf, err := ioutil.ReadAll(r) + if err != nil { + return m, fmt.Errorf("read: %w", err) + } + + b, err := trimEnd(buf) + if err != nil { + return m, fmt.Errorf("malformed input: %w", err) + } + + m = make(map[string][]string) + for i, kv := range bytes.Split(b, endOfValue) { + split := bytes.Split(kv, endOfKey) + if len(split) == 1 { + return m, fmt.Errorf("no key-value pair on line %d: %q", i+1, string(kv)) + } + + key := string(split[0]) + value := string(bytes.Join(split[1:], endOfKey)) + if _, ok := m[key]; !ok { + m[key] = make([]string, 0, 1) + } + m[key] = append(m[key], value) + } + + return m, nil +} + +// trimEnd ensures that we can range over the output of a split on endOfValue +// without the last itteration being an empty string. Note that it would not be +// correct to simply skip the last itteration. That line could me malformed. +func trimEnd(buf []byte) ([]byte, error) { + if len(buf) <= len(endOfValue) { + return nil, fmt.Errorf("buffer contains no key-value pair") + } + offset := len(buf) - len(endOfValue) + if !bytes.Equal(buf[offset:], endOfValue) { + return nil, fmt.Errorf("buffer must end with %q", EndOfValue) + } + return buf[:offset], nil +} diff --git a/pkg/types/ascii/ascii_test.go b/pkg/types/ascii/ascii_test.go new file mode 100644 index 0000000..d0d578b --- /dev/null +++ b/pkg/types/ascii/ascii_test.go @@ -0,0 +1,290 @@ +package ascii + +import ( + "bytes" + "fmt" + "io" + "reflect" + "testing" +) + +func TestWritePair(t *testing.T) { + key := "red" + value := "1" + want := "red=1\n" + + buf := bytes.NewBuffer(nil) + if err := WritePair(buf, key, value); err != nil { + t.Errorf("write pair: %v", err) + } + if got := string(buf.Bytes()); got != want { + t.Errorf("got key-value pair %q but wanted %q", got, want) + } +} + +func TestReadPairs(t *testing.T) { + type collection struct { + String string + Num uint64 + Array [2]byte + Arrays [][2]byte + } + + var c collection + parser := func(m *Map) error { + if err := m.DequeueString("string", &c.String); err != nil { + return fmt.Errorf("string: %w", err) + } + if err := m.DequeueUint64("num", &c.Num); err != nil { + return fmt.Errorf("num: %w", err) + } + if err := m.DequeueArray("array", c.Array[:]); err != nil { + return fmt.Errorf("array: %w", err) + } + + n := m.NumValues("arrays") + if n == 0 { + return fmt.Errorf("arrays: empty") + } + c.Arrays = make([][2]byte, 0, n) + for i := uint64(0); i < n; i++ { + var array [2]byte + if err := m.DequeueArray("arrays", array[:]); err != nil { + return fmt.Errorf("%d: arrays: %w", i+1, err) + } + c.Arrays = append(c.Arrays, array) + } + return nil + } + + for _, table := range []struct { + desc string + input io.Reader + want *collection + }{ + { + desc: "invalid: cannot parse into map", + input: bytes.NewBufferString("string=a"), + }, + { + desc: "invalid: malformed value", + input: bytes.NewBufferString("string=a\nnum=a\narray=0101\narrays=0101\narrays=ffff\n"), + }, + { + desc: "invalid: remaining value", + input: bytes.NewBufferString("string=a\nnum=1\narray=0101\narrays=0101\narrays=ffff\nhello=abc\n"), + }, + { + desc: "valid", + input: bytes.NewBufferString("string=a\nnum=1\narray=0101\narrays=0101\narrays=ffff\n"), + want: &collection{ + String: "a", + Num: 1, + Array: [2]byte{1, 1}, + Arrays: [][2]byte{ + [2]byte{1, 1}, + [2]byte{255, 255}, + }, + }, + }, + } { + c = collection{} + err := ReadPairs(table.input, parser) + if got, want := err != nil, table.want == nil; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := c, *table.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got collection\n%+v\nbut wanted\n%+v", table.desc, got, want) + } + } +} + +func TestNewMap(t *testing.T) { + for _, table := range []struct { + desc string + input io.Reader + want Map + }{ + { + desc: "invalid: trim: no key-value pairs", + input: bytes.NewBuffer(nil), + }, + { + desc: "invalid: trim: ending", + input: bytes.NewBufferString("red=1\nblue=2"), + }, + { + desc: "invalid: missing key-value pair on line", + input: bytes.NewBufferString("red=1\n\nblue=2\n"), + }, + { + desc: "valid", + input: bytes.NewBufferString("red=1\nblue=1\nblue=2\ngreen=1\nred==2\n"), + want: map[string][]string{ + "red": []string{"1", "=2"}, + "blue": []string{"1", "2"}, + "green": []string{"1"}, + }, + }, + } { + m, err := newMap(table.input) + if got, want := err != nil, table.want == nil; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := m, table.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got map\n%v\nbut wanted\n%v", table.desc, got, want) + } + } +} + +func TestDone(t *testing.T) { + for _, table := range []struct { + desc string + input Map + wantOK bool + }{ + { + desc: "valid: keys with no values", + input: map[string][]string{ + "red": []string{"1"}, + "blue": []string{}, + }, + }, + { + desc: "valid: empty", + input: map[string][]string{}, + wantOK: true, + }, + { + desc: "valid: keys with no values", + input: map[string][]string{ + "red": []string{}, + "blue": []string{}, + }, + wantOK: true, + }, + } { + err := table.input.done() + if got, want := err != nil, !table.wantOK; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + } +} + +func TestNumValues(t *testing.T) { + var m Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"a", "bc", "def"}, + } + if got, want := m.NumValues("orange"), uint64(0); got != want { + t.Errorf("orange: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("red"), uint64(0); got != want { + t.Errorf("red: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("blue"), uint64(1); got != want { + t.Errorf("blue: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("green"), uint64(3); got != want { + t.Errorf("green: got %d values but wanted %d", got, want) + } +} + +func TestDequeue(t *testing.T) { + var first Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"a", "bc", "def"}, + } + if _, err := first.dequeue("orange"); err == nil { + t.Errorf("orange: expected dequeue error but got none") + } + if _, err := first.dequeue("red"); err == nil { + t.Errorf("red: expected dequeue error but got none") + } + + str, err := first.dequeue("green") + if err != nil { + t.Errorf("green: expected dequeue to succeed but got error: %v", err) + } + if got, want := str, "a"; got != want { + t.Errorf("green: got value %q but wanted %q", got, want) + } + + var second Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"bc", "def"}, + } + if got, want := second, first; !reflect.DeepEqual(got, want) { + t.Errorf("got map\n%v\nbut wanted\n%v", got, want) + } +} + +func TestDequeueString(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"1"}, + } + + var str string + if err := first.DequeueString("blue", &str); err != nil { + t.Errorf("expected dequeue ok but got error: %v", err) + return + } + if got, want := str, "1"; got != want { + t.Errorf("got string %q but wanted %q", got, want) + } + if err := first.DequeueString("blue", &str); err == nil { + t.Errorf("expected dequeue error but got none") + } +} + +func TestDequeueUint64(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"a", "1"}, + } + + var num uint64 + if err := first.DequeueUint64("blue", &num); err == nil { + t.Errorf("expected parse error but got none") + } + if err := first.DequeueUint64("blue", &num); err != nil { + t.Errorf("expected dequeue success but got error: %v", err) + } + if got, want := num, uint64(1); got != want { + t.Errorf("got number %d but wanted %d", got, want) + } + if err := first.DequeueUint64("blue", &num); err == nil { + t.Errorf("expected dequeue error but got none") + } +} + +func TestDequeueArray(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"00FF", "0001ff", "00ff"}, + } + + var arr [2]byte + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected parse error but got none (bad hex)") + } + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected parse error but got none (bad length)") + } + if err := first.DequeueArray("blue", arr[:]); err != nil { + t.Errorf("expected dequeue success but got error: %v", err) + } + if got, want := arr, [2]byte{0, 255}; got != want { + t.Errorf("got array %v but wanted %v", got, want) + } + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected dequeue error but got none") + } +} |