262 lines
5.7 KiB
Go
262 lines
5.7 KiB
Go
package gitobject
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1" //nolint:gosec // used for git object hashes
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"hash"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type GitObject struct {
|
|
Type string
|
|
Headers map[string][]string
|
|
Message string
|
|
Signature string
|
|
SignedData string
|
|
Raw []byte
|
|
}
|
|
|
|
type Actor struct {
|
|
Name string
|
|
Email string
|
|
When *time.Time
|
|
}
|
|
|
|
type Commit struct {
|
|
Tree string
|
|
Parents []string
|
|
Author Actor
|
|
Committer Actor
|
|
Message string
|
|
}
|
|
|
|
type Tag struct {
|
|
Object string
|
|
Type string
|
|
Tag string
|
|
Tagger Actor
|
|
Message string
|
|
}
|
|
|
|
func Parse(raw []byte) (*GitObject, error) {
|
|
obj := &GitObject{Headers: make(map[string][]string), Raw: raw}
|
|
lines := strings.Split(string(raw), "\n")
|
|
if len(lines) == 0 {
|
|
return nil, errors.Errorf("invalid empty git object")
|
|
}
|
|
|
|
isTag := bytes.HasPrefix(raw, []byte("object "))
|
|
if isTag {
|
|
obj.Type = "tag"
|
|
} else {
|
|
obj.Type = "commit"
|
|
}
|
|
|
|
var headersDone bool
|
|
var messageDone bool
|
|
var sigLines []string
|
|
var messageLines []string
|
|
var signedDataLines []string
|
|
inSig := false
|
|
|
|
for _, l := range lines {
|
|
if !headersDone {
|
|
if l == "" {
|
|
headersDone = true
|
|
signedDataLines = append(signedDataLines, l)
|
|
continue
|
|
}
|
|
if !isTag && strings.HasPrefix(l, "gpgsig ") {
|
|
inSig = true
|
|
sigLines = append(sigLines, strings.TrimPrefix(l, "gpgsig "))
|
|
continue
|
|
}
|
|
if !isTag && strings.HasPrefix(l, "gpgsig-sha256 ") {
|
|
inSig = true
|
|
sigLines = append(sigLines, strings.TrimPrefix(l, "gpgsig-sha256 "))
|
|
continue
|
|
}
|
|
if inSig {
|
|
if v, ok := strings.CutPrefix(l, " "); ok {
|
|
sigLines = append(sigLines, v)
|
|
continue
|
|
} else {
|
|
inSig = false
|
|
}
|
|
}
|
|
signedDataLines = append(signedDataLines, l)
|
|
parts := strings.SplitN(l, " ", 2)
|
|
if len(parts) == 2 {
|
|
obj.Headers[parts[0]] = append(obj.Headers[parts[0]], parts[1])
|
|
}
|
|
continue
|
|
}
|
|
if isTag && (l == "-----BEGIN PGP SIGNATURE-----" || l == "-----BEGIN SSH SIGNATURE-----") {
|
|
messageDone = true
|
|
}
|
|
if messageDone {
|
|
sigLines = append(sigLines, l)
|
|
} else {
|
|
messageLines = append(messageLines, l)
|
|
signedDataLines = append(signedDataLines, l)
|
|
}
|
|
}
|
|
|
|
obj.Message = strings.Join(messageLines, "\n")
|
|
obj.Message = strings.TrimSuffix(obj.Message, "\n") // body ends with newline but no extra newline between message and signature
|
|
if len(sigLines) > 0 {
|
|
obj.Signature = strings.Join(sigLines, "\n")
|
|
}
|
|
|
|
obj.SignedData = strings.Join(signedDataLines, "\n")
|
|
if isTag {
|
|
obj.SignedData += "\n"
|
|
}
|
|
|
|
// basic validation
|
|
requiredHeaders := []string{}
|
|
switch obj.Type {
|
|
case "commit":
|
|
requiredHeaders = append(requiredHeaders, "tree", "author", "committer")
|
|
case "tag":
|
|
requiredHeaders = append(requiredHeaders, "object", "type", "tag", "tagger")
|
|
}
|
|
|
|
for _, header := range requiredHeaders {
|
|
if _, ok := obj.Headers[header]; !ok {
|
|
return nil, errors.Errorf("invalid %s object: missing %s header", obj.Type, header)
|
|
}
|
|
}
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
func (obj *GitObject) Checksum(hashFunc func() hash.Hash) ([]byte, error) {
|
|
h := hashFunc()
|
|
header := fmt.Sprintf("commit %d\u0000", len(obj.Raw))
|
|
if obj.Type == "tag" {
|
|
header = fmt.Sprintf("tag %d\u0000", len(obj.Raw))
|
|
}
|
|
data := append([]byte(header), obj.Raw...)
|
|
h.Write(data)
|
|
return h.Sum(nil), nil
|
|
}
|
|
|
|
func (obj *GitObject) VerifyChecksum(sha string) error {
|
|
var hf func() hash.Hash
|
|
switch len(sha) {
|
|
case 40:
|
|
hf = sha1.New
|
|
case 64:
|
|
hf = sha256.New
|
|
default:
|
|
return errors.Errorf("unsupported sha length %d", len(sha))
|
|
}
|
|
sum, err := obj.Checksum(hf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hexValue := hex.EncodeToString(sum); sha != hexValue {
|
|
return errors.Errorf("checksum mismatch: expected %s, got %s", sha, hexValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (obj *GitObject) ToCommit() (*Commit, error) {
|
|
if obj.Type != "commit" {
|
|
return nil, errors.Errorf("not a commit object")
|
|
}
|
|
c := &Commit{}
|
|
if trees, ok := obj.Headers["tree"]; ok && len(trees) > 0 {
|
|
c.Tree = trees[0]
|
|
}
|
|
if parents, ok := obj.Headers["parent"]; ok && len(parents) > 0 {
|
|
c.Parents = parents
|
|
}
|
|
if authors, ok := obj.Headers["author"]; ok && len(authors) > 0 {
|
|
c.Author = parseActor(authors[0])
|
|
}
|
|
if committers, ok := obj.Headers["committer"]; ok && len(committers) > 0 {
|
|
c.Committer = parseActor(committers[0])
|
|
}
|
|
c.Message = obj.Message
|
|
return c, nil
|
|
}
|
|
|
|
func (obj *GitObject) ToTag() (*Tag, error) {
|
|
if obj.Type != "tag" {
|
|
return nil, errors.Errorf("not a tag object")
|
|
}
|
|
t := &Tag{}
|
|
if objects, ok := obj.Headers["object"]; ok && len(objects) > 0 {
|
|
t.Object = objects[0]
|
|
}
|
|
if types, ok := obj.Headers["type"]; ok && len(types) > 0 {
|
|
t.Type = types[0]
|
|
}
|
|
if tags, ok := obj.Headers["tag"]; ok && len(tags) > 0 {
|
|
t.Tag = tags[0]
|
|
}
|
|
if taggers, ok := obj.Headers["tagger"]; ok && len(taggers) > 0 {
|
|
t.Tagger = parseActor(taggers[0])
|
|
}
|
|
t.Message = obj.Message
|
|
return t, nil
|
|
}
|
|
|
|
func parseActor(s string) Actor {
|
|
s = strings.TrimSpace(s)
|
|
var a Actor
|
|
|
|
// find last angle brackets, because name can contain '<'
|
|
start := strings.LastIndex(s, "<")
|
|
end := strings.LastIndex(s, ">")
|
|
if start == -1 || end == -1 || end < start {
|
|
// malformed, treat as plain name
|
|
a.Name = s
|
|
return a
|
|
}
|
|
|
|
a.Name = strings.TrimSpace(s[:start])
|
|
a.Email = strings.TrimSpace(s[start+1 : end])
|
|
|
|
rest := strings.TrimSpace(s[end+1:])
|
|
if rest == "" {
|
|
return a
|
|
}
|
|
|
|
parts := strings.Fields(rest)
|
|
if len(parts) < 2 {
|
|
return a
|
|
}
|
|
|
|
unix, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
return a
|
|
}
|
|
|
|
tz := parts[1]
|
|
if len(tz) != 5 || (tz[0] != '+' && tz[0] != '-') {
|
|
return a
|
|
}
|
|
|
|
sign := 1
|
|
if tz[0] == '-' {
|
|
sign = -1
|
|
}
|
|
hh, _ := strconv.Atoi(tz[1:3])
|
|
mm, _ := strconv.Atoi(tz[3:5])
|
|
offset := sign * (hh*3600 + mm*60)
|
|
|
|
t := time.Unix(unix, 0).In(time.FixedZone("", offset))
|
|
a.When = &t
|
|
return a
|
|
}
|