From b250490999bfee7194a3fbbfd66d91037b5b0b26 Mon Sep 17 00:00:00 2001 From: Ross Light Date: Sat, 16 Jan 2021 14:06:25 -0800 Subject: [PATCH] object: add Prefix type --- CHANGELOG.md | 2 ++ object/object.go | 52 ++++++++++++++++++++++++++++++++++++++++ object/object_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efcfadc..0c235ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 interoperable with Git packfile index files. ([#12](https://github.com/gg-scm/gg-git/issues/12)) - `packfile.ReadHeader` enables random access to a packfile. +- `object.Prefix` allows marshaling and unmarshaling the `"blob 42\x00"` prefix + used as part of the Git object hash. ### Changed diff --git a/object/object.go b/object/object.go index af8b775..dba6640 100644 --- a/object/object.go +++ b/object/object.go @@ -22,6 +22,7 @@ https://git-scm.com/book/en/v2/Git-Internals-Git-Objects package object import ( + "bytes" "crypto/sha1" "fmt" "io" @@ -64,6 +65,57 @@ func BlobSum(r io.Reader, size int64) (githash.SHA1, error) { return sum, nil } +// Prefix is a parsed Git object prefix like "blob 42\x00". +type Prefix struct { + Type Type + Size int64 +} + +// MarshalBinary returns the result of AppendPrefix. +func (p Prefix) MarshalBinary() ([]byte, error) { + if !p.Type.IsValid() { + return nil, fmt.Errorf("marshal git object prefix: unknown type %q", p.Type) + } + if p.Size < 0 { + return nil, fmt.Errorf("marshal git object prefix: negative size") + } + return AppendPrefix(nil, p.Type, p.Size), nil +} + +// UnmarshalBinary parses an object prefix. +func (p *Prefix) UnmarshalBinary(data []byte) error { + if len(data) == 0 || data[len(data)-1] != 0 { + return fmt.Errorf("unmarshal git object prefix: does not end with NUL") + } + typeEnd := bytes.IndexByte(data, ' ') + if typeEnd == -1 { + return fmt.Errorf("unmarshal git object prefix: missing space") + } + typ := Type(data[:typeEnd]) + if !typ.IsValid() { + return fmt.Errorf("unmarshal git object prefix: unknown type %q", typ) + } + sizeStart := typeEnd + 1 + sizeEnd := len(data) - 1 + size, err := strconv.ParseInt(string(data[sizeStart:sizeEnd]), 10, 64) + if err != nil { + return fmt.Errorf("unmarshal git object prefix: size: %v", err) + } + if size < 0 { + return fmt.Errorf("unmarshal git object prefix: negative size") + } + p.Type = typ + p.Size = size + return nil +} + +// String returns the prefix without the trailing NUL byte. +func (p Prefix) String() string { + buf := AppendPrefix(nil, p.Type, p.Size) + buf = buf[:len(buf)-1] + return string(buf) +} + // AppendPrefix appends a Git object prefix (e.g. "blob 42\x00") // to a byte slice. func AppendPrefix(dst []byte, typ Type, n int64) []byte { diff --git a/object/object_test.go b/object/object_test.go index 1799d68..c4f66af 100644 --- a/object/object_test.go +++ b/object/object_test.go @@ -17,12 +17,18 @@ package object import ( + "encoding" "strings" "testing" "gg-scm.io/pkg/git/githash" ) +var ( + _ encoding.BinaryMarshaler = Prefix{} + _ encoding.BinaryUnmarshaler = new(Prefix) +) + func TestBlobSum(t *testing.T) { tests := []struct { data string @@ -56,6 +62,56 @@ func TestBlobSum(t *testing.T) { }) } +func TestPrefixUnmarshalBinary(t *testing.T) { + tests := []struct { + data string + want Prefix + wantError bool + }{ + { + data: "blob 0\x00", + want: Prefix{Type: TypeBlob, Size: 0}, + }, + { + data: "tree 42\x00", + want: Prefix{Type: TypeTree, Size: 42}, + }, + { + data: "tree abc\x00", + wantError: true, + }, + { + data: "tree -42\x00", + wantError: true, + }, + { + data: "foo 42\x00", + wantError: true, + }, + { + data: "blob 0", + wantError: true, + }, + } + for _, test := range tests { + var got Prefix + err := got.UnmarshalBinary([]byte(test.data)) + if err != nil { + if !test.wantError { + t.Errorf("new(Prefix).UnmarshalBinary([]byte(%q)) = %v; want ", test.data, err) + } + continue + } + if test.wantError { + t.Errorf("new(Prefix).UnmarshalBinary([]byte(%q)) = ; want error", test.data) + continue + } + if got != test.want { + t.Errorf("new(Prefix).UnarshalBinary([]byte(%q)) yields %+v; want %+v", test.data, got, test.want) + } + } +} + func hashLiteral(s string) githash.SHA1 { var h githash.SHA1 if err := h.UnmarshalText([]byte(s)); err != nil {