| // Copyright 2021 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package gitfs | |
| import ( | |
| "bytes" | |
| "compress/zlib" | |
| "crypto/sha1" | |
| "encoding/binary" | |
| "fmt" | |
| "io" | |
| ) | |
| // unpack parses data, which is a Git pack-formatted archive, | |
| // writing every object it contains to the store s. | |
| // | |
| // See https://git-scm.com/docs/pack-format for format documentation. | |
| func unpack(s *store, data []byte) error { | |
| // If the store is empty, pre-allocate the length of data. | |
| // This should be about the right order of magnitude for the eventual data, | |
| // avoiding many growing steps during append. | |
| if len(s.data) == 0 { | |
| s.data = make([]byte, 0, len(data)) | |
| } | |
| // Pack data starts with 12-byte header: "PACK" version[4] nobj[4]. | |
| if len(data) < 12+20 { | |
| return fmt.Errorf("malformed git pack: too short") | |
| } | |
| hdr := data[:12] | |
| vers := binary.BigEndian.Uint32(hdr[4:8]) | |
| nobj := binary.BigEndian.Uint32(hdr[8:12]) | |
| if string(hdr[:4]) != "PACK" || vers != 2 && vers != 3 || len(data) < 12+20 || int64(nobj) >= int64(len(data)) { | |
| return fmt.Errorf("malformed git pack") | |
| } | |
| if vers == 3 { | |
| return fmt.Errorf("cannot read git pack v3") | |
| } | |
| // Pack data ends with SHA1 of the entire pack. | |
| sum := sha1.Sum(data[:len(data)-20]) | |
| if !bytes.Equal(sum[:], data[len(data)-20:]) { | |
| return fmt.Errorf("malformed git pack: bad checksum") | |
| } | |
| // Object data is everything between hdr and ending SHA1. | |
| // Unpack every object into the store. | |
| objs := data[12 : len(data)-20] | |
| off := 0 | |
| for i := 0; i < int(nobj); i++ { | |
| _, _, _, encSize, err := unpackObject(s, objs, off) | |
| if err != nil { | |
| return fmt.Errorf("unpack: malformed git pack: %v", err) | |
| } | |
| off += encSize | |
| } | |
| if off != len(objs) { | |
| return fmt.Errorf("malformed git pack: junk after objects") | |
| } | |
| return nil | |
| } | |
| // unpackObject unpacks the object at objs[off:] and writes it to the store s. | |
| // It returns the type, hash, and content of the object, as well as the encoded size, | |
| // meaning the number of bytes at the start of objs[off:] that this record occupies. | |
| func unpackObject(s *store, objs []byte, off int) (typ objType, h Hash, content []byte, encSize int, err error) { | |
| fail := func(err error) (objType, Hash, []byte, int, error) { | |
| return 0, Hash{}, nil, 0, err | |
| } | |
| if off < 0 || off >= len(objs) { | |
| return fail(fmt.Errorf("invalid object offset")) | |
| } | |
| // Object starts with varint-encoded type and length n. | |
| // (The length n is the length of the compressed data that follows, | |
| // not the length of the actual object.) | |
| u, size := binary.Uvarint(objs[off:]) | |
| if size <= 0 { | |
| return fail(fmt.Errorf("invalid object: bad varint header")) | |
| } | |
| typ = objType((u >> 4) & 7) | |
| n := int(u&15 | u>>7<<4) | |
| // Git often stores objects that differ very little (different revs of a file). | |
| // It can save space by encoding one as "start with this other object and apply these diffs". | |
| // There are two ways to specify "this other object": an object ref (20-byte SHA1) | |
| // or as a relative offset to an earlier position in the objs slice. | |
| // For either of these, we need to fetch the other object's type and data (deltaTyp and deltaBase). | |
| // The Git docs call this the "deltified representation". | |
| var deltaTyp objType | |
| var deltaBase []byte | |
| switch typ { | |
| case objRefDelta: | |
| if len(objs)-(off+size) < 20 { | |
| return fail(fmt.Errorf("invalid object: bad delta ref")) | |
| } | |
| // Base block identified by SHA1 of an already unpacked hash. | |
| var h Hash | |
| copy(h[:], objs[off+size:]) | |
| size += 20 | |
| deltaTyp, deltaBase = s.object(h) | |
| if deltaTyp == 0 { | |
| return fail(fmt.Errorf("invalid object: unknown delta ref %v", h)) | |
| } | |
| case objOfsDelta: | |
| i := off + size | |
| if len(objs)-i < 20 { | |
| return fail(fmt.Errorf("invalid object: too short")) | |
| } | |
| // Base block identified by relative offset to earlier position in objs, | |
| // using a varint-like but not-quite-varint encoding. | |
| // Look for "offset encoding:" in https://git-scm.com/docs/pack-format. | |
| d := int64(objs[i] & 0x7f) | |
| for objs[i]&0x80 != 0 { | |
| i++ | |
| if i-(off+size) > 10 { | |
| return fail(fmt.Errorf("invalid object: malformed delta offset")) | |
| } | |
| d = d<<7 | int64(objs[i]&0x7f) | |
| d += 1 << 7 | |
| } | |
| i++ | |
| size = i - off | |
| // Re-unpack the object at the earlier offset to find its type and content. | |
| if d == 0 || d > int64(off) { | |
| return fail(fmt.Errorf("invalid object: bad delta offset")) | |
| } | |
| var err error | |
| deltaTyp, _, deltaBase, _, err = unpackObject(s, objs, off-int(d)) | |
| if err != nil { | |
| return fail(fmt.Errorf("invalid object: bad delta offset")) | |
| } | |
| } | |
| // The main encoded data is a zlib-compressed stream. | |
| br := bytes.NewReader(objs[off+size:]) | |
| zr, err := zlib.NewReader(br) | |
| if err != nil { | |
| return fail(fmt.Errorf("invalid object deflate: %v", err)) | |
| } | |
| data, err := io.ReadAll(zr) | |
| if err != nil { | |
| return fail(fmt.Errorf("invalid object: bad deflate: %v", err)) | |
| } | |
| if len(data) != n { | |
| return fail(fmt.Errorf("invalid object: deflate size %d != %d", len(data), n)) | |
| } | |
| encSize = len(objs[off:]) - br.Len() | |
| // If we fetched a base object above, the stream is an encoded delta. | |
| // Otherwise it is the raw data. | |
| switch typ { | |
| default: | |
| return fail(fmt.Errorf("invalid object: unknown object type")) | |
| case objCommit, objTree, objBlob, objTag: | |
| // ok | |
| case objRefDelta, objOfsDelta: | |
| // Actual object type is the type of the base object. | |
| typ = deltaTyp | |
| // Delta encoding starts with size of base object and size of new object. | |
| baseSize, s := binary.Uvarint(data) | |
| data = data[s:] | |
| if baseSize != uint64(len(deltaBase)) { | |
| return fail(fmt.Errorf("invalid object: mismatched delta src size")) | |
| } | |
| targSize, s := binary.Uvarint(data) | |
| data = data[s:] | |
| // Apply delta to base object, producing new object. | |
| targ := make([]byte, targSize) | |
| if err := applyDelta(targ, deltaBase, data); err != nil { | |
| return fail(fmt.Errorf("invalid object: %v", err)) | |
| } | |
| data = targ | |
| } | |
| h, data = s.add(typ, data) | |
| return typ, h, data, encSize, nil | |
| } | |
| // applyDelta applies the delta encoding to src, producing dst, | |
| // which has already been allocated to the expected final size. | |
| // See https://git-scm.com/docs/pack-format#_deltified_representation for docs. | |
| func applyDelta(dst, src, delta []byte) error { | |
| for len(delta) > 0 { | |
| // Command byte says what comes next. | |
| cmd := delta[0] | |
| delta = delta[1:] | |
| switch { | |
| case cmd == 0: | |
| // cmd == 0 is reserved. | |
| return fmt.Errorf("invalid delta cmd") | |
| case cmd&0x80 != 0: | |
| // Copy from base object, 4-byte offset, 3-byte size. | |
| // But any zero byte in the offset or size can be omitted. | |
| // The bottom 7 bits of cmd say which offset/size bytes are present. | |
| var off, size int64 | |
| for i := uint(0); i < 4; i++ { | |
| if cmd&(1<<i) != 0 { | |
| off |= int64(delta[0]) << (8 * i) | |
| delta = delta[1:] | |
| } | |
| } | |
| for i := uint(0); i < 3; i++ { | |
| if cmd&(0x10<<i) != 0 { | |
| size |= int64(delta[0]) << (8 * i) | |
| delta = delta[1:] | |
| } | |
| } | |
| // Size 0 means size 0x10000 for some reason. (!) | |
| if size == 0 { | |
| size = 0x10000 | |
| } | |
| copy(dst[:size], src[off:off+size]) | |
| dst = dst[size:] | |
| default: | |
| // Up to 0x7F bytes of literal data, length in bottom 7 bits of cmd. | |
| n := int(cmd) | |
| copy(dst[:n], delta[:n]) | |
| dst = dst[n:] | |
| delta = delta[n:] | |
| } | |
| } | |
| if len(dst) != 0 { | |
| return fmt.Errorf("delta encoding too short") | |
| } | |
| return nil | |
| } |