diff options
| author | 2026-04-02 09:05:11 +0000 | |
|---|---|---|
| committer | 2026-04-02 09:05:11 +0000 | |
| commit | d6ce6d250c0fc9adc1992d853cd1c64b57ed62c4 (patch) | |
| tree | 3847008dd41b6c63fca780d6651b1cb39bcc2ad5 | |
| parent | object/header: Add (diff) | |
| signature | No signature | |
object/signature: Add
| -rw-r--r-- | object/signature/doc.go | 3 | ||||
| -rw-r--r-- | object/signature/errors.go | 6 | ||||
| -rw-r--r-- | object/signature/parse.go | 95 | ||||
| -rw-r--r-- | object/signature/serialize.go | 33 | ||||
| -rw-r--r-- | object/signature/signature.go | 9 | ||||
| -rw-r--r-- | object/signature/when.go | 10 |
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) +} |
