notary/internal/tlog: add record formatting and parsing
Move this out of noteweb and into tlog to be near the
tree formatting and parsing, and to allow its use from
other clients/servers.
Change-Id: I7da0f345f81e1204cb343d951e9ee57b72da6fea
Reviewed-on: https://go-review.googlesource.com/c/exp/+/172957
Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/notary/internal/tlog/note.go b/notary/internal/tlog/note.go
index 2212016..65c7164 100644
--- a/notary/internal/tlog/note.go
+++ b/notary/internal/tlog/note.go
@@ -11,6 +11,7 @@
"fmt"
"strconv"
"strings"
+ "unicode/utf8"
)
// A Tree is a tree description, to be signed by a go.sum database server.
@@ -68,3 +69,67 @@
copy(hash[:], h)
return Tree{n, hash}, nil
}
+
+var errMalformedRecord = errors.New("malformed record data")
+
+// FormatRecord formats a record for serving to a client
+// in a lookup response or data tile.
+//
+// The encoded form is the record ID as a single number,
+// then the text of the record, and then a terminating blank line.
+// Record text must be valid UTF-8 and must not contain any ASCII control
+// characters (those below U+0020) other than newline (U+000A).
+// It must end in a terminating newline and not contain any blank lines.
+func FormatRecord(id int64, text []byte) (msg []byte, err error) {
+ if !isValidRecordText(text) {
+ return nil, errMalformedRecord
+ }
+ msg = []byte(fmt.Sprintf("%d\n", id))
+ msg = append(msg, text...)
+ msg = append(msg, '\n')
+ return msg, nil
+}
+
+// isValidRecordText reports whether text is syntactically valid record text.
+func isValidRecordText(text []byte) bool {
+ var last rune
+ for i := 0; i < len(text); {
+ r, size := utf8.DecodeRune(text[i:])
+ if r < 0x20 && r != '\n' || r == utf8.RuneError && size == 1 || last == '\n' && r == '\n' {
+ return false
+ }
+ i += size
+ last = r
+ }
+ if last != '\n' {
+ return false
+ }
+ return true
+}
+
+// ParseRecord parses a record description at the start of text,
+// stopping immediately after the terminating blank line.
+// It returns the record id, the record text, and the remainder of text.
+func ParseRecord(msg []byte) (id int64, text, rest []byte, err error) {
+ // Leading record id.
+ i := bytes.IndexByte(msg, '\n')
+ if i < 0 {
+ return 0, nil, nil, errMalformedRecord
+ }
+ id, err = strconv.ParseInt(string(msg[:i]), 10, 64)
+ if err != nil {
+ return 0, nil, nil, errMalformedRecord
+ }
+ msg = msg[i+1:]
+
+ // Record text.
+ i = bytes.Index(msg, []byte("\n\n"))
+ if i < 0 {
+ return 0, nil, nil, errMalformedRecord
+ }
+ text, rest = msg[:i+1], msg[i+2:]
+ if !isValidRecordText(text) {
+ return 0, nil, nil, errMalformedRecord
+ }
+ return id, text, rest, nil
+}
diff --git a/notary/internal/tlog/note_test.go b/notary/internal/tlog/note_test.go
index 78061be..a32d6d2 100644
--- a/notary/internal/tlog/note_test.go
+++ b/notary/internal/tlog/note_test.go
@@ -54,3 +54,64 @@
}
}
}
+
+func TestFormatRecord(t *testing.T) {
+ id := int64(123456789012)
+ text := "hello, world\n"
+ golden := "123456789012\nhello, world\n\n"
+ msg, err := FormatRecord(id, []byte(text))
+ if err != nil {
+ t.Fatalf("FormatRecord: %v", err)
+ }
+ if string(msg) != golden {
+ t.Fatalf("FormatRecord(...) = %q, want %q", msg, golden)
+ }
+
+ var badTexts = []string{
+ "",
+ "hello\nworld",
+ "hello\n\nworld\n",
+ "hello\x01world\n",
+ }
+ for _, bad := range badTexts {
+ msg, err := FormatRecord(id, []byte(bad))
+ if err == nil {
+ t.Errorf("FormatRecord(id, %q) = %q, want error", bad, msg)
+ }
+ }
+}
+
+func TestParseRecord(t *testing.T) {
+ in := "123456789012\nhello, world\n\njunk on end\x01\xff"
+ goldID := int64(123456789012)
+ goldText := "hello, world\n"
+ goldRest := "junk on end\x01\xff"
+ id, text, rest, err := ParseRecord([]byte(in))
+ if id != goldID || string(text) != goldText || string(rest) != goldRest || err != nil {
+ t.Fatalf("ParseRecord(%q) = %d, %q, %q, %v, want %d, %q, %q, nil", in, id, text, rest, err, goldID, goldText, goldRest)
+ }
+
+ in = "123456789012\nhello, world\n\n"
+ id, text, rest, err = ParseRecord([]byte(in))
+ if id != goldID || string(text) != goldText || len(rest) != 0 || err != nil {
+ t.Fatalf("ParseRecord(%q) = %d, %q, %q, %v, want %d, %q, %q, nil", in, id, text, rest, err, goldID, goldText, "")
+ }
+ if rest == nil {
+ t.Fatalf("ParseRecord(%q): rest = []byte(nil), want []byte{}", in)
+ }
+
+ // Check invalid records.
+ var badRecords = []string{
+ "not-" + in,
+ "123\nhello\x01world\n\n",
+ "123\nhello\xffworld\n\n",
+ "123\nhello world\n",
+ "0x123\nhello world\n\n",
+ }
+ for _, bad := range badRecords {
+ id, text, rest, err := ParseRecord([]byte(bad))
+ if err == nil {
+ t.Fatalf("ParseRecord(%q) = %d, %q, %q, nil, want error", in, id, text, rest)
+ }
+ }
+}