-
-
Notifications
You must be signed in to change notification settings - Fork 640
trees/checkpoint: Add checkpoint.Checkpoint #8830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| package checkpoint | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "errors" | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
| "unicode/utf8" | ||
|
|
||
| "golang.org/x/mod/sumdb/note" | ||
| "golang.org/x/mod/sumdb/tlog" | ||
| ) | ||
|
|
||
| // Checkpoint represents a tlog-checkpoint note body. | ||
| // | ||
| // https://c2sp.org/tlog-checkpoint | ||
| type Checkpoint struct { | ||
| Origin string | ||
| Tree tlog.Tree | ||
| Extensions []string | ||
| } | ||
|
|
||
| // String returns the note body of the checkpoint. Unlike Marshal, it does not | ||
| // validate the checkpoint fields. Call Marshal to validate the fields before | ||
| // serializing. | ||
| // | ||
| // https://c2sp.org/tlog-checkpoint | ||
| func (c Checkpoint) String() string { | ||
| var b strings.Builder | ||
| fmt.Fprintf(&b, "%s\n%d\n%s\n", c.Origin, c.Tree.N, c.Tree.Hash) | ||
| for _, ext := range c.Extensions { | ||
| b.WriteString(ext) | ||
| b.WriteByte('\n') | ||
| } | ||
| return b.String() | ||
| } | ||
|
|
||
| // validNoteLine reports whether s is a valid signed-note line: UTF-8 with no | ||
| // control character below U+0020. | ||
| func validNoteLine(s string) bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are checkpoints the only kind of signed note we're going to be producing or parsing? If not, then this and other helpers should probably be in a separate |
||
| if !utf8.ValidString(s) { | ||
| return false | ||
| } | ||
| for _, r := range s { | ||
| if r < 0x20 { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // Marshal returns the note body, first checking the checkpoint against the | ||
| // tlog-checkpoint rules. It returns an error if the checkpoint is invalid. | ||
| // | ||
| // - https://c2sp.org/tlog-checkpoint | ||
| // - https://c2sp.org/signed-note | ||
| func (c Checkpoint) Marshal() (string, error) { | ||
| if c.Origin == "" { | ||
| return "", errors.New("empty checkpoint origin") | ||
| } | ||
| if !validNoteLine(c.Origin) { | ||
| return "", errors.New("checkpoint origin contains a control character or invalid UTF-8") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There'd be no need for this verbose "or" error line if |
||
| } | ||
| if c.Tree.N < 0 { | ||
| return "", fmt.Errorf("negative checkpoint tree size %d", c.Tree.N) | ||
| } | ||
| for _, ext := range c.Extensions { | ||
| if ext == "" { | ||
| return "", errors.New("empty checkpoint extension line") | ||
| } | ||
| if !validNoteLine(ext) { | ||
| return "", errors.New("checkpoint extension line contains a control character or invalid UTF-8") | ||
| } | ||
| } | ||
| return c.String(), nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I love this relationship between String() and Marshal(). I like having both methods! But: How would you feel about having .String() instead return a purposefully non-compliant single-line result, specifically for internal debugging use, while .Marshal() constructs the actual format? |
||
| } | ||
|
|
||
| // Parse parses a checkpoint note body. It must not have any signature lines. | ||
| // For a signed note, use Open. | ||
| // | ||
| // - https://c2sp.org/tlog-checkpoint | ||
| // - https://c2sp.org/signed-note | ||
| func Parse(text string) (Checkpoint, error) { | ||
| if !strings.HasSuffix(text, "\n") { | ||
| return Checkpoint{}, errors.New("checkpoint does not end in newline") | ||
| } | ||
| lines := strings.Split(strings.TrimSuffix(text, "\n"), "\n") | ||
| if len(lines) < 3 { | ||
| return Checkpoint{}, errors.New("checkpoint has too few lines") | ||
| } | ||
|
|
||
| origin := lines[0] | ||
| if origin == "" { | ||
| return Checkpoint{}, errors.New("empty checkpoint origin") | ||
| } | ||
| if !validNoteLine(origin) { | ||
| return Checkpoint{}, errors.New("checkpoint origin contains a control character or invalid UTF-8") | ||
| } | ||
|
|
||
| size, err := strconv.ParseInt(lines[1], 10, 64) | ||
| if err != nil || size < 0 || strconv.FormatInt(size, 10) != lines[1] { | ||
| return Checkpoint{}, errors.New("malformed checkpoint tree size") | ||
| } | ||
|
|
||
| hashBytes, err := base64.StdEncoding.DecodeString(lines[2]) | ||
| if err != nil || len(hashBytes) != tlog.HashSize || base64.StdEncoding.EncodeToString(hashBytes) != lines[2] { | ||
| return Checkpoint{}, errors.New("malformed checkpoint root hash") | ||
| } | ||
| var hash tlog.Hash | ||
| copy(hash[:], hashBytes) | ||
|
|
||
| extensions := lines[3:] | ||
| for _, ext := range extensions { | ||
| if ext == "" { | ||
| return Checkpoint{}, errors.New("empty checkpoint extension line") | ||
| } | ||
| if !validNoteLine(ext) { | ||
| return Checkpoint{}, errors.New("checkpoint extension line contains a control character or invalid UTF-8") | ||
| } | ||
| } | ||
| return Checkpoint{Origin: origin, Tree: tlog.Tree{N: size, Hash: hash}, Extensions: extensions}, nil | ||
| } | ||
|
|
||
| // Open opens a signed checkpoint note and parses its body. An error is returned | ||
| // if the note is not valid or the signature is not verified by one of the | ||
| // verifiers. | ||
| // | ||
| // https://c2sp.org/signed-note | ||
| func Open(signedNote []byte, verifiers note.Verifiers) (Checkpoint, *note.Note, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Who are the prospective callers of this function? Why do they want access to the underlying Note, and not just the Checkpoint? They're just different in-memory views onto the same text, right? |
||
| n, err := note.Open(signedNote, verifiers) | ||
| if err != nil { | ||
| return Checkpoint{}, nil, err | ||
| } | ||
| c, err := Parse(n.Text) | ||
| if err != nil { | ||
| return Checkpoint{}, nil, err | ||
| } | ||
| return c, n, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Checkpoint type contains both a slice (Extensions) and a potentially-large array (Tree.Hash), so we should probably not be passing Checkpoints around by value. These should be pointer receivers, and Parse should return a
*Checkpoint.