diff options
Diffstat (limited to 'object/signature')
| -rw-r--r-- | object/signature/parse.go | 97 | ||||
| -rw-r--r-- | object/signature/serialize.go | 33 | ||||
| -rw-r--r-- | object/signature/signature.go | 10 | ||||
| -rw-r--r-- | object/signature/when.go | 10 |
4 files changed, 150 insertions, 0 deletions
diff --git a/object/signature/parse.go b/object/signature/parse.go new file mode 100644 index 00000000..a6880eee --- /dev/null +++ b/object/signature/parse.go @@ -0,0 +1,97 @@ +package signature + +import ( + "bytes" + "errors" + "fmt" + "strconv" + + "codeberg.org/lindenii/furgit/internal/intconv" +) + +// Parse parses a canonical Git signature line: +// "Name <email> 123456789 +0000". +func Parse(line []byte) (*Signature, error) { + lt := bytes.IndexByte(line, '<') + if lt < 0 { + return nil, errors.New("object: signature: missing opening <") + } + + gtRel := bytes.IndexByte(line[lt+1:], '>') + if gtRel < 0 { + return nil, errors.New("object: signature: missing closing >") + } + + gt := lt + 1 + gtRel + + nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...) + emailBytes := append([]byte(nil), line[lt+1:gt]...) + + rest := line[gt+1:] + if len(rest) == 0 || rest[0] != ' ' { + return nil, errors.New("object: signature: missing timestamp separator") + } + + rest = rest[1:] + + before, after, ok := bytes.Cut(rest, []byte{' '}) + if !ok { + return nil, errors.New("object: signature: missing timezone separator") + } + + when, err := strconv.ParseInt(string(before), 10, 64) + if err != nil { + return nil, fmt.Errorf("object: signature: invalid timestamp: %w", err) + } + + tz := after + if len(tz) < 5 { + return nil, errors.New("object: signature: invalid timezone encoding") + } + + sign := 1 + + switch tz[0] { + case '-': + sign = -1 + case '+': + default: + return nil, errors.New("object: signature: invalid timezone sign") + } + + hh, err := strconv.Atoi(string(tz[1:3])) + if err != nil { + return nil, fmt.Errorf("object: signature: invalid timezone hours: %w", err) + } + + mm, err := strconv.Atoi(string(tz[3:5])) + if err != nil { + return nil, fmt.Errorf("object: signature: invalid timezone minutes: %w", err) + } + + if hh < 0 || hh > 23 { + return nil, errors.New("object: signature: invalid timezone hours range") + } + + if mm < 0 || mm > 59 { + return nil, errors.New("object: signature: invalid timezone minutes range") + } + + total := int64(hh)*60 + int64(mm) + + offset, err := intconv.Int64ToInt32(total) + if err != nil { + return nil, errors.New("object: signature: timezone overflow") + } + + if sign < 0 { + offset = -offset + } + + return &Signature{ + Name: nameBytes, + Email: emailBytes, + WhenUnix: when, + OffsetMinutes: offset, + }, nil +} diff --git a/object/signature/serialize.go b/object/signature/serialize.go new file mode 100644 index 00000000..3f60d20d --- /dev/null +++ b/object/signature/serialize.go @@ -0,0 +1,33 @@ +package signature + +import ( + "fmt" + "strconv" + "strings" +) + +// Serialize renders the signature in canonical Git format. +func (signature Signature) Serialize() ([]byte, error) { + var b strings.Builder + b.Grow(len(signature.Name) + len(signature.Email) + 32) + b.Write(signature.Name) + b.WriteString(" <") + b.Write(signature.Email) + b.WriteString("> ") + b.WriteString(strconv.FormatInt(signature.WhenUnix, 10)) + b.WriteByte(' ') + + offset := signature.OffsetMinutes + + sign := '+' + if offset < 0 { + sign = '-' + offset = -offset + } + + hh := offset / 60 + mm := offset % 60 + fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm) + + return []byte(b.String()), nil +} diff --git a/object/signature/signature.go b/object/signature/signature.go new file mode 100644 index 00000000..22e516f9 --- /dev/null +++ b/object/signature/signature.go @@ -0,0 +1,10 @@ +// Package signature provides routines and representations that implement author/committer/tagger signatures. +package signature + +// Signature represents a Git signature (author/committer/tagger). +type Signature struct { + Name []byte + Email []byte + WhenUnix int64 + OffsetMinutes int32 +} diff --git a/object/signature/when.go b/object/signature/when.go new file mode 100644 index 00000000..0a252f68 --- /dev/null +++ b/object/signature/when.go @@ -0,0 +1,10 @@ +package signature + +import "time" + +// When returns a time.Time with the signature's timezone offset. +func (signature Signature) When() time.Time { + loc := time.FixedZone("git", int(signature.OffsetMinutes)*60) + + return time.Unix(signature.WhenUnix, 0).In(loc) +} |
