aboutsummaryrefslogtreecommitdiff
path: root/pkg/types/ascii
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@mullvad.net>2022-04-25 00:43:06 +0200
committerRasmus Dahlberg <rasmus@mullvad.net>2022-04-25 00:43:06 +0200
commit528a53f7f76f08af5902f4cfa8235380b3434ba0 (patch)
tree662b7834d5ce15627554e9307a4e00f7364fba11 /pkg/types/ascii
parent4fc0ff2ec2f48519ee245d6d7edee1921cb3b8bc (diff)
drafty types refactor with simple ascii packagergdd/sketch
types.go compiles but that is about it, here be dragons. Pushing so that we can get an idea of what this refactor would roughly look like.
Diffstat (limited to 'pkg/types/ascii')
-rw-r--r--pkg/types/ascii/ascii.go173
-rw-r--r--pkg/types/ascii/ascii_test.go290
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")
+ }
+}