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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
package commit
import (
"bytes"
"errors"
"fmt"
"lindenii.org/go/furgit/object/id"
"lindenii.org/go/furgit/object/signature"
)
// ErrInvalidCommit indicates a malformed commit object.
var ErrInvalidCommit = errors.New("object/commit: invalid commit")
// Parse decodes a commit object body.
func Parse(body []byte, objectFormat id.ObjectFormat) (*Commit, error) {
c := new(Commit)
i := 0
state := parseStateTree
sawHeaderEnd := false
for i < len(body) {
lineStart := i
rel := bytes.IndexByte(body[i:], '\n')
if rel < 0 {
return nil, fmt.Errorf("%w: unterminated header line at offset %d", ErrInvalidCommit, lineStart)
}
line := body[i : i+rel]
i += rel + 1
if len(line) == 0 {
sawHeaderEnd = true
break
}
key, value, found := bytes.Cut(line, []byte{' '})
if !found {
return nil, fmt.Errorf("%w: header line at offset %d has no ' ' separator", ErrInvalidCommit, lineStart)
}
switch string(key) {
case "tree":
if state != parseStateTree {
return nil, fmt.Errorf("%w: unexpected tree header at offset %d", ErrInvalidCommit, lineStart)
}
id, err := objectFormat.FromString(string(value))
if err != nil {
return nil, fmt.Errorf("%w: tree: %w", ErrInvalidCommit, err)
}
c.Tree = id
state = parseStateParentOrAuthor
case "parent":
if state != parseStateParentOrAuthor {
return nil, fmt.Errorf("%w: unexpected parent header at offset %d", ErrInvalidCommit, lineStart)
}
id, err := objectFormat.FromString(string(value))
if err != nil {
return nil, fmt.Errorf("%w: parent: %w", ErrInvalidCommit, err)
}
c.Parents = append(c.Parents, id)
case "author":
if state != parseStateParentOrAuthor {
return nil, fmt.Errorf("%w: unexpected author header at offset %d", ErrInvalidCommit, lineStart)
}
idt, err := signature.Parse(value)
if err != nil {
return nil, fmt.Errorf("%w: author: %w", ErrInvalidCommit, err)
}
c.Author = *idt
state = parseStateCommitter
case "committer":
if state != parseStateCommitter {
return nil, fmt.Errorf("%w: unexpected committer header at offset %d", ErrInvalidCommit, lineStart)
}
idt, err := signature.Parse(value)
if err != nil {
return nil, fmt.Errorf("%w: committer: %w", ErrInvalidCommit, err)
}
c.Committer = *idt
state = parseStateExtra
case "change-id":
if state != parseStateExtra {
return nil, fmt.Errorf("%w: unexpected change-id header at offset %d", ErrInvalidCommit, lineStart)
}
c.ChangeID = string(value)
case "gpgsig", "gpgsig-sha256":
if state != parseStateExtra {
return nil, fmt.Errorf("%w: unexpected %s header at offset %d", ErrInvalidCommit, key, lineStart)
}
for i < len(body) {
nextRel := bytes.IndexByte(body[i:], '\n')
if nextRel < 0 {
return nil, fmt.Errorf("%w: unterminated signature header at offset %d", ErrInvalidCommit, i)
}
if body[i] != ' ' {
break
}
i += nextRel + 1
}
default:
if state != parseStateExtra {
return nil, fmt.Errorf("%w: unexpected %s header at offset %d", ErrInvalidCommit, key, lineStart)
}
c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{
Key: string(key),
Value: append([]byte(nil), value...),
})
}
}
if !sawHeaderEnd {
return nil, fmt.Errorf("%w: missing blank line before message", ErrInvalidCommit)
}
switch state {
case parseStateTree:
return nil, fmt.Errorf("%w: missing tree header", ErrInvalidCommit)
case parseStateParentOrAuthor:
return nil, fmt.Errorf("%w: missing author header", ErrInvalidCommit)
case parseStateCommitter:
return nil, fmt.Errorf("%w: missing committer header", ErrInvalidCommit)
case parseStateExtra:
default:
panic("unreachable parse state")
}
c.Message = append([]byte(nil), body[i:]...)
return c, nil
}
type parseState uint8
const (
parseStateTree parseState = iota
parseStateParentOrAuthor
parseStateCommitter
parseStateExtra
)
|