summaryrefslogtreecommitdiffstats
path: root/sumdb/internal/tlog/note.go
blob: 65c71644baf6ad7d2bff69f1da6ce0a7f7b77383 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tlog

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"
)

// A Tree is a tree description, to be signed by a go.sum database server.
type Tree struct {
	N    int64
	Hash Hash
}

// FormatTree formats a tree description for inclusion in a note.
//
// The encoded form is three lines, each ending in a newline (U+000A):
//
//	go.sum database tree
//	N
//	Hash
//
// where N is in decimal and Hash is in base64.
//
// A future backwards-compatible encoding may add additional lines,
// which the parser can ignore.
// A future backwards-incompatible encoding would use a different
// first line (for example, "go.sum database tree v2").
func FormatTree(tree Tree) []byte {
	return []byte(fmt.Sprintf("go.sum database tree\n%d\n%s\n", tree.N, tree.Hash))
}

var errMalformedTree = errors.New("malformed tree note")
var treePrefix = []byte("go.sum database tree\n")

// ParseTree parses a tree root description.
func ParseTree(text []byte) (tree Tree, err error) {
	// The message looks like:
	//
	//	go.sum database tree
	//	2
	//	nND/nri/U0xuHUrYSy0HtMeal2vzD9V4k/BO79C+QeI=
	//
	// For forwards compatibility, extra text lines after the encoding are ignored.
	if !bytes.HasPrefix(text, treePrefix) || bytes.Count(text, []byte("\n")) < 3 || len(text) > 1e6 {
		return Tree{}, errMalformedTree
	}

	lines := strings.SplitN(string(text), "\n", 4)
	n, err := strconv.ParseInt(lines[1], 10, 64)
	if err != nil || n < 0 || lines[1] != strconv.FormatInt(n, 10) {
		return Tree{}, errMalformedTree
	}

	h, err := base64.StdEncoding.DecodeString(lines[2])
	if err != nil || len(h) != HashSize {
		return Tree{}, errMalformedTree
	}

	var hash Hash
	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
}