aboutsummaryrefslogtreecommitdiff
path: root/pkg/types/ascii
diff options
context:
space:
mode:
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")
+ }
+}