aboutsummaryrefslogtreecommitdiff
path: root/pkg/types/ascii/ascii.go
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/ascii.go
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/ascii.go')
-rw-r--r--pkg/types/ascii/ascii.go173
1 files changed, 173 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
+}