aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-04-02 09:05:11 +0000
committerGravatar Runxi Yu2026-04-02 09:05:11 +0000
commitd6ce6d250c0fc9adc1992d853cd1c64b57ed62c4 (patch)
tree3847008dd41b6c63fca780d6651b1cb39bcc2ad5
parentobject/header: Add (diff)
signatureNo signature
object/signature: Add
-rw-r--r--object/signature/doc.go3
-rw-r--r--object/signature/errors.go6
-rw-r--r--object/signature/parse.go95
-rw-r--r--object/signature/serialize.go33
-rw-r--r--object/signature/signature.go9
-rw-r--r--object/signature/when.go10
6 files changed, 156 insertions, 0 deletions
diff --git a/object/signature/doc.go b/object/signature/doc.go
new file mode 100644
index 00000000..042a0374
--- /dev/null
+++ b/object/signature/doc.go
@@ -0,0 +1,3 @@
+// Package signature provides Git author, committer, and tagger signatures
+// of form "Name <email> 123456789 +0000".
+package signature
diff --git a/object/signature/errors.go b/object/signature/errors.go
new file mode 100644
index 00000000..83bb5e4c
--- /dev/null
+++ b/object/signature/errors.go
@@ -0,0 +1,6 @@
+package signature
+
+import "errors"
+
+// ErrInvalidSignature indicates an attempt to parse an invalid signature.
+var ErrInvalidSignature = errors.New("object: signature: invalid signature")
diff --git a/object/signature/parse.go b/object/signature/parse.go
new file mode 100644
index 00000000..1de72ab5
--- /dev/null
+++ b/object/signature/parse.go
@@ -0,0 +1,95 @@
+package signature
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+)
+
+// Parse parses a canonical Git signature line.
+func Parse(line []byte) (*Signature, error) {
+ lt := bytes.IndexByte(line, '<')
+ if lt < 0 {
+ return nil, fmt.Errorf("%w: missing opening <", ErrInvalidSignature)
+ }
+
+ gtRel := bytes.IndexByte(line[lt+1:], '>')
+ if gtRel < 0 {
+ return nil, fmt.Errorf("%w: missing closing >", ErrInvalidSignature)
+ }
+
+ 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, fmt.Errorf("%w: missing timestamp separator", ErrInvalidSignature)
+ }
+
+ rest = rest[1:]
+
+ before, after, ok := bytes.Cut(rest, []byte{' '})
+ if !ok {
+ return nil, fmt.Errorf("%w: missing timezone separator", ErrInvalidSignature)
+ }
+
+ 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, fmt.Errorf("%w: invalid timezone encoding", ErrInvalidSignature)
+ }
+
+ sign := 1
+
+ switch tz[0] {
+ case '-':
+ sign = -1
+ case '+':
+ default:
+ return nil, fmt.Errorf("%w: invalid timezone sign", ErrInvalidSignature)
+ }
+
+ 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, fmt.Errorf("%w: invalid timezone hours range", ErrInvalidSignature)
+ }
+
+ if mm < 0 || mm > 59 {
+ return nil, fmt.Errorf("%w: invalid timezone minutes range", ErrInvalidSignature)
+ }
+
+ total := int64(hh)*60 + int64(mm)
+
+ offset, err := intconv.Int64ToInt32(total)
+ if err != nil {
+ return nil, fmt.Errorf("%w: timezone overflow", ErrInvalidSignature)
+ }
+
+ 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..f01c5e2e
--- /dev/null
+++ b/object/signature/signature.go
@@ -0,0 +1,9 @@
+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)
+}