From d6ce6d250c0fc9adc1992d853cd1c64b57ed62c4 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 2 Apr 2026 09:05:11 +0000 Subject: object/signature: Add --- object/signature/doc.go | 3 ++ object/signature/errors.go | 6 +++ object/signature/parse.go | 95 +++++++++++++++++++++++++++++++++++++++++++ object/signature/serialize.go | 33 +++++++++++++++ object/signature/signature.go | 9 ++++ object/signature/when.go | 10 +++++ 6 files changed, 156 insertions(+) create mode 100644 object/signature/doc.go create mode 100644 object/signature/errors.go create mode 100644 object/signature/parse.go create mode 100644 object/signature/serialize.go create mode 100644 object/signature/signature.go create mode 100644 object/signature/when.go (limited to 'object') 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 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) +} -- cgit v1.3.1-10-gc9f91