···11+package signedtag
22+33+import (
44+ "bytes"
55+ "slices"
66+77+ objectid "codeberg.org/lindenii/furgit/object/id"
88+)
99+1010+var signatureBeginLines = [][]byte{ //nolint:gochecknoglobals
1111+ []byte("-----BEGIN PGP SIGNATURE-----"),
1212+ []byte("-----BEGIN PGP MESSAGE-----"),
1313+ []byte("-----BEGIN SSH SIGNATURE-----"),
1414+ []byte("-----BEGIN SIGNED MESSAGE-----"),
1515+}
1616+1717+// Parse parses one raw tag object body for signature extraction.
1818+//
1919+// Git stores the signature for storageAlgo as an in-body ASCII-armored
2020+// trailer, and may store additional signatures for other algorithms in
2121+// gpgsig* headers.
2222+//
2323+// The returned Tag remains valid only while body remains unchanged.
2424+//
2525+// Labels: Deps-Borrowed, Life-Parent.
2626+func Parse(body []byte, storageAlgo objectid.Algorithm) (*Tag, error) {
2727+ tag := &Tag{
2828+ body: body,
2929+ signatures: make(map[objectid.Algorithm][]byteRange),
3030+ }
3131+3232+ signatureStart := len(body)
3333+ for i := 0; i < len(body); {
3434+ lineStart := i
3535+ rel := bytes.IndexByte(body[i:], '\n')
3636+ next := len(body)
3737+3838+ lineEnd := len(body)
3939+ if rel >= 0 {
4040+ lineEnd = i + rel
4141+ next = lineEnd + 1
4242+ }
4343+4444+ line := body[lineStart:lineEnd]
4545+ if slices.ContainsFunc(signatureBeginLines, func(begin []byte) bool {
4646+ return bytes.HasPrefix(line, begin)
4747+ }) {
4848+ signatureStart = lineStart
4949+ }
5050+5151+ i = next
5252+ }
5353+5454+ payloadStart := 0
5555+ payloadEnd := signatureStart
5656+ if signatureStart == len(body) {
5757+ payloadEnd = len(body)
5858+ }
5959+6060+ for i := 0; i < payloadEnd; {
6161+ lineStart := i
6262+ rel := bytes.IndexByte(body[i:payloadEnd], '\n')
6363+ next := payloadEnd
6464+6565+ lineEnd := payloadEnd
6666+ if rel >= 0 {
6767+ lineEnd = i + rel
6868+ next = lineEnd + 1
6969+ }
7070+7171+ line := body[lineStart:lineEnd]
7272+ i = next
7373+7474+ if len(line) == 0 {
7575+ break
7676+ }
7777+7878+ if line[0] == ' ' {
7979+ continue
8080+ }
8181+8282+ key, valueStart, found := bytes.Cut(line, []byte{' '})
8383+ if !found {
8484+ continue
8585+ }
8686+8787+ algo, ok := objectid.ParseSignatureHeaderName(string(key))
8888+ if !ok {
8989+ continue
9090+ }
9191+9292+ tag.appendPayloadRange(payloadStart, lineStart)
9393+ tag.signatures[algo] = append(tag.signatures[algo], byteRange{
9494+ start: lineEnd - len(valueStart),
9595+ end: next,
9696+ })
9797+9898+ for i < payloadEnd {
9999+ rel := bytes.IndexByte(body[i:payloadEnd], '\n')
100100+ next = payloadEnd
101101+102102+ lineEnd = payloadEnd
103103+ if rel >= 0 {
104104+ lineEnd = i + rel
105105+ next = lineEnd + 1
106106+ }
107107+108108+ cont := body[i:lineEnd]
109109+ if len(cont) == 0 || cont[0] != ' ' {
110110+ break
111111+ }
112112+113113+ tag.signatures[algo] = append(tag.signatures[algo], byteRange{
114114+ start: i + 1,
115115+ end: next,
116116+ })
117117+118118+ i = next
119119+ }
120120+121121+ payloadStart = i
122122+ }
123123+124124+ tag.appendPayloadRange(payloadStart, payloadEnd)
125125+ if signatureStart != len(body) {
126126+ tag.signatures[storageAlgo] = append(tag.signatures[storageAlgo], byteRange{
127127+ start: signatureStart,
128128+ end: len(body),
129129+ })
130130+ }
131131+132132+ return tag, nil
133133+}
134134+135135+func (tag *Tag) appendPayloadRange(start, end int) {
136136+ if start >= end {
137137+ return
138138+ }
139139+140140+ tag.payload = append(tag.payload, byteRange{start: start, end: end})
141141+}
+11
object/signed/tag/payload_append.go
···11+package signedtag
22+33+// AppendPayload appends the tag verification payload to dst, omitting all
44+// embedded signatures.
55+func (tag *Tag) AppendPayload(dst []byte) []byte {
66+ for _, part := range tag.payload {
77+ dst = append(dst, tag.body[part.start:part.end]...)
88+ }
99+1010+ return dst
1111+}
+16
object/signed/tag/signature_algorithms.go
···11+package signedtag
22+33+import objectid "codeberg.org/lindenii/furgit/object/id"
44+55+// Algorithms returns the algorithms for which the tag carries signatures.
66+func (tag *Tag) Algorithms() []objectid.Algorithm {
77+ var algorithms []objectid.Algorithm
88+99+ for _, algo := range objectid.SupportedAlgorithms() {
1010+ if _, ok := tag.signatures[algo]; ok {
1111+ algorithms = append(algorithms, algo)
1212+ }
1313+ }
1414+1515+ return algorithms
1616+}
+17
object/signed/tag/signature_append.go
···11+package signedtag
22+33+import objectid "codeberg.org/lindenii/furgit/object/id"
44+55+// AppendSignature appends the signature for algo to dst.
66+func (tag *Tag) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) {
77+ signature, ok := tag.signatures[algo]
88+ if !ok {
99+ return dst, false
1010+ }
1111+1212+ for _, part := range signature {
1313+ dst = append(dst, tag.body[part.start:part.end]...)
1414+ }
1515+1616+ return dst, true
1717+}
+15
object/signed/tag/tag.go
···11+package signedtag
22+33+import objectid "codeberg.org/lindenii/furgit/object/id"
44+55+// Tag represents the payload and signatures parsed from a raw tag object.
66+type Tag struct {
77+ body []byte
88+ payload []byteRange
99+ signatures map[objectid.Algorithm][]byteRange
1010+}
1111+1212+type byteRange struct {
1313+ start int
1414+ end int
1515+}