feat(git): read refs, loose and packed objects, trees, blobs and commits
Open a bare repository or a .git directory, read-only and safe for concurrent use. Refs (loose over packed, peeled lines, symbolic HEAD, unborn branch), loose objects, pack v2 with mmap, fan-out lookup, 64-bit offsets, ofs and ref deltas with a byte-bounded LRU of delta bases and a depth bound, prefix resolution with ErrAmbiguous, commit, tree and tag parsers that allocate nothing per entry, streaming blobs, and Refresh with refcounted packs so a concurrent repack never breaks a read. Unsupported layouts fail Open with ErrUnsupported and the reason.
- commit
- 4d153b9d35cbf7448239f45f50dffacefa8417ab
- author
- brice · · 2026-10-10 20:35 UTC
- parent
- e62a0a1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
21 files · +3539 -0
A blob.go +214 -0
| @@ -0,0 +1,214 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "errors" | |
| 8 | + | "io" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // OpenBlob opens the blob id for reading and returns its size. Undeltified | |
| 12 | + | // blobs stream from the memory-mapped pack or the loose file, inflated as they | |
| 13 | + | // are read, so a large file is never held in memory. A deltified blob is | |
| 14 | + | // rebuilt in a pooled buffer first and is bounded by WithMaxObjectSize | |
| 15 | + | // (ErrTooLarge). The caller must close the reader, which also releases the | |
| 16 | + | // pack it reads from. Reads detect truncated or oversized data and return | |
| 17 | + | // ErrCorrupt. | |
| 18 | + | func (r *Repo) OpenBlob(id ObjectID) (int64, io.ReadCloser, error) { | |
| 19 | + | f, err := r.finder() | |
| 20 | + | if err != nil { | |
| 21 | + | return 0, nil, err | |
| 22 | + | } | |
| 23 | + | defer f.close() | |
| 24 | + | for { | |
| 25 | + | p, off, err := f.pack(&id) | |
| 26 | + | if err != nil { | |
| 27 | + | return 0, nil, err | |
| 28 | + | } | |
| 29 | + | if p != nil { | |
| 30 | + | size, rc, err := r.openPackedBlob(p, off, id) | |
| 31 | + | if err != nil { | |
| 32 | + | return 0, nil, wrapPack(p.name, err) | |
| 33 | + | } | |
| 34 | + | return size, rc, nil | |
| 35 | + | } | |
| 36 | + | size, rc, err := r.openLooseBlob(&id) | |
| 37 | + | if err == nil { | |
| 38 | + | return size, rc, nil | |
| 39 | + | } | |
| 40 | + | if !errors.Is(err, ErrNotFound) { | |
| 41 | + | return 0, nil, err | |
| 42 | + | } | |
| 43 | + | if !f.again() { | |
| 44 | + | return 0, nil, notFound("object %s", id) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func (r *Repo) openPackedBlob(p *pack, off uint64, id ObjectID) (int64, io.ReadCloser, error) { | |
| 50 | + | h, err := parseEntryHeader(p.data, off) | |
| 51 | + | if err != nil { | |
| 52 | + | return 0, nil, err | |
| 53 | + | } | |
| 54 | + | if h.isDelta() { | |
| 55 | + | b := getBuf() | |
| 56 | + | typ, data, err := r.readPacked(p, off, b.b) | |
| 57 | + | if err != nil { | |
| 58 | + | putBuf(b) | |
| 59 | + | return 0, nil, err | |
| 60 | + | } | |
| 61 | + | b.b = data | |
| 62 | + | if typ != TypeBlob { | |
| 63 | + | putBuf(b) | |
| 64 | + | return 0, nil, wrongType(id, typ, TypeBlob) | |
| 65 | + | } | |
| 66 | + | return int64(len(data)), &memBlob{buf: b, r: bytes.NewReader(data)}, nil | |
| 67 | + | } | |
| 68 | + | if Type(h.typ) != TypeBlob { | |
| 69 | + | return 0, nil, wrongType(id, Type(h.typ), TypeBlob) | |
| 70 | + | } | |
| 71 | + | if h.size > 1<<62 || h.size/1032 > uint64(len(p.stream(&h))) { | |
| 72 | + | return 0, nil, corrupt("blob of %d bytes at %d", h.size, off) | |
| 73 | + | } | |
| 74 | + | m, err := getMem(p.stream(&h)) | |
| 75 | + | if err != nil { | |
| 76 | + | return 0, nil, err | |
| 77 | + | } | |
| 78 | + | p.retain() | |
| 79 | + | return int64(h.size), &streamBlob{src: m, pack: p, left: int64(h.size), zr: m.zr}, nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (r *Repo) openLooseBlob(id *ObjectID) (int64, io.ReadCloser, error) { | |
| 83 | + | l, err := r.openLoose(id) | |
| 84 | + | if err != nil { | |
| 85 | + | return 0, nil, err | |
| 86 | + | } | |
| 87 | + | if l.typ != TypeBlob { | |
| 88 | + | l.close() | |
| 89 | + | return 0, nil, wrongType(*id, l.typ, TypeBlob) | |
| 90 | + | } | |
| 91 | + | if l.size > 1<<62 || uint64(l.nlea) > l.size { | |
| 92 | + | l.close() | |
| 93 | + | return 0, nil, corrupt("loose object %s: size %d", id.String(), l.size) | |
| 94 | + | } | |
| 95 | + | return int64(l.size), &streamBlob{loose: l, left: int64(l.size), zr: l.fi.zr, lead: l.lead[:l.nlea]}, nil | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // memBlob serves a blob rebuilt in memory. | |
| 99 | + | type memBlob struct { | |
| 100 | + | buf *buffer | |
| 101 | + | r *bytes.Reader | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (m *memBlob) Read(p []byte) (int, error) { | |
| 105 | + | if m.r == nil { | |
| 106 | + | return 0, io.ErrClosedPipe | |
| 107 | + | } | |
| 108 | + | return m.r.Read(p) | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (m *memBlob) WriteTo(w io.Writer) (int64, error) { | |
| 112 | + | if m.r == nil { | |
| 113 | + | return 0, io.ErrClosedPipe | |
| 114 | + | } | |
| 115 | + | return m.r.WriteTo(w) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (m *memBlob) Close() error { | |
| 119 | + | if m.r != nil { | |
| 120 | + | m.r = nil | |
| 121 | + | putBuf(m.buf) | |
| 122 | + | m.buf = nil | |
| 123 | + | } | |
| 124 | + | return nil | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // streamBlob inflates a blob while it is read. | |
| 128 | + | type streamBlob struct { | |
| 129 | + | src *memInflater // packed | |
| 130 | + | pack *pack | |
| 131 | + | loose *looseObject // loose | |
| 132 | + | zr io.Reader | |
| 133 | + | lead []byte // loose: content already read with the header | |
| 134 | + | left int64 // bytes still to return | |
| 135 | + | done bool // end of stream verified | |
| 136 | + | err error | |
| 137 | + | probe [1]byte | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (s *streamBlob) Read(p []byte) (int, error) { | |
| 141 | + | if s.zr == nil { | |
| 142 | + | return 0, io.ErrClosedPipe | |
| 143 | + | } | |
| 144 | + | if s.err != nil { | |
| 145 | + | return 0, s.err | |
| 146 | + | } | |
| 147 | + | if len(p) == 0 { | |
| 148 | + | return 0, nil | |
| 149 | + | } | |
| 150 | + | if s.left == 0 { | |
| 151 | + | return 0, s.finish() | |
| 152 | + | } | |
| 153 | + | if int64(len(p)) > s.left { | |
| 154 | + | p = p[:s.left] | |
| 155 | + | } | |
| 156 | + | n := 0 | |
| 157 | + | if len(s.lead) > 0 { | |
| 158 | + | n = copy(p, s.lead) | |
| 159 | + | s.lead = s.lead[n:] | |
| 160 | + | s.left -= int64(n) | |
| 161 | + | return n, nil | |
| 162 | + | } | |
| 163 | + | n, err := s.zr.Read(p) | |
| 164 | + | s.left -= int64(n) | |
| 165 | + | if err != nil && !errors.Is(err, io.EOF) { | |
| 166 | + | s.err = corrupt("blob: %v", err) | |
| 167 | + | return n, s.err | |
| 168 | + | } | |
| 169 | + | if errors.Is(err, io.EOF) && s.left > 0 { | |
| 170 | + | s.err = corrupt("blob shorter than its declared size") | |
| 171 | + | return n, s.err | |
| 172 | + | } | |
| 173 | + | if n > 0 { | |
| 174 | + | return n, nil | |
| 175 | + | } | |
| 176 | + | if s.left == 0 { | |
| 177 | + | return 0, s.finish() | |
| 178 | + | } | |
| 179 | + | return 0, nil | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // finish checks that the stream ends exactly here and that its checksum is | |
| 183 | + | // right, then reports EOF. | |
| 184 | + | func (s *streamBlob) finish() error { | |
| 185 | + | if !s.done { | |
| 186 | + | n, err := s.zr.Read(s.probe[:]) | |
| 187 | + | switch { | |
| 188 | + | case n != 0: | |
| 189 | + | s.err = corrupt("blob longer than its declared size") | |
| 190 | + | case !errors.Is(err, io.EOF): | |
| 191 | + | s.err = corrupt("blob: %v", err) | |
| 192 | + | } | |
| 193 | + | s.done = true | |
| 194 | + | } | |
| 195 | + | if s.err != nil { | |
| 196 | + | return s.err | |
| 197 | + | } | |
| 198 | + | return io.EOF | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | func (s *streamBlob) Close() error { | |
| 202 | + | if s.zr == nil { | |
| 203 | + | return nil | |
| 204 | + | } | |
| 205 | + | s.zr = nil | |
| 206 | + | if s.src != nil { | |
| 207 | + | putMem(s.src) | |
| 208 | + | s.pack.release() | |
| 209 | + | } | |
| 210 | + | if s.loose != nil { | |
| 211 | + | s.loose.close() | |
| 212 | + | } | |
| 213 | + | return nil | |
| 214 | + | } | |
| @@ -0,0 +1,214 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "errors" | |||
| 8 | + | "io" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // OpenBlob opens the blob id for reading and returns its size. Undeltified | |||
| 12 | + | // blobs stream from the memory-mapped pack or the loose file, inflated as they | |||
| 13 | + | // are read, so a large file is never held in memory. A deltified blob is | |||
| 14 | + | // rebuilt in a pooled buffer first and is bounded by WithMaxObjectSize | |||
| 15 | + | // (ErrTooLarge). The caller must close the reader, which also releases the | |||
| 16 | + | // pack it reads from. Reads detect truncated or oversized data and return | |||
| 17 | + | // ErrCorrupt. | |||
| 18 | + | func (r *Repo) OpenBlob(id ObjectID) (int64, io.ReadCloser, error) { | |||
| 19 | + | f, err := r.finder() | |||
| 20 | + | if err != nil { | |||
| 21 | + | return 0, nil, err | |||
| 22 | + | } | |||
| 23 | + | defer f.close() | |||
| 24 | + | for { | |||
| 25 | + | p, off, err := f.pack(&id) | |||
| 26 | + | if err != nil { | |||
| 27 | + | return 0, nil, err | |||
| 28 | + | } | |||
| 29 | + | if p != nil { | |||
| 30 | + | size, rc, err := r.openPackedBlob(p, off, id) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return 0, nil, wrapPack(p.name, err) | |||
| 33 | + | } | |||
| 34 | + | return size, rc, nil | |||
| 35 | + | } | |||
| 36 | + | size, rc, err := r.openLooseBlob(&id) | |||
| 37 | + | if err == nil { | |||
| 38 | + | return size, rc, nil | |||
| 39 | + | } | |||
| 40 | + | if !errors.Is(err, ErrNotFound) { | |||
| 41 | + | return 0, nil, err | |||
| 42 | + | } | |||
| 43 | + | if !f.again() { | |||
| 44 | + | return 0, nil, notFound("object %s", id) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func (r *Repo) openPackedBlob(p *pack, off uint64, id ObjectID) (int64, io.ReadCloser, error) { | |||
| 50 | + | h, err := parseEntryHeader(p.data, off) | |||
| 51 | + | if err != nil { | |||
| 52 | + | return 0, nil, err | |||
| 53 | + | } | |||
| 54 | + | if h.isDelta() { | |||
| 55 | + | b := getBuf() | |||
| 56 | + | typ, data, err := r.readPacked(p, off, b.b) | |||
| 57 | + | if err != nil { | |||
| 58 | + | putBuf(b) | |||
| 59 | + | return 0, nil, err | |||
| 60 | + | } | |||
| 61 | + | b.b = data | |||
| 62 | + | if typ != TypeBlob { | |||
| 63 | + | putBuf(b) | |||
| 64 | + | return 0, nil, wrongType(id, typ, TypeBlob) | |||
| 65 | + | } | |||
| 66 | + | return int64(len(data)), &memBlob{buf: b, r: bytes.NewReader(data)}, nil | |||
| 67 | + | } | |||
| 68 | + | if Type(h.typ) != TypeBlob { | |||
| 69 | + | return 0, nil, wrongType(id, Type(h.typ), TypeBlob) | |||
| 70 | + | } | |||
| 71 | + | if h.size > 1<<62 || h.size/1032 > uint64(len(p.stream(&h))) { | |||
| 72 | + | return 0, nil, corrupt("blob of %d bytes at %d", h.size, off) | |||
| 73 | + | } | |||
| 74 | + | m, err := getMem(p.stream(&h)) | |||
| 75 | + | if err != nil { | |||
| 76 | + | return 0, nil, err | |||
| 77 | + | } | |||
| 78 | + | p.retain() | |||
| 79 | + | return int64(h.size), &streamBlob{src: m, pack: p, left: int64(h.size), zr: m.zr}, nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func (r *Repo) openLooseBlob(id *ObjectID) (int64, io.ReadCloser, error) { | |||
| 83 | + | l, err := r.openLoose(id) | |||
| 84 | + | if err != nil { | |||
| 85 | + | return 0, nil, err | |||
| 86 | + | } | |||
| 87 | + | if l.typ != TypeBlob { | |||
| 88 | + | l.close() | |||
| 89 | + | return 0, nil, wrongType(*id, l.typ, TypeBlob) | |||
| 90 | + | } | |||
| 91 | + | if l.size > 1<<62 || uint64(l.nlea) > l.size { | |||
| 92 | + | l.close() | |||
| 93 | + | return 0, nil, corrupt("loose object %s: size %d", id.String(), l.size) | |||
| 94 | + | } | |||
| 95 | + | return int64(l.size), &streamBlob{loose: l, left: int64(l.size), zr: l.fi.zr, lead: l.lead[:l.nlea]}, nil | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // memBlob serves a blob rebuilt in memory. | |||
| 99 | + | type memBlob struct { | |||
| 100 | + | buf *buffer | |||
| 101 | + | r *bytes.Reader | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func (m *memBlob) Read(p []byte) (int, error) { | |||
| 105 | + | if m.r == nil { | |||
| 106 | + | return 0, io.ErrClosedPipe | |||
| 107 | + | } | |||
| 108 | + | return m.r.Read(p) | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (m *memBlob) WriteTo(w io.Writer) (int64, error) { | |||
| 112 | + | if m.r == nil { | |||
| 113 | + | return 0, io.ErrClosedPipe | |||
| 114 | + | } | |||
| 115 | + | return m.r.WriteTo(w) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | func (m *memBlob) Close() error { | |||
| 119 | + | if m.r != nil { | |||
| 120 | + | m.r = nil | |||
| 121 | + | putBuf(m.buf) | |||
| 122 | + | m.buf = nil | |||
| 123 | + | } | |||
| 124 | + | return nil | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // streamBlob inflates a blob while it is read. | |||
| 128 | + | type streamBlob struct { | |||
| 129 | + | src *memInflater // packed | |||
| 130 | + | pack *pack | |||
| 131 | + | loose *looseObject // loose | |||
| 132 | + | zr io.Reader | |||
| 133 | + | lead []byte // loose: content already read with the header | |||
| 134 | + | left int64 // bytes still to return | |||
| 135 | + | done bool // end of stream verified | |||
| 136 | + | err error | |||
| 137 | + | probe [1]byte | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func (s *streamBlob) Read(p []byte) (int, error) { | |||
| 141 | + | if s.zr == nil { | |||
| 142 | + | return 0, io.ErrClosedPipe | |||
| 143 | + | } | |||
| 144 | + | if s.err != nil { | |||
| 145 | + | return 0, s.err | |||
| 146 | + | } | |||
| 147 | + | if len(p) == 0 { | |||
| 148 | + | return 0, nil | |||
| 149 | + | } | |||
| 150 | + | if s.left == 0 { | |||
| 151 | + | return 0, s.finish() | |||
| 152 | + | } | |||
| 153 | + | if int64(len(p)) > s.left { | |||
| 154 | + | p = p[:s.left] | |||
| 155 | + | } | |||
| 156 | + | n := 0 | |||
| 157 | + | if len(s.lead) > 0 { | |||
| 158 | + | n = copy(p, s.lead) | |||
| 159 | + | s.lead = s.lead[n:] | |||
| 160 | + | s.left -= int64(n) | |||
| 161 | + | return n, nil | |||
| 162 | + | } | |||
| 163 | + | n, err := s.zr.Read(p) | |||
| 164 | + | s.left -= int64(n) | |||
| 165 | + | if err != nil && !errors.Is(err, io.EOF) { | |||
| 166 | + | s.err = corrupt("blob: %v", err) | |||
| 167 | + | return n, s.err | |||
| 168 | + | } | |||
| 169 | + | if errors.Is(err, io.EOF) && s.left > 0 { | |||
| 170 | + | s.err = corrupt("blob shorter than its declared size") | |||
| 171 | + | return n, s.err | |||
| 172 | + | } | |||
| 173 | + | if n > 0 { | |||
| 174 | + | return n, nil | |||
| 175 | + | } | |||
| 176 | + | if s.left == 0 { | |||
| 177 | + | return 0, s.finish() | |||
| 178 | + | } | |||
| 179 | + | return 0, nil | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // finish checks that the stream ends exactly here and that its checksum is | |||
| 183 | + | // right, then reports EOF. | |||
| 184 | + | func (s *streamBlob) finish() error { | |||
| 185 | + | if !s.done { | |||
| 186 | + | n, err := s.zr.Read(s.probe[:]) | |||
| 187 | + | switch { | |||
| 188 | + | case n != 0: | |||
| 189 | + | s.err = corrupt("blob longer than its declared size") | |||
| 190 | + | case !errors.Is(err, io.EOF): | |||
| 191 | + | s.err = corrupt("blob: %v", err) | |||
| 192 | + | } | |||
| 193 | + | s.done = true | |||
| 194 | + | } | |||
| 195 | + | if s.err != nil { | |||
| 196 | + | return s.err | |||
| 197 | + | } | |||
| 198 | + | return io.EOF | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | func (s *streamBlob) Close() error { | |||
| 202 | + | if s.zr == nil { | |||
| 203 | + | return nil | |||
| 204 | + | } | |||
| 205 | + | s.zr = nil | |||
| 206 | + | if s.src != nil { | |||
| 207 | + | putMem(s.src) | |||
| 208 | + | s.pack.release() | |||
| 209 | + | } | |||
| 210 | + | if s.loose != nil { | |||
| 211 | + | s.loose.close() | |||
| 212 | + | } | |||
| 213 | + | return nil | |||
| 214 | + | } | |||
A cache.go +130 -0
| @@ -0,0 +1,130 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import "sync" | |
| 6 | + | ||
| 7 | + | // cacheShards is the number of independently locked LRU lists. | |
| 8 | + | const cacheShards = 8 | |
| 9 | + | ||
| 10 | + | type cacheKey struct { | |
| 11 | + | pack uint32 | |
| 12 | + | off uint64 | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | type cacheEntry struct { | |
| 16 | + | key cacheKey | |
| 17 | + | typ Type | |
| 18 | + | data []byte // immutable once cached | |
| 19 | + | prev, next *cacheEntry | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // deltaCache is an LRU cache of delta bases bounded in bytes. Cached slices | |
| 23 | + | // are never modified or recycled, so a reader may keep using one after it is | |
| 24 | + | // evicted. | |
| 25 | + | type deltaCache struct { | |
| 26 | + | shards [cacheShards]cacheShard | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | type cacheShard struct { | |
| 30 | + | mu sync.Mutex | |
| 31 | + | m map[cacheKey]*cacheEntry | |
| 32 | + | head, tail *cacheEntry // head is the most recently used | |
| 33 | + | bytes int64 | |
| 34 | + | budget int64 | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func newDeltaCache(budget int64) *deltaCache { | |
| 38 | + | c := &deltaCache{} | |
| 39 | + | for i := range c.shards { | |
| 40 | + | c.shards[i].budget = budget / cacheShards | |
| 41 | + | c.shards[i].m = make(map[cacheKey]*cacheEntry) | |
| 42 | + | } | |
| 43 | + | return c | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (c *deltaCache) shard(k cacheKey) *cacheShard { | |
| 47 | + | h := uint64(k.pack)*0x9e3779b97f4a7c15 ^ k.off*0xff51afd7ed558ccd | |
| 48 | + | return &c.shards[(h>>32)%cacheShards] | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | func (c *deltaCache) get(k cacheKey) (Type, []byte, bool) { | |
| 52 | + | s := c.shard(k) | |
| 53 | + | if s.budget <= 0 { | |
| 54 | + | return 0, nil, false | |
| 55 | + | } | |
| 56 | + | s.mu.Lock() | |
| 57 | + | e := s.m[k] | |
| 58 | + | if e == nil { | |
| 59 | + | s.mu.Unlock() | |
| 60 | + | return 0, nil, false | |
| 61 | + | } | |
| 62 | + | s.moveFront(e) | |
| 63 | + | typ, data := e.typ, e.data | |
| 64 | + | s.mu.Unlock() | |
| 65 | + | return typ, data, true | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // fits reports whether an object of n bytes can be cached. | |
| 69 | + | func (c *deltaCache) fits(n int) bool { | |
| 70 | + | return int64(n)+64 <= c.shards[0].budget | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // putOwned stores data, which the caller must not modify afterwards. | |
| 74 | + | func (c *deltaCache) putOwned(k cacheKey, typ Type, data []byte) { | |
| 75 | + | s := c.shard(k) | |
| 76 | + | n := int64(len(data)) + 64 | |
| 77 | + | if n > s.budget { | |
| 78 | + | return | |
| 79 | + | } | |
| 80 | + | cp := data | |
| 81 | + | s.mu.Lock() | |
| 82 | + | defer s.mu.Unlock() | |
| 83 | + | if e := s.m[k]; e != nil { | |
| 84 | + | s.moveFront(e) | |
| 85 | + | return | |
| 86 | + | } | |
| 87 | + | e := &cacheEntry{key: k, typ: typ, data: cp} | |
| 88 | + | s.m[k] = e | |
| 89 | + | s.pushFront(e) | |
| 90 | + | s.bytes += n | |
| 91 | + | for s.bytes > s.budget && s.tail != nil { | |
| 92 | + | old := s.tail | |
| 93 | + | s.unlink(old) | |
| 94 | + | delete(s.m, old.key) | |
| 95 | + | s.bytes -= int64(len(old.data)) + 64 | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func (s *cacheShard) unlink(e *cacheEntry) { | |
| 100 | + | if e.prev != nil { | |
| 101 | + | e.prev.next = e.next | |
| 102 | + | } else { | |
| 103 | + | s.head = e.next | |
| 104 | + | } | |
| 105 | + | if e.next != nil { | |
| 106 | + | e.next.prev = e.prev | |
| 107 | + | } else { | |
| 108 | + | s.tail = e.prev | |
| 109 | + | } | |
| 110 | + | e.prev, e.next = nil, nil | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func (s *cacheShard) pushFront(e *cacheEntry) { | |
| 114 | + | e.next = s.head | |
| 115 | + | if s.head != nil { | |
| 116 | + | s.head.prev = e | |
| 117 | + | } | |
| 118 | + | s.head = e | |
| 119 | + | if s.tail == nil { | |
| 120 | + | s.tail = e | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | func (s *cacheShard) moveFront(e *cacheEntry) { | |
| 125 | + | if s.head == e { | |
| 126 | + | return | |
| 127 | + | } | |
| 128 | + | s.unlink(e) | |
| 129 | + | s.pushFront(e) | |
| 130 | + | } | |
| @@ -0,0 +1,130 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import "sync" | |||
| 6 | + | ||||
| 7 | + | // cacheShards is the number of independently locked LRU lists. | |||
| 8 | + | const cacheShards = 8 | |||
| 9 | + | ||||
| 10 | + | type cacheKey struct { | |||
| 11 | + | pack uint32 | |||
| 12 | + | off uint64 | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | type cacheEntry struct { | |||
| 16 | + | key cacheKey | |||
| 17 | + | typ Type | |||
| 18 | + | data []byte // immutable once cached | |||
| 19 | + | prev, next *cacheEntry | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // deltaCache is an LRU cache of delta bases bounded in bytes. Cached slices | |||
| 23 | + | // are never modified or recycled, so a reader may keep using one after it is | |||
| 24 | + | // evicted. | |||
| 25 | + | type deltaCache struct { | |||
| 26 | + | shards [cacheShards]cacheShard | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | type cacheShard struct { | |||
| 30 | + | mu sync.Mutex | |||
| 31 | + | m map[cacheKey]*cacheEntry | |||
| 32 | + | head, tail *cacheEntry // head is the most recently used | |||
| 33 | + | bytes int64 | |||
| 34 | + | budget int64 | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func newDeltaCache(budget int64) *deltaCache { | |||
| 38 | + | c := &deltaCache{} | |||
| 39 | + | for i := range c.shards { | |||
| 40 | + | c.shards[i].budget = budget / cacheShards | |||
| 41 | + | c.shards[i].m = make(map[cacheKey]*cacheEntry) | |||
| 42 | + | } | |||
| 43 | + | return c | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (c *deltaCache) shard(k cacheKey) *cacheShard { | |||
| 47 | + | h := uint64(k.pack)*0x9e3779b97f4a7c15 ^ k.off*0xff51afd7ed558ccd | |||
| 48 | + | return &c.shards[(h>>32)%cacheShards] | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | func (c *deltaCache) get(k cacheKey) (Type, []byte, bool) { | |||
| 52 | + | s := c.shard(k) | |||
| 53 | + | if s.budget <= 0 { | |||
| 54 | + | return 0, nil, false | |||
| 55 | + | } | |||
| 56 | + | s.mu.Lock() | |||
| 57 | + | e := s.m[k] | |||
| 58 | + | if e == nil { | |||
| 59 | + | s.mu.Unlock() | |||
| 60 | + | return 0, nil, false | |||
| 61 | + | } | |||
| 62 | + | s.moveFront(e) | |||
| 63 | + | typ, data := e.typ, e.data | |||
| 64 | + | s.mu.Unlock() | |||
| 65 | + | return typ, data, true | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // fits reports whether an object of n bytes can be cached. | |||
| 69 | + | func (c *deltaCache) fits(n int) bool { | |||
| 70 | + | return int64(n)+64 <= c.shards[0].budget | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // putOwned stores data, which the caller must not modify afterwards. | |||
| 74 | + | func (c *deltaCache) putOwned(k cacheKey, typ Type, data []byte) { | |||
| 75 | + | s := c.shard(k) | |||
| 76 | + | n := int64(len(data)) + 64 | |||
| 77 | + | if n > s.budget { | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | cp := data | |||
| 81 | + | s.mu.Lock() | |||
| 82 | + | defer s.mu.Unlock() | |||
| 83 | + | if e := s.m[k]; e != nil { | |||
| 84 | + | s.moveFront(e) | |||
| 85 | + | return | |||
| 86 | + | } | |||
| 87 | + | e := &cacheEntry{key: k, typ: typ, data: cp} | |||
| 88 | + | s.m[k] = e | |||
| 89 | + | s.pushFront(e) | |||
| 90 | + | s.bytes += n | |||
| 91 | + | for s.bytes > s.budget && s.tail != nil { | |||
| 92 | + | old := s.tail | |||
| 93 | + | s.unlink(old) | |||
| 94 | + | delete(s.m, old.key) | |||
| 95 | + | s.bytes -= int64(len(old.data)) + 64 | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func (s *cacheShard) unlink(e *cacheEntry) { | |||
| 100 | + | if e.prev != nil { | |||
| 101 | + | e.prev.next = e.next | |||
| 102 | + | } else { | |||
| 103 | + | s.head = e.next | |||
| 104 | + | } | |||
| 105 | + | if e.next != nil { | |||
| 106 | + | e.next.prev = e.prev | |||
| 107 | + | } else { | |||
| 108 | + | s.tail = e.prev | |||
| 109 | + | } | |||
| 110 | + | e.prev, e.next = nil, nil | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func (s *cacheShard) pushFront(e *cacheEntry) { | |||
| 114 | + | e.next = s.head | |||
| 115 | + | if s.head != nil { | |||
| 116 | + | s.head.prev = e | |||
| 117 | + | } | |||
| 118 | + | s.head = e | |||
| 119 | + | if s.tail == nil { | |||
| 120 | + | s.tail = e | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | func (s *cacheShard) moveFront(e *cacheEntry) { | |||
| 125 | + | if s.head == e { | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 128 | + | s.unlink(e) | |||
| 129 | + | s.pushFront(e) | |||
| 130 | + | } | |||
A commit.go +231 -0
| @@ -0,0 +1,231 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "time" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Signature is the author or committer line of a commit or the tagger of a | |
| 11 | + | // tag. Name and Email point into the buffer of the object they come from. | |
| 12 | + | type Signature struct { | |
| 13 | + | Name []byte | |
| 14 | + | Email []byte | |
| 15 | + | Time int64 // seconds since the Unix epoch | |
| 16 | + | Offset int // time zone, in seconds east of UTC | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // Date returns the time of the signature, in its own time zone. It allocates. | |
| 20 | + | func (s Signature) Date() time.Time { | |
| 21 | + | t := time.Unix(s.Time, 0) | |
| 22 | + | if s.Offset == 0 { | |
| 23 | + | return t.UTC() | |
| 24 | + | } | |
| 25 | + | return t.In(time.FixedZone("", s.Offset)) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and | |
| 29 | + | // ParseCommit overwrite it and reuse its storage, so a hot loop allocates | |
| 30 | + | // nothing after the first few commits. Every []byte field is valid until the | |
| 31 | + | // next call that fills this Commit. | |
| 32 | + | type Commit struct { | |
| 33 | + | ID ObjectID // set by ReadCommit | |
| 34 | + | Tree ObjectID | |
| 35 | + | Parents []ObjectID | |
| 36 | + | Author Signature | |
| 37 | + | Committer Signature | |
| 38 | + | // Encoding is the value of the encoding header, nil when absent. The | |
| 39 | + | // other headers (gpgsig, mergetag...) are skipped. | |
| 40 | + | Encoding []byte | |
| 41 | + | // Message is everything after the blank line that ends the headers, | |
| 42 | + | // as raw bytes: it may not be valid UTF-8. | |
| 43 | + | Message []byte | |
| 44 | + | ||
| 45 | + | buf []byte | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // ReadCommit reads and parses the commit id into c. | |
| 49 | + | func (r *Repo) ReadCommit(id ObjectID, c *Commit) error { | |
| 50 | + | typ, data, err := r.ReadObject(id, c.buf) | |
| 51 | + | if err != nil { | |
| 52 | + | return err | |
| 53 | + | } | |
| 54 | + | c.buf = data | |
| 55 | + | if typ != TypeCommit { | |
| 56 | + | return wrongType(id, typ, TypeCommit) | |
| 57 | + | } | |
| 58 | + | if err := ParseCommit(data, c); err != nil { | |
| 59 | + | return wrapObj(id, err) | |
| 60 | + | } | |
| 61 | + | c.ID = id | |
| 62 | + | return nil | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // ParseCommit parses the content of a commit object into c. The fields of c | |
| 66 | + | // point into data, which must stay unchanged while they are used. It returns | |
| 67 | + | // ErrCorrupt if the commit has no valid tree or a malformed parent. | |
| 68 | + | func ParseCommit(data []byte, c *Commit) error { | |
| 69 | + | c.ID = ObjectID{} | |
| 70 | + | c.Parents = c.Parents[:0] | |
| 71 | + | c.Author, c.Committer = Signature{}, Signature{} | |
| 72 | + | c.Encoding, c.Message = nil, nil | |
| 73 | + | haveTree := false | |
| 74 | + | for len(data) > 0 { | |
| 75 | + | if data[0] == '\n' { | |
| 76 | + | c.Message = data[1:] | |
| 77 | + | break | |
| 78 | + | } | |
| 79 | + | var line []byte | |
| 80 | + | if i := bytes.IndexByte(data, '\n'); i >= 0 { | |
| 81 | + | line, data = data[:i], data[i+1:] | |
| 82 | + | } else { | |
| 83 | + | line, data = data, nil | |
| 84 | + | } | |
| 85 | + | if line[0] == ' ' { | |
| 86 | + | continue // continuation of a multi-line header such as gpgsig | |
| 87 | + | } | |
| 88 | + | key, val, _ := bytes.Cut(line, []byte{' '}) | |
| 89 | + | switch string(key) { | |
| 90 | + | case "tree": | |
| 91 | + | id, ok := parseIDBytes(val) | |
| 92 | + | if !ok || haveTree { | |
| 93 | + | return corrupt("commit: bad tree header") | |
| 94 | + | } | |
| 95 | + | c.Tree, haveTree = id, true | |
| 96 | + | case "parent": | |
| 97 | + | id, ok := parseIDBytes(val) | |
| 98 | + | if !ok { | |
| 99 | + | return corrupt("commit: bad parent header") | |
| 100 | + | } | |
| 101 | + | c.Parents = append(c.Parents, id) | |
| 102 | + | case "author": | |
| 103 | + | c.Author = parseSignature(val) | |
| 104 | + | case "committer": | |
| 105 | + | c.Committer = parseSignature(val) | |
| 106 | + | case "encoding": | |
| 107 | + | c.Encoding = val | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | if !haveTree { | |
| 111 | + | return corrupt("commit: no tree") | |
| 112 | + | } | |
| 113 | + | return nil | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient | |
| 117 | + | // as Git: what cannot be parsed is left zero. | |
| 118 | + | func parseSignature(b []byte) Signature { | |
| 119 | + | var s Signature | |
| 120 | + | lt := bytes.IndexByte(b, '<') | |
| 121 | + | gt := bytes.LastIndexByte(b, '>') | |
| 122 | + | if lt < 0 || gt < lt { | |
| 123 | + | s.Name = bytes.TrimSpace(b) | |
| 124 | + | return s | |
| 125 | + | } | |
| 126 | + | s.Name = bytes.TrimRight(b[:lt], " ") | |
| 127 | + | s.Email = b[lt+1 : gt] | |
| 128 | + | rest := bytes.TrimLeft(b[gt+1:], " ") | |
| 129 | + | secs, rest, _ := bytes.Cut(rest, []byte{' '}) | |
| 130 | + | if t, ok := parseInt(secs); ok { | |
| 131 | + | s.Time = t | |
| 132 | + | if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') { | |
| 133 | + | hh, ok1 := parseInt(tz[1:3]) | |
| 134 | + | mm, ok2 := parseInt(tz[3:5]) | |
| 135 | + | if ok1 && ok2 { | |
| 136 | + | off := int(hh*3600 + mm*60) | |
| 137 | + | if tz[0] == '-' { | |
| 138 | + | off = -off | |
| 139 | + | } | |
| 140 | + | s.Offset = off | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | return s | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // parseInt parses a decimal integer of up to 18 digits. | |
| 148 | + | func parseInt(b []byte) (int64, bool) { | |
| 149 | + | if len(b) == 0 || len(b) > 18 { | |
| 150 | + | return 0, false | |
| 151 | + | } | |
| 152 | + | var v int64 | |
| 153 | + | for _, c := range b { | |
| 154 | + | if c < '0' || c > '9' { | |
| 155 | + | return 0, false | |
| 156 | + | } | |
| 157 | + | v = v*10 + int64(c-'0') | |
| 158 | + | } | |
| 159 | + | return v, true | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // Tag is a parsed annotated tag. Name, Tagger and Message point into the | |
| 163 | + | // data that was parsed. | |
| 164 | + | type Tag struct { | |
| 165 | + | Object ObjectID | |
| 166 | + | Type Type // the type of Object | |
| 167 | + | Name []byte | |
| 168 | + | Tagger Signature | |
| 169 | + | Message []byte | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // ReadTag reads and parses the annotated tag id into t. The byte fields of t | |
| 173 | + | // are copies, owned by the caller. | |
| 174 | + | func (r *Repo) ReadTag(id ObjectID, t *Tag) error { | |
| 175 | + | b, err := r.readTyped(id, TypeTag) | |
| 176 | + | if err != nil { | |
| 177 | + | return err | |
| 178 | + | } | |
| 179 | + | defer putBuf(b) | |
| 180 | + | if err := ParseTag(b.b, t); err != nil { | |
| 181 | + | return wrapObj(id, err) | |
| 182 | + | } | |
| 183 | + | // The pooled buffer goes back: detach the fields. | |
| 184 | + | t.Name = bytes.Clone(t.Name) | |
| 185 | + | t.Message = bytes.Clone(t.Message) | |
| 186 | + | t.Tagger.Name = bytes.Clone(t.Tagger.Name) | |
| 187 | + | t.Tagger.Email = bytes.Clone(t.Tagger.Email) | |
| 188 | + | return nil | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // ParseTag parses the content of a tag object into t, whose byte fields point | |
| 192 | + | // into data. It returns ErrCorrupt if the object or its type is missing. | |
| 193 | + | func ParseTag(data []byte, t *Tag) error { | |
| 194 | + | *t = Tag{} | |
| 195 | + | haveObj, haveType := false, false | |
| 196 | + | for len(data) > 0 { | |
| 197 | + | if data[0] == '\n' { | |
| 198 | + | t.Message = data[1:] | |
| 199 | + | break | |
| 200 | + | } | |
| 201 | + | var line []byte | |
| 202 | + | if i := bytes.IndexByte(data, '\n'); i >= 0 { | |
| 203 | + | line, data = data[:i], data[i+1:] | |
| 204 | + | } else { | |
| 205 | + | line, data = data, nil | |
| 206 | + | } | |
| 207 | + | key, val, _ := bytes.Cut(line, []byte{' '}) | |
| 208 | + | switch string(key) { | |
| 209 | + | case "object": | |
| 210 | + | id, ok := parseIDBytes(val) | |
| 211 | + | if !ok { | |
| 212 | + | return corrupt("tag: bad object header") | |
| 213 | + | } | |
| 214 | + | t.Object, haveObj = id, true | |
| 215 | + | case "type": | |
| 216 | + | typ, ok := parseTypeName(val) | |
| 217 | + | if !ok { | |
| 218 | + | return corrupt("tag: bad type header") | |
| 219 | + | } | |
| 220 | + | t.Type, haveType = typ, true | |
| 221 | + | case "tag": | |
| 222 | + | t.Name = val | |
| 223 | + | case "tagger": | |
| 224 | + | t.Tagger = parseSignature(val) | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | if !haveObj || !haveType { | |
| 228 | + | return corrupt("tag: missing object or type") | |
| 229 | + | } | |
| 230 | + | return nil | |
| 231 | + | } | |
| @@ -0,0 +1,231 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "time" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Signature is the author or committer line of a commit or the tagger of a | |||
| 11 | + | // tag. Name and Email point into the buffer of the object they come from. | |||
| 12 | + | type Signature struct { | |||
| 13 | + | Name []byte | |||
| 14 | + | Email []byte | |||
| 15 | + | Time int64 // seconds since the Unix epoch | |||
| 16 | + | Offset int // time zone, in seconds east of UTC | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // Date returns the time of the signature, in its own time zone. It allocates. | |||
| 20 | + | func (s Signature) Date() time.Time { | |||
| 21 | + | t := time.Unix(s.Time, 0) | |||
| 22 | + | if s.Offset == 0 { | |||
| 23 | + | return t.UTC() | |||
| 24 | + | } | |||
| 25 | + | return t.In(time.FixedZone("", s.Offset)) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and | |||
| 29 | + | // ParseCommit overwrite it and reuse its storage, so a hot loop allocates | |||
| 30 | + | // nothing after the first few commits. Every []byte field is valid until the | |||
| 31 | + | // next call that fills this Commit. | |||
| 32 | + | type Commit struct { | |||
| 33 | + | ID ObjectID // set by ReadCommit | |||
| 34 | + | Tree ObjectID | |||
| 35 | + | Parents []ObjectID | |||
| 36 | + | Author Signature | |||
| 37 | + | Committer Signature | |||
| 38 | + | // Encoding is the value of the encoding header, nil when absent. The | |||
| 39 | + | // other headers (gpgsig, mergetag...) are skipped. | |||
| 40 | + | Encoding []byte | |||
| 41 | + | // Message is everything after the blank line that ends the headers, | |||
| 42 | + | // as raw bytes: it may not be valid UTF-8. | |||
| 43 | + | Message []byte | |||
| 44 | + | ||||
| 45 | + | buf []byte | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // ReadCommit reads and parses the commit id into c. | |||
| 49 | + | func (r *Repo) ReadCommit(id ObjectID, c *Commit) error { | |||
| 50 | + | typ, data, err := r.ReadObject(id, c.buf) | |||
| 51 | + | if err != nil { | |||
| 52 | + | return err | |||
| 53 | + | } | |||
| 54 | + | c.buf = data | |||
| 55 | + | if typ != TypeCommit { | |||
| 56 | + | return wrongType(id, typ, TypeCommit) | |||
| 57 | + | } | |||
| 58 | + | if err := ParseCommit(data, c); err != nil { | |||
| 59 | + | return wrapObj(id, err) | |||
| 60 | + | } | |||
| 61 | + | c.ID = id | |||
| 62 | + | return nil | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // ParseCommit parses the content of a commit object into c. The fields of c | |||
| 66 | + | // point into data, which must stay unchanged while they are used. It returns | |||
| 67 | + | // ErrCorrupt if the commit has no valid tree or a malformed parent. | |||
| 68 | + | func ParseCommit(data []byte, c *Commit) error { | |||
| 69 | + | c.ID = ObjectID{} | |||
| 70 | + | c.Parents = c.Parents[:0] | |||
| 71 | + | c.Author, c.Committer = Signature{}, Signature{} | |||
| 72 | + | c.Encoding, c.Message = nil, nil | |||
| 73 | + | haveTree := false | |||
| 74 | + | for len(data) > 0 { | |||
| 75 | + | if data[0] == '\n' { | |||
| 76 | + | c.Message = data[1:] | |||
| 77 | + | break | |||
| 78 | + | } | |||
| 79 | + | var line []byte | |||
| 80 | + | if i := bytes.IndexByte(data, '\n'); i >= 0 { | |||
| 81 | + | line, data = data[:i], data[i+1:] | |||
| 82 | + | } else { | |||
| 83 | + | line, data = data, nil | |||
| 84 | + | } | |||
| 85 | + | if line[0] == ' ' { | |||
| 86 | + | continue // continuation of a multi-line header such as gpgsig | |||
| 87 | + | } | |||
| 88 | + | key, val, _ := bytes.Cut(line, []byte{' '}) | |||
| 89 | + | switch string(key) { | |||
| 90 | + | case "tree": | |||
| 91 | + | id, ok := parseIDBytes(val) | |||
| 92 | + | if !ok || haveTree { | |||
| 93 | + | return corrupt("commit: bad tree header") | |||
| 94 | + | } | |||
| 95 | + | c.Tree, haveTree = id, true | |||
| 96 | + | case "parent": | |||
| 97 | + | id, ok := parseIDBytes(val) | |||
| 98 | + | if !ok { | |||
| 99 | + | return corrupt("commit: bad parent header") | |||
| 100 | + | } | |||
| 101 | + | c.Parents = append(c.Parents, id) | |||
| 102 | + | case "author": | |||
| 103 | + | c.Author = parseSignature(val) | |||
| 104 | + | case "committer": | |||
| 105 | + | c.Committer = parseSignature(val) | |||
| 106 | + | case "encoding": | |||
| 107 | + | c.Encoding = val | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | if !haveTree { | |||
| 111 | + | return corrupt("commit: no tree") | |||
| 112 | + | } | |||
| 113 | + | return nil | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient | |||
| 117 | + | // as Git: what cannot be parsed is left zero. | |||
| 118 | + | func parseSignature(b []byte) Signature { | |||
| 119 | + | var s Signature | |||
| 120 | + | lt := bytes.IndexByte(b, '<') | |||
| 121 | + | gt := bytes.LastIndexByte(b, '>') | |||
| 122 | + | if lt < 0 || gt < lt { | |||
| 123 | + | s.Name = bytes.TrimSpace(b) | |||
| 124 | + | return s | |||
| 125 | + | } | |||
| 126 | + | s.Name = bytes.TrimRight(b[:lt], " ") | |||
| 127 | + | s.Email = b[lt+1 : gt] | |||
| 128 | + | rest := bytes.TrimLeft(b[gt+1:], " ") | |||
| 129 | + | secs, rest, _ := bytes.Cut(rest, []byte{' '}) | |||
| 130 | + | if t, ok := parseInt(secs); ok { | |||
| 131 | + | s.Time = t | |||
| 132 | + | if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') { | |||
| 133 | + | hh, ok1 := parseInt(tz[1:3]) | |||
| 134 | + | mm, ok2 := parseInt(tz[3:5]) | |||
| 135 | + | if ok1 && ok2 { | |||
| 136 | + | off := int(hh*3600 + mm*60) | |||
| 137 | + | if tz[0] == '-' { | |||
| 138 | + | off = -off | |||
| 139 | + | } | |||
| 140 | + | s.Offset = off | |||
| 141 | + | } | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | return s | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // parseInt parses a decimal integer of up to 18 digits. | |||
| 148 | + | func parseInt(b []byte) (int64, bool) { | |||
| 149 | + | if len(b) == 0 || len(b) > 18 { | |||
| 150 | + | return 0, false | |||
| 151 | + | } | |||
| 152 | + | var v int64 | |||
| 153 | + | for _, c := range b { | |||
| 154 | + | if c < '0' || c > '9' { | |||
| 155 | + | return 0, false | |||
| 156 | + | } | |||
| 157 | + | v = v*10 + int64(c-'0') | |||
| 158 | + | } | |||
| 159 | + | return v, true | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // Tag is a parsed annotated tag. Name, Tagger and Message point into the | |||
| 163 | + | // data that was parsed. | |||
| 164 | + | type Tag struct { | |||
| 165 | + | Object ObjectID | |||
| 166 | + | Type Type // the type of Object | |||
| 167 | + | Name []byte | |||
| 168 | + | Tagger Signature | |||
| 169 | + | Message []byte | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // ReadTag reads and parses the annotated tag id into t. The byte fields of t | |||
| 173 | + | // are copies, owned by the caller. | |||
| 174 | + | func (r *Repo) ReadTag(id ObjectID, t *Tag) error { | |||
| 175 | + | b, err := r.readTyped(id, TypeTag) | |||
| 176 | + | if err != nil { | |||
| 177 | + | return err | |||
| 178 | + | } | |||
| 179 | + | defer putBuf(b) | |||
| 180 | + | if err := ParseTag(b.b, t); err != nil { | |||
| 181 | + | return wrapObj(id, err) | |||
| 182 | + | } | |||
| 183 | + | // The pooled buffer goes back: detach the fields. | |||
| 184 | + | t.Name = bytes.Clone(t.Name) | |||
| 185 | + | t.Message = bytes.Clone(t.Message) | |||
| 186 | + | t.Tagger.Name = bytes.Clone(t.Tagger.Name) | |||
| 187 | + | t.Tagger.Email = bytes.Clone(t.Tagger.Email) | |||
| 188 | + | return nil | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | // ParseTag parses the content of a tag object into t, whose byte fields point | |||
| 192 | + | // into data. It returns ErrCorrupt if the object or its type is missing. | |||
| 193 | + | func ParseTag(data []byte, t *Tag) error { | |||
| 194 | + | *t = Tag{} | |||
| 195 | + | haveObj, haveType := false, false | |||
| 196 | + | for len(data) > 0 { | |||
| 197 | + | if data[0] == '\n' { | |||
| 198 | + | t.Message = data[1:] | |||
| 199 | + | break | |||
| 200 | + | } | |||
| 201 | + | var line []byte | |||
| 202 | + | if i := bytes.IndexByte(data, '\n'); i >= 0 { | |||
| 203 | + | line, data = data[:i], data[i+1:] | |||
| 204 | + | } else { | |||
| 205 | + | line, data = data, nil | |||
| 206 | + | } | |||
| 207 | + | key, val, _ := bytes.Cut(line, []byte{' '}) | |||
| 208 | + | switch string(key) { | |||
| 209 | + | case "object": | |||
| 210 | + | id, ok := parseIDBytes(val) | |||
| 211 | + | if !ok { | |||
| 212 | + | return corrupt("tag: bad object header") | |||
| 213 | + | } | |||
| 214 | + | t.Object, haveObj = id, true | |||
| 215 | + | case "type": | |||
| 216 | + | typ, ok := parseTypeName(val) | |||
| 217 | + | if !ok { | |||
| 218 | + | return corrupt("tag: bad type header") | |||
| 219 | + | } | |||
| 220 | + | t.Type, haveType = typ, true | |||
| 221 | + | case "tag": | |||
| 222 | + | t.Name = val | |||
| 223 | + | case "tagger": | |||
| 224 | + | t.Tagger = parseSignature(val) | |||
| 225 | + | } | |||
| 226 | + | } | |||
| 227 | + | if !haveObj || !haveType { | |||
| 228 | + | return corrupt("tag: missing object or type") | |||
| 229 | + | } | |||
| 230 | + | return nil | |||
| 231 | + | } | |||
A config.go +116 -0
| @@ -0,0 +1,116 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "errors" | |
| 8 | + | "io/fs" | |
| 9 | + | "os" | |
| 10 | + | "strconv" | |
| 11 | + | "strings" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // maxConfigSize bounds the config file that is read. | |
| 15 | + | const maxConfigSize = 1 << 20 | |
| 16 | + | ||
| 17 | + | // checkConfig reads the few settings that decide whether the repository is | |
| 18 | + | // readable: core.repositoryformatversion and the extensions.* keys. Other | |
| 19 | + | // settings, includes and conditional includes are ignored. | |
| 20 | + | func checkConfig(path string) error { | |
| 21 | + | data, err := os.ReadFile(path) | |
| 22 | + | if errors.Is(err, fs.ErrNotExist) { | |
| 23 | + | return nil | |
| 24 | + | } | |
| 25 | + | if err != nil { | |
| 26 | + | return err | |
| 27 | + | } | |
| 28 | + | if len(data) > maxConfigSize { | |
| 29 | + | return unsupported("config file larger than %d bytes", maxConfigSize) | |
| 30 | + | } | |
| 31 | + | return checkConfigData(data) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func checkConfigData(data []byte) error { | |
| 35 | + | section := "" | |
| 36 | + | version := int64(0) | |
| 37 | + | for len(data) > 0 { | |
| 38 | + | var line []byte | |
| 39 | + | line, data, _ = bytes.Cut(data, []byte{'\n'}) | |
| 40 | + | line = bytes.TrimSpace(line) | |
| 41 | + | if len(line) == 0 || line[0] == '#' || line[0] == ';' { | |
| 42 | + | continue | |
| 43 | + | } | |
| 44 | + | if line[0] == '[' { | |
| 45 | + | end := bytes.IndexByte(line, ']') | |
| 46 | + | if end < 0 { | |
| 47 | + | return corrupt("config: bad section line %q", truncate(string(line))) | |
| 48 | + | } | |
| 49 | + | head := string(bytes.TrimSpace(line[1:end])) | |
| 50 | + | // "[core]", "[remote \"origin\"]", "[branch.main]" | |
| 51 | + | name, _, _ := strings.Cut(head, " ") | |
| 52 | + | name, _, _ = strings.Cut(name, ".") | |
| 53 | + | section = strings.ToLower(name) | |
| 54 | + | line = bytes.TrimSpace(line[end+1:]) | |
| 55 | + | if len(line) == 0 || line[0] == '#' || line[0] == ';' { | |
| 56 | + | continue | |
| 57 | + | } | |
| 58 | + | // "[core] bare = true" on one line. | |
| 59 | + | } | |
| 60 | + | keyb, valb, _ := bytes.Cut(line, []byte{'='}) | |
| 61 | + | key := strings.ToLower(string(bytes.TrimSpace(keyb))) | |
| 62 | + | val := configValue(valb) | |
| 63 | + | switch section { | |
| 64 | + | case "core": | |
| 65 | + | if key == "repositoryformatversion" { | |
| 66 | + | v, err := strconv.ParseInt(val, 10, 64) | |
| 67 | + | if err != nil { | |
| 68 | + | return corrupt("config: core.repositoryformatversion %q", truncate(val)) | |
| 69 | + | } | |
| 70 | + | version = v | |
| 71 | + | } | |
| 72 | + | case "extensions": | |
| 73 | + | switch key { | |
| 74 | + | case "objectformat": | |
| 75 | + | if !strings.EqualFold(val, "sha1") { | |
| 76 | + | return unsupported("extensions.objectformat = %s", truncate(val)) | |
| 77 | + | } | |
| 78 | + | case "refstorage": | |
| 79 | + | if !strings.EqualFold(val, "files") { | |
| 80 | + | return unsupported("extensions.refstorage = %s", truncate(val)) | |
| 81 | + | } | |
| 82 | + | case "noop", "worktreeconfig": | |
| 83 | + | // No effect on reading a bare repository. | |
| 84 | + | default: | |
| 85 | + | return unsupported("extensions.%s", truncate(key)) | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | if version != 0 && version != 1 { | |
| 90 | + | return unsupported("core.repositoryformatversion = %d", version) | |
| 91 | + | } | |
| 92 | + | return nil | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // configValue returns the unquoted value of a config line, without a trailing | |
| 96 | + | // comment. | |
| 97 | + | func configValue(b []byte) string { | |
| 98 | + | b = bytes.TrimSpace(b) | |
| 99 | + | var sb strings.Builder | |
| 100 | + | inQuote := false | |
| 101 | + | for i := 0; i < len(b); i++ { | |
| 102 | + | c := b[i] | |
| 103 | + | switch { | |
| 104 | + | case c == '\\' && i+1 < len(b): | |
| 105 | + | i++ | |
| 106 | + | sb.WriteByte(b[i]) | |
| 107 | + | case c == '"': | |
| 108 | + | inQuote = !inQuote | |
| 109 | + | case !inQuote && (c == '#' || c == ';'): | |
| 110 | + | return strings.TrimSpace(sb.String()) | |
| 111 | + | default: | |
| 112 | + | sb.WriteByte(c) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | return strings.TrimSpace(sb.String()) | |
| 116 | + | } | |
| @@ -0,0 +1,116 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "errors" | |||
| 8 | + | "io/fs" | |||
| 9 | + | "os" | |||
| 10 | + | "strconv" | |||
| 11 | + | "strings" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // maxConfigSize bounds the config file that is read. | |||
| 15 | + | const maxConfigSize = 1 << 20 | |||
| 16 | + | ||||
| 17 | + | // checkConfig reads the few settings that decide whether the repository is | |||
| 18 | + | // readable: core.repositoryformatversion and the extensions.* keys. Other | |||
| 19 | + | // settings, includes and conditional includes are ignored. | |||
| 20 | + | func checkConfig(path string) error { | |||
| 21 | + | data, err := os.ReadFile(path) | |||
| 22 | + | if errors.Is(err, fs.ErrNotExist) { | |||
| 23 | + | return nil | |||
| 24 | + | } | |||
| 25 | + | if err != nil { | |||
| 26 | + | return err | |||
| 27 | + | } | |||
| 28 | + | if len(data) > maxConfigSize { | |||
| 29 | + | return unsupported("config file larger than %d bytes", maxConfigSize) | |||
| 30 | + | } | |||
| 31 | + | return checkConfigData(data) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func checkConfigData(data []byte) error { | |||
| 35 | + | section := "" | |||
| 36 | + | version := int64(0) | |||
| 37 | + | for len(data) > 0 { | |||
| 38 | + | var line []byte | |||
| 39 | + | line, data, _ = bytes.Cut(data, []byte{'\n'}) | |||
| 40 | + | line = bytes.TrimSpace(line) | |||
| 41 | + | if len(line) == 0 || line[0] == '#' || line[0] == ';' { | |||
| 42 | + | continue | |||
| 43 | + | } | |||
| 44 | + | if line[0] == '[' { | |||
| 45 | + | end := bytes.IndexByte(line, ']') | |||
| 46 | + | if end < 0 { | |||
| 47 | + | return corrupt("config: bad section line %q", truncate(string(line))) | |||
| 48 | + | } | |||
| 49 | + | head := string(bytes.TrimSpace(line[1:end])) | |||
| 50 | + | // "[core]", "[remote \"origin\"]", "[branch.main]" | |||
| 51 | + | name, _, _ := strings.Cut(head, " ") | |||
| 52 | + | name, _, _ = strings.Cut(name, ".") | |||
| 53 | + | section = strings.ToLower(name) | |||
| 54 | + | line = bytes.TrimSpace(line[end+1:]) | |||
| 55 | + | if len(line) == 0 || line[0] == '#' || line[0] == ';' { | |||
| 56 | + | continue | |||
| 57 | + | } | |||
| 58 | + | // "[core] bare = true" on one line. | |||
| 59 | + | } | |||
| 60 | + | keyb, valb, _ := bytes.Cut(line, []byte{'='}) | |||
| 61 | + | key := strings.ToLower(string(bytes.TrimSpace(keyb))) | |||
| 62 | + | val := configValue(valb) | |||
| 63 | + | switch section { | |||
| 64 | + | case "core": | |||
| 65 | + | if key == "repositoryformatversion" { | |||
| 66 | + | v, err := strconv.ParseInt(val, 10, 64) | |||
| 67 | + | if err != nil { | |||
| 68 | + | return corrupt("config: core.repositoryformatversion %q", truncate(val)) | |||
| 69 | + | } | |||
| 70 | + | version = v | |||
| 71 | + | } | |||
| 72 | + | case "extensions": | |||
| 73 | + | switch key { | |||
| 74 | + | case "objectformat": | |||
| 75 | + | if !strings.EqualFold(val, "sha1") { | |||
| 76 | + | return unsupported("extensions.objectformat = %s", truncate(val)) | |||
| 77 | + | } | |||
| 78 | + | case "refstorage": | |||
| 79 | + | if !strings.EqualFold(val, "files") { | |||
| 80 | + | return unsupported("extensions.refstorage = %s", truncate(val)) | |||
| 81 | + | } | |||
| 82 | + | case "noop", "worktreeconfig": | |||
| 83 | + | // No effect on reading a bare repository. | |||
| 84 | + | default: | |||
| 85 | + | return unsupported("extensions.%s", truncate(key)) | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | if version != 0 && version != 1 { | |||
| 90 | + | return unsupported("core.repositoryformatversion = %d", version) | |||
| 91 | + | } | |||
| 92 | + | return nil | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // configValue returns the unquoted value of a config line, without a trailing | |||
| 96 | + | // comment. | |||
| 97 | + | func configValue(b []byte) string { | |||
| 98 | + | b = bytes.TrimSpace(b) | |||
| 99 | + | var sb strings.Builder | |||
| 100 | + | inQuote := false | |||
| 101 | + | for i := 0; i < len(b); i++ { | |||
| 102 | + | c := b[i] | |||
| 103 | + | switch { | |||
| 104 | + | case c == '\\' && i+1 < len(b): | |||
| 105 | + | i++ | |||
| 106 | + | sb.WriteByte(b[i]) | |||
| 107 | + | case c == '"': | |||
| 108 | + | inQuote = !inQuote | |||
| 109 | + | case !inQuote && (c == '#' || c == ';'): | |||
| 110 | + | return strings.TrimSpace(sb.String()) | |||
| 111 | + | default: | |||
| 112 | + | sb.WriteByte(c) | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | return strings.TrimSpace(sb.String()) | |||
| 116 | + | } | |||
A delta.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | // deltaVarint reads a little-endian base-128 size of a delta header. | |
| 6 | + | func deltaVarint(d []byte, p int) (v uint64, next int, ok bool) { | |
| 7 | + | var shift uint | |
| 8 | + | for p < len(d) { | |
| 9 | + | c := d[p] | |
| 10 | + | p++ | |
| 11 | + | if shift > 56 { | |
| 12 | + | return 0, 0, false | |
| 13 | + | } | |
| 14 | + | v |= uint64(c&0x7f) << shift | |
| 15 | + | shift += 7 | |
| 16 | + | if c&0x80 == 0 { | |
| 17 | + | return v, p, true | |
| 18 | + | } | |
| 19 | + | } | |
| 20 | + | return 0, 0, false | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // deltaHeader parses the base size and the result size of a delta. | |
| 24 | + | func deltaHeader(d []byte) (baseSize, resultSize uint64, body int, err error) { | |
| 25 | + | var ok bool | |
| 26 | + | baseSize, p, ok := deltaVarint(d, 0) | |
| 27 | + | if !ok { | |
| 28 | + | return 0, 0, 0, corrupt("delta: bad base size") | |
| 29 | + | } | |
| 30 | + | resultSize, p, ok = deltaVarint(d, p) | |
| 31 | + | if !ok { | |
| 32 | + | return 0, 0, 0, corrupt("delta: bad result size") | |
| 33 | + | } | |
| 34 | + | return baseSize, resultSize, p, nil | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // applyDelta applies delta to base and writes the result into dst, whose | |
| 38 | + | // length is the result size announced by the delta header (checked by the | |
| 39 | + | // caller with deltaHeader). Every copy and insert is bounds checked. | |
| 40 | + | func applyDelta(dst, base, delta []byte, body int) error { | |
| 41 | + | out := 0 | |
| 42 | + | p := body | |
| 43 | + | for p < len(delta) { | |
| 44 | + | op := delta[p] | |
| 45 | + | p++ | |
| 46 | + | switch { | |
| 47 | + | case op&0x80 != 0: | |
| 48 | + | var off, size uint64 | |
| 49 | + | for i := uint(0); i < 4; i++ { | |
| 50 | + | if op&(1<<i) != 0 { | |
| 51 | + | if p >= len(delta) { | |
| 52 | + | return corrupt("delta: truncated copy offset") | |
| 53 | + | } | |
| 54 | + | off |= uint64(delta[p]) << (8 * i) | |
| 55 | + | p++ | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | for i := uint(0); i < 3; i++ { | |
| 59 | + | if op&(0x10<<i) != 0 { | |
| 60 | + | if p >= len(delta) { | |
| 61 | + | return corrupt("delta: truncated copy size") | |
| 62 | + | } | |
| 63 | + | size |= uint64(delta[p]) << (8 * i) | |
| 64 | + | p++ | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | if size == 0 { | |
| 68 | + | size = 0x10000 | |
| 69 | + | } | |
| 70 | + | if off > uint64(len(base)) || size > uint64(len(base))-off { | |
| 71 | + | return corrupt("delta: copy %d+%d outside base of %d", off, size, len(base)) | |
| 72 | + | } | |
| 73 | + | if size > uint64(len(dst)-out) { | |
| 74 | + | return corrupt("delta: result longer than announced") | |
| 75 | + | } | |
| 76 | + | copy(dst[out:], base[off:off+size]) | |
| 77 | + | out += int(size) | |
| 78 | + | case op != 0: | |
| 79 | + | n := int(op) | |
| 80 | + | if n > len(delta)-p { | |
| 81 | + | return corrupt("delta: truncated insert") | |
| 82 | + | } | |
| 83 | + | if n > len(dst)-out { | |
| 84 | + | return corrupt("delta: result longer than announced") | |
| 85 | + | } | |
| 86 | + | copy(dst[out:], delta[p:p+n]) | |
| 87 | + | p += n | |
| 88 | + | out += n | |
| 89 | + | default: | |
| 90 | + | return corrupt("delta: reserved opcode 0") | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | if out != len(dst) { | |
| 94 | + | return corrupt("delta: result of %d bytes, %d announced", out, len(dst)) | |
| 95 | + | } | |
| 96 | + | return nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // patch applies a delta to base and returns the result, which is at most | |
| 100 | + | // limit bytes. It is the allocating form of applyDelta, for tests and fuzzing. | |
| 101 | + | func patch(base, delta []byte, limit int64) ([]byte, error) { | |
| 102 | + | bs, rs, body, err := deltaHeader(delta) | |
| 103 | + | if err != nil { | |
| 104 | + | return nil, err | |
| 105 | + | } | |
| 106 | + | if bs != uint64(len(base)) { | |
| 107 | + | return nil, corrupt("delta: base is %d bytes, delta expects %d", len(base), bs) | |
| 108 | + | } | |
| 109 | + | if rs > uint64(limit) { | |
| 110 | + | return nil, ErrTooLarge | |
| 111 | + | } | |
| 112 | + | dst := make([]byte, rs) | |
| 113 | + | if err := applyDelta(dst, base, delta, body); err != nil { | |
| 114 | + | return nil, err | |
| 115 | + | } | |
| 116 | + | return dst, nil | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | // deltaVarint reads a little-endian base-128 size of a delta header. | |||
| 6 | + | func deltaVarint(d []byte, p int) (v uint64, next int, ok bool) { | |||
| 7 | + | var shift uint | |||
| 8 | + | for p < len(d) { | |||
| 9 | + | c := d[p] | |||
| 10 | + | p++ | |||
| 11 | + | if shift > 56 { | |||
| 12 | + | return 0, 0, false | |||
| 13 | + | } | |||
| 14 | + | v |= uint64(c&0x7f) << shift | |||
| 15 | + | shift += 7 | |||
| 16 | + | if c&0x80 == 0 { | |||
| 17 | + | return v, p, true | |||
| 18 | + | } | |||
| 19 | + | } | |||
| 20 | + | return 0, 0, false | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // deltaHeader parses the base size and the result size of a delta. | |||
| 24 | + | func deltaHeader(d []byte) (baseSize, resultSize uint64, body int, err error) { | |||
| 25 | + | var ok bool | |||
| 26 | + | baseSize, p, ok := deltaVarint(d, 0) | |||
| 27 | + | if !ok { | |||
| 28 | + | return 0, 0, 0, corrupt("delta: bad base size") | |||
| 29 | + | } | |||
| 30 | + | resultSize, p, ok = deltaVarint(d, p) | |||
| 31 | + | if !ok { | |||
| 32 | + | return 0, 0, 0, corrupt("delta: bad result size") | |||
| 33 | + | } | |||
| 34 | + | return baseSize, resultSize, p, nil | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // applyDelta applies delta to base and writes the result into dst, whose | |||
| 38 | + | // length is the result size announced by the delta header (checked by the | |||
| 39 | + | // caller with deltaHeader). Every copy and insert is bounds checked. | |||
| 40 | + | func applyDelta(dst, base, delta []byte, body int) error { | |||
| 41 | + | out := 0 | |||
| 42 | + | p := body | |||
| 43 | + | for p < len(delta) { | |||
| 44 | + | op := delta[p] | |||
| 45 | + | p++ | |||
| 46 | + | switch { | |||
| 47 | + | case op&0x80 != 0: | |||
| 48 | + | var off, size uint64 | |||
| 49 | + | for i := uint(0); i < 4; i++ { | |||
| 50 | + | if op&(1<<i) != 0 { | |||
| 51 | + | if p >= len(delta) { | |||
| 52 | + | return corrupt("delta: truncated copy offset") | |||
| 53 | + | } | |||
| 54 | + | off |= uint64(delta[p]) << (8 * i) | |||
| 55 | + | p++ | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | for i := uint(0); i < 3; i++ { | |||
| 59 | + | if op&(0x10<<i) != 0 { | |||
| 60 | + | if p >= len(delta) { | |||
| 61 | + | return corrupt("delta: truncated copy size") | |||
| 62 | + | } | |||
| 63 | + | size |= uint64(delta[p]) << (8 * i) | |||
| 64 | + | p++ | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | if size == 0 { | |||
| 68 | + | size = 0x10000 | |||
| 69 | + | } | |||
| 70 | + | if off > uint64(len(base)) || size > uint64(len(base))-off { | |||
| 71 | + | return corrupt("delta: copy %d+%d outside base of %d", off, size, len(base)) | |||
| 72 | + | } | |||
| 73 | + | if size > uint64(len(dst)-out) { | |||
| 74 | + | return corrupt("delta: result longer than announced") | |||
| 75 | + | } | |||
| 76 | + | copy(dst[out:], base[off:off+size]) | |||
| 77 | + | out += int(size) | |||
| 78 | + | case op != 0: | |||
| 79 | + | n := int(op) | |||
| 80 | + | if n > len(delta)-p { | |||
| 81 | + | return corrupt("delta: truncated insert") | |||
| 82 | + | } | |||
| 83 | + | if n > len(dst)-out { | |||
| 84 | + | return corrupt("delta: result longer than announced") | |||
| 85 | + | } | |||
| 86 | + | copy(dst[out:], delta[p:p+n]) | |||
| 87 | + | p += n | |||
| 88 | + | out += n | |||
| 89 | + | default: | |||
| 90 | + | return corrupt("delta: reserved opcode 0") | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | if out != len(dst) { | |||
| 94 | + | return corrupt("delta: result of %d bytes, %d announced", out, len(dst)) | |||
| 95 | + | } | |||
| 96 | + | return nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // patch applies a delta to base and returns the result, which is at most | |||
| 100 | + | // limit bytes. It is the allocating form of applyDelta, for tests and fuzzing. | |||
| 101 | + | func patch(base, delta []byte, limit int64) ([]byte, error) { | |||
| 102 | + | bs, rs, body, err := deltaHeader(delta) | |||
| 103 | + | if err != nil { | |||
| 104 | + | return nil, err | |||
| 105 | + | } | |||
| 106 | + | if bs != uint64(len(base)) { | |||
| 107 | + | return nil, corrupt("delta: base is %d bytes, delta expects %d", len(base), bs) | |||
| 108 | + | } | |||
| 109 | + | if rs > uint64(limit) { | |||
| 110 | + | return nil, ErrTooLarge | |||
| 111 | + | } | |||
| 112 | + | dst := make([]byte, rs) | |||
| 113 | + | if err := applyDelta(dst, base, delta, body); err != nil { | |||
| 114 | + | return nil, err | |||
| 115 | + | } | |||
| 116 | + | return dst, nil | |||
| 117 | + | } | |||
A errors.go +46 -0
| @@ -0,0 +1,46 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "fmt" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Errors returned by the package. They are wrapped with context, so test them | |
| 11 | + | // with errors.Is. | |
| 12 | + | var ( | |
| 13 | + | // ErrNotFound reports a missing object, ref or path, or a directory that | |
| 14 | + | // is not a Git repository. | |
| 15 | + | ErrNotFound = errors.New("git: not found") | |
| 16 | + | // ErrUnsupported reports a repository feature the package does not read: | |
| 17 | + | // the error is wrapped with the reason. | |
| 18 | + | ErrUnsupported = errors.New("git: unsupported repository") | |
| 19 | + | // ErrAmbiguous reports an abbreviated object id that matches several | |
| 20 | + | // objects. | |
| 21 | + | ErrAmbiguous = errors.New("git: ambiguous object id") | |
| 22 | + | // ErrCorrupt reports malformed repository data. Reading never panics on | |
| 23 | + | // such data. | |
| 24 | + | ErrCorrupt = errors.New("git: corrupt data") | |
| 25 | + | // ErrInvalidID reports a malformed object id or abbreviation. | |
| 26 | + | ErrInvalidID = errors.New("git: invalid object id") | |
| 27 | + | // ErrWrongType reports an object that does not have the expected type. | |
| 28 | + | ErrWrongType = errors.New("git: wrong object type") | |
| 29 | + | // ErrTooLarge reports an object bigger than the configured bound | |
| 30 | + | // (see WithMaxObjectSize). | |
| 31 | + | ErrTooLarge = errors.New("git: object too large") | |
| 32 | + | // ErrClosed reports the use of a closed Repo. | |
| 33 | + | ErrClosed = errors.New("git: repository closed") | |
| 34 | + | ) | |
| 35 | + | ||
| 36 | + | func corrupt(format string, a ...any) error { | |
| 37 | + | return fmt.Errorf("%w: %s", ErrCorrupt, fmt.Sprintf(format, a...)) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func unsupported(format string, a ...any) error { | |
| 41 | + | return fmt.Errorf("%w: %s", ErrUnsupported, fmt.Sprintf(format, a...)) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func notFound(format string, a ...any) error { | |
| 45 | + | return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, a...)) | |
| 46 | + | } | |
| @@ -0,0 +1,46 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "fmt" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Errors returned by the package. They are wrapped with context, so test them | |||
| 11 | + | // with errors.Is. | |||
| 12 | + | var ( | |||
| 13 | + | // ErrNotFound reports a missing object, ref or path, or a directory that | |||
| 14 | + | // is not a Git repository. | |||
| 15 | + | ErrNotFound = errors.New("git: not found") | |||
| 16 | + | // ErrUnsupported reports a repository feature the package does not read: | |||
| 17 | + | // the error is wrapped with the reason. | |||
| 18 | + | ErrUnsupported = errors.New("git: unsupported repository") | |||
| 19 | + | // ErrAmbiguous reports an abbreviated object id that matches several | |||
| 20 | + | // objects. | |||
| 21 | + | ErrAmbiguous = errors.New("git: ambiguous object id") | |||
| 22 | + | // ErrCorrupt reports malformed repository data. Reading never panics on | |||
| 23 | + | // such data. | |||
| 24 | + | ErrCorrupt = errors.New("git: corrupt data") | |||
| 25 | + | // ErrInvalidID reports a malformed object id or abbreviation. | |||
| 26 | + | ErrInvalidID = errors.New("git: invalid object id") | |||
| 27 | + | // ErrWrongType reports an object that does not have the expected type. | |||
| 28 | + | ErrWrongType = errors.New("git: wrong object type") | |||
| 29 | + | // ErrTooLarge reports an object bigger than the configured bound | |||
| 30 | + | // (see WithMaxObjectSize). | |||
| 31 | + | ErrTooLarge = errors.New("git: object too large") | |||
| 32 | + | // ErrClosed reports the use of a closed Repo. | |||
| 33 | + | ErrClosed = errors.New("git: repository closed") | |||
| 34 | + | ) | |||
| 35 | + | ||||
| 36 | + | func corrupt(format string, a ...any) error { | |||
| 37 | + | return fmt.Errorf("%w: %s", ErrCorrupt, fmt.Sprintf(format, a...)) | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func unsupported(format string, a ...any) error { | |||
| 41 | + | return fmt.Errorf("%w: %s", ErrUnsupported, fmt.Sprintf(format, a...)) | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func notFound(format string, a ...any) error { | |||
| 45 | + | return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, a...)) | |||
| 46 | + | } | |||
A id.go +81 -0
| @@ -0,0 +1,81 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import "fmt" | |
| 6 | + | ||
| 7 | + | // ObjectID is a SHA-1 object id. | |
| 8 | + | type ObjectID [20]byte | |
| 9 | + | ||
| 10 | + | const hexDigits = "0123456789abcdef" | |
| 11 | + | ||
| 12 | + | // oidHexVal maps a hex digit to its value, or 0xff. | |
| 13 | + | var oidHexVal = func() (t [256]byte) { | |
| 14 | + | for i := range t { | |
| 15 | + | t[i] = 0xff | |
| 16 | + | } | |
| 17 | + | for i := 0; i < 10; i++ { | |
| 18 | + | t['0'+i] = byte(i) | |
| 19 | + | } | |
| 20 | + | for i := 0; i < 6; i++ { | |
| 21 | + | t['a'+i] = byte(10 + i) | |
| 22 | + | t['A'+i] = byte(10 + i) | |
| 23 | + | } | |
| 24 | + | return t | |
| 25 | + | }() | |
| 26 | + | ||
| 27 | + | // ParseID parses 40 hexadecimal digits. | |
| 28 | + | func ParseID(s string) (ObjectID, error) { | |
| 29 | + | var id ObjectID | |
| 30 | + | if len(s) != 40 { | |
| 31 | + | return id, fmt.Errorf("%w: %q has %d characters, want 40", ErrInvalidID, truncate(s), len(s)) | |
| 32 | + | } | |
| 33 | + | for i := 0; i < 20; i++ { | |
| 34 | + | hi, lo := oidHexVal[s[2*i]], oidHexVal[s[2*i+1]] | |
| 35 | + | if hi|lo > 0x0f { | |
| 36 | + | return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(s)) | |
| 37 | + | } | |
| 38 | + | id[i] = hi<<4 | lo | |
| 39 | + | } | |
| 40 | + | return id, nil | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // parseIDBytes parses 40 hexadecimal digits. | |
| 44 | + | func parseIDBytes(b []byte) (id ObjectID, ok bool) { | |
| 45 | + | if len(b) != 40 { | |
| 46 | + | return id, false | |
| 47 | + | } | |
| 48 | + | for i := 0; i < 20; i++ { | |
| 49 | + | hi, lo := oidHexVal[b[2*i]], oidHexVal[b[2*i+1]] | |
| 50 | + | if hi|lo > 0x0f { | |
| 51 | + | return ObjectID{}, false | |
| 52 | + | } | |
| 53 | + | id[i] = hi<<4 | lo | |
| 54 | + | } | |
| 55 | + | return id, true | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // String returns the 40 lowercase hexadecimal digits of the id. | |
| 59 | + | func (id ObjectID) String() string { | |
| 60 | + | var b [40]byte | |
| 61 | + | return string(id.AppendHex(b[:0])) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // AppendHex appends the 40 lowercase hexadecimal digits of the id to dst. | |
| 65 | + | // It does not allocate when dst has room. | |
| 66 | + | func (id ObjectID) AppendHex(dst []byte) []byte { | |
| 67 | + | for _, c := range id { | |
| 68 | + | dst = append(dst, hexDigits[c>>4], hexDigits[c&0x0f]) | |
| 69 | + | } | |
| 70 | + | return dst | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // IsZero reports whether the id is all zeros (the null id). | |
| 74 | + | func (id ObjectID) IsZero() bool { return id == ObjectID{} } | |
| 75 | + | ||
| 76 | + | func truncate(s string) string { | |
| 77 | + | if len(s) > 64 { | |
| 78 | + | return s[:64] + "..." | |
| 79 | + | } | |
| 80 | + | return s | |
| 81 | + | } | |
| @@ -0,0 +1,81 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import "fmt" | |||
| 6 | + | ||||
| 7 | + | // ObjectID is a SHA-1 object id. | |||
| 8 | + | type ObjectID [20]byte | |||
| 9 | + | ||||
| 10 | + | const hexDigits = "0123456789abcdef" | |||
| 11 | + | ||||
| 12 | + | // oidHexVal maps a hex digit to its value, or 0xff. | |||
| 13 | + | var oidHexVal = func() (t [256]byte) { | |||
| 14 | + | for i := range t { | |||
| 15 | + | t[i] = 0xff | |||
| 16 | + | } | |||
| 17 | + | for i := 0; i < 10; i++ { | |||
| 18 | + | t['0'+i] = byte(i) | |||
| 19 | + | } | |||
| 20 | + | for i := 0; i < 6; i++ { | |||
| 21 | + | t['a'+i] = byte(10 + i) | |||
| 22 | + | t['A'+i] = byte(10 + i) | |||
| 23 | + | } | |||
| 24 | + | return t | |||
| 25 | + | }() | |||
| 26 | + | ||||
| 27 | + | // ParseID parses 40 hexadecimal digits. | |||
| 28 | + | func ParseID(s string) (ObjectID, error) { | |||
| 29 | + | var id ObjectID | |||
| 30 | + | if len(s) != 40 { | |||
| 31 | + | return id, fmt.Errorf("%w: %q has %d characters, want 40", ErrInvalidID, truncate(s), len(s)) | |||
| 32 | + | } | |||
| 33 | + | for i := 0; i < 20; i++ { | |||
| 34 | + | hi, lo := oidHexVal[s[2*i]], oidHexVal[s[2*i+1]] | |||
| 35 | + | if hi|lo > 0x0f { | |||
| 36 | + | return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(s)) | |||
| 37 | + | } | |||
| 38 | + | id[i] = hi<<4 | lo | |||
| 39 | + | } | |||
| 40 | + | return id, nil | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // parseIDBytes parses 40 hexadecimal digits. | |||
| 44 | + | func parseIDBytes(b []byte) (id ObjectID, ok bool) { | |||
| 45 | + | if len(b) != 40 { | |||
| 46 | + | return id, false | |||
| 47 | + | } | |||
| 48 | + | for i := 0; i < 20; i++ { | |||
| 49 | + | hi, lo := oidHexVal[b[2*i]], oidHexVal[b[2*i+1]] | |||
| 50 | + | if hi|lo > 0x0f { | |||
| 51 | + | return ObjectID{}, false | |||
| 52 | + | } | |||
| 53 | + | id[i] = hi<<4 | lo | |||
| 54 | + | } | |||
| 55 | + | return id, true | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // String returns the 40 lowercase hexadecimal digits of the id. | |||
| 59 | + | func (id ObjectID) String() string { | |||
| 60 | + | var b [40]byte | |||
| 61 | + | return string(id.AppendHex(b[:0])) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // AppendHex appends the 40 lowercase hexadecimal digits of the id to dst. | |||
| 65 | + | // It does not allocate when dst has room. | |||
| 66 | + | func (id ObjectID) AppendHex(dst []byte) []byte { | |||
| 67 | + | for _, c := range id { | |||
| 68 | + | dst = append(dst, hexDigits[c>>4], hexDigits[c&0x0f]) | |||
| 69 | + | } | |||
| 70 | + | return dst | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // IsZero reports whether the id is all zeros (the null id). | |||
| 74 | + | func (id ObjectID) IsZero() bool { return id == ObjectID{} } | |||
| 75 | + | ||||
| 76 | + | func truncate(s string) string { | |||
| 77 | + | if len(s) > 64 { | |||
| 78 | + | return s[:64] + "..." | |||
| 79 | + | } | |||
| 80 | + | return s | |||
| 81 | + | } | |||
A idx.go +164 -0
| @@ -0,0 +1,164 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "encoding/binary" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // idxFile is a validated, read-only pack index (version 2) held in memory. | |
| 11 | + | type idxFile struct { | |
| 12 | + | data []byte | |
| 13 | + | n uint32 | |
| 14 | + | fanout [256]uint32 | |
| 15 | + | names []byte // n*20 sorted object ids | |
| 16 | + | offs []byte // n*4 offsets | |
| 17 | + | large []byte // 8 byte offsets | |
| 18 | + | packHash []byte // 20 bytes: the checksum of the pack file | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | const ( | |
| 22 | + | idxHeaderLen = 8 | |
| 23 | + | idxFanoutLen = 256 * 4 | |
| 24 | + | idxTrailer = 40 | |
| 25 | + | ) | |
| 26 | + | ||
| 27 | + | var idxMagic = []byte{0xff, 't', 'O', 'c'} | |
| 28 | + | ||
| 29 | + | // parseIdx validates the layout of a v2 index: sizes, fan-out monotony and | |
| 30 | + | // the large offset table. It does not verify the checksums. | |
| 31 | + | func parseIdx(data []byte) (*idxFile, error) { | |
| 32 | + | if len(data) < idxHeaderLen+idxFanoutLen+idxTrailer { | |
| 33 | + | return nil, corrupt("idx: file too short (%d bytes)", len(data)) | |
| 34 | + | } | |
| 35 | + | if !bytes.Equal(data[:4], idxMagic) { | |
| 36 | + | return nil, unsupported("idx: version 1 index") | |
| 37 | + | } | |
| 38 | + | if v := binary.BigEndian.Uint32(data[4:8]); v != 2 { | |
| 39 | + | return nil, unsupported("idx: version %d", v) | |
| 40 | + | } | |
| 41 | + | x := &idxFile{data: data} | |
| 42 | + | prev := uint32(0) | |
| 43 | + | for i := range x.fanout { | |
| 44 | + | v := binary.BigEndian.Uint32(data[idxHeaderLen+4*i:]) | |
| 45 | + | if v < prev { | |
| 46 | + | return nil, corrupt("idx: fan-out decreases at %#02x", i) | |
| 47 | + | } | |
| 48 | + | x.fanout[i] = v | |
| 49 | + | prev = v | |
| 50 | + | } | |
| 51 | + | x.n = prev | |
| 52 | + | n := uint64(x.n) | |
| 53 | + | fixed := uint64(idxHeaderLen+idxFanoutLen) + n*28 + idxTrailer | |
| 54 | + | if uint64(len(data)) < fixed { | |
| 55 | + | return nil, corrupt("idx: %d objects do not fit in %d bytes", n, len(data)) | |
| 56 | + | } | |
| 57 | + | extra := uint64(len(data)) - fixed | |
| 58 | + | if extra%8 != 0 { | |
| 59 | + | return nil, corrupt("idx: large offset table of %d bytes", extra) | |
| 60 | + | } | |
| 61 | + | p := uint64(idxHeaderLen + idxFanoutLen) | |
| 62 | + | x.names = data[p : p+n*20] | |
| 63 | + | p += n * 20 | |
| 64 | + | p += n * 4 // crc32 | |
| 65 | + | x.offs = data[p : p+n*4] | |
| 66 | + | p += n * 4 | |
| 67 | + | x.large = data[p : p+extra] | |
| 68 | + | p += extra | |
| 69 | + | x.packHash = data[p : p+20] | |
| 70 | + | return x, nil | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // id returns the id at position i. | |
| 74 | + | func (x *idxFile) id(i uint32) []byte { return x.names[uint64(i)*20 : uint64(i)*20+20] } | |
| 75 | + | ||
| 76 | + | // find returns the position of id. | |
| 77 | + | func (x *idxFile) find(id *ObjectID) (uint32, bool) { | |
| 78 | + | lo, hi := x.bounds(id[0]) | |
| 79 | + | pos := x.lowerBound(id[:], lo, hi) | |
| 80 | + | if pos < hi && bytes.Equal(x.id(pos), id[:]) { | |
| 81 | + | return pos, true | |
| 82 | + | } | |
| 83 | + | return 0, false | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // bounds returns the range of positions whose first byte is b. | |
| 87 | + | func (x *idxFile) bounds(b byte) (lo, hi uint32) { | |
| 88 | + | if b > 0 { | |
| 89 | + | lo = x.fanout[b-1] | |
| 90 | + | } | |
| 91 | + | return lo, x.fanout[b] | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // lowerBound returns the first position in [lo, hi) whose id is not less | |
| 95 | + | // than key, comparing len(key) bytes. | |
| 96 | + | func (x *idxFile) lowerBound(key []byte, lo, hi uint32) uint32 { | |
| 97 | + | for lo < hi { | |
| 98 | + | mid := lo + (hi-lo)/2 | |
| 99 | + | if bytes.Compare(x.id(mid)[:len(key)], key) < 0 { | |
| 100 | + | lo = mid + 1 | |
| 101 | + | } else { | |
| 102 | + | hi = mid | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | return lo | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // offset returns the offset in the pack of the object at position i. | |
| 109 | + | func (x *idxFile) offset(i uint32) (uint64, error) { | |
| 110 | + | v := binary.BigEndian.Uint32(x.offs[uint64(i)*4:]) | |
| 111 | + | if v&0x80000000 == 0 { | |
| 112 | + | return uint64(v), nil | |
| 113 | + | } | |
| 114 | + | k := uint64(v & 0x7fffffff) | |
| 115 | + | if (k+1)*8 > uint64(len(x.large)) { | |
| 116 | + | return 0, corrupt("idx: large offset %d out of %d", k, len(x.large)/8) | |
| 117 | + | } | |
| 118 | + | return binary.BigEndian.Uint64(x.large[k*8:]), nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // prefixMatches calls fn with each id that starts with the nibble prefix | |
| 122 | + | // (nibbles hex digits, packed two per byte in key), stopping when fn returns | |
| 123 | + | // false. | |
| 124 | + | func (x *idxFile) prefixMatches(key []byte, nibbles int, fn func(id ObjectID) bool) { | |
| 125 | + | var lo, hi uint32 | |
| 126 | + | var first, last byte | |
| 127 | + | whole := nibbles / 2 | |
| 128 | + | first, last = key[0], key[0] | |
| 129 | + | if nibbles == 1 { | |
| 130 | + | first, last = key[0]&0xf0, key[0]|0x0f | |
| 131 | + | } | |
| 132 | + | lo, _ = x.bounds(first) | |
| 133 | + | _, hi = x.bounds(last) | |
| 134 | + | var pad [20]byte | |
| 135 | + | copy(pad[:], key[:(nibbles+1)/2]) | |
| 136 | + | if nibbles%2 == 1 { | |
| 137 | + | pad[whole] &= 0xf0 | |
| 138 | + | } | |
| 139 | + | pos := x.lowerBound(pad[:(nibbles+1)/2], lo, hi) | |
| 140 | + | for ; pos < hi; pos++ { | |
| 141 | + | id := x.id(pos) | |
| 142 | + | if !hasNibblePrefix(id, key, nibbles) { | |
| 143 | + | return | |
| 144 | + | } | |
| 145 | + | var o ObjectID | |
| 146 | + | copy(o[:], id) | |
| 147 | + | if !fn(o) { | |
| 148 | + | return | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // hasNibblePrefix reports whether id starts with the first n hex digits | |
| 154 | + | // encoded in key. | |
| 155 | + | func hasNibblePrefix(id, key []byte, n int) bool { | |
| 156 | + | whole := n / 2 | |
| 157 | + | if !bytes.Equal(id[:whole], key[:whole]) { | |
| 158 | + | return false | |
| 159 | + | } | |
| 160 | + | if n%2 == 1 { | |
| 161 | + | return id[whole]&0xf0 == key[whole]&0xf0 | |
| 162 | + | } | |
| 163 | + | return true | |
| 164 | + | } | |
| @@ -0,0 +1,164 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "encoding/binary" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // idxFile is a validated, read-only pack index (version 2) held in memory. | |||
| 11 | + | type idxFile struct { | |||
| 12 | + | data []byte | |||
| 13 | + | n uint32 | |||
| 14 | + | fanout [256]uint32 | |||
| 15 | + | names []byte // n*20 sorted object ids | |||
| 16 | + | offs []byte // n*4 offsets | |||
| 17 | + | large []byte // 8 byte offsets | |||
| 18 | + | packHash []byte // 20 bytes: the checksum of the pack file | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | const ( | |||
| 22 | + | idxHeaderLen = 8 | |||
| 23 | + | idxFanoutLen = 256 * 4 | |||
| 24 | + | idxTrailer = 40 | |||
| 25 | + | ) | |||
| 26 | + | ||||
| 27 | + | var idxMagic = []byte{0xff, 't', 'O', 'c'} | |||
| 28 | + | ||||
| 29 | + | // parseIdx validates the layout of a v2 index: sizes, fan-out monotony and | |||
| 30 | + | // the large offset table. It does not verify the checksums. | |||
| 31 | + | func parseIdx(data []byte) (*idxFile, error) { | |||
| 32 | + | if len(data) < idxHeaderLen+idxFanoutLen+idxTrailer { | |||
| 33 | + | return nil, corrupt("idx: file too short (%d bytes)", len(data)) | |||
| 34 | + | } | |||
| 35 | + | if !bytes.Equal(data[:4], idxMagic) { | |||
| 36 | + | return nil, unsupported("idx: version 1 index") | |||
| 37 | + | } | |||
| 38 | + | if v := binary.BigEndian.Uint32(data[4:8]); v != 2 { | |||
| 39 | + | return nil, unsupported("idx: version %d", v) | |||
| 40 | + | } | |||
| 41 | + | x := &idxFile{data: data} | |||
| 42 | + | prev := uint32(0) | |||
| 43 | + | for i := range x.fanout { | |||
| 44 | + | v := binary.BigEndian.Uint32(data[idxHeaderLen+4*i:]) | |||
| 45 | + | if v < prev { | |||
| 46 | + | return nil, corrupt("idx: fan-out decreases at %#02x", i) | |||
| 47 | + | } | |||
| 48 | + | x.fanout[i] = v | |||
| 49 | + | prev = v | |||
| 50 | + | } | |||
| 51 | + | x.n = prev | |||
| 52 | + | n := uint64(x.n) | |||
| 53 | + | fixed := uint64(idxHeaderLen+idxFanoutLen) + n*28 + idxTrailer | |||
| 54 | + | if uint64(len(data)) < fixed { | |||
| 55 | + | return nil, corrupt("idx: %d objects do not fit in %d bytes", n, len(data)) | |||
| 56 | + | } | |||
| 57 | + | extra := uint64(len(data)) - fixed | |||
| 58 | + | if extra%8 != 0 { | |||
| 59 | + | return nil, corrupt("idx: large offset table of %d bytes", extra) | |||
| 60 | + | } | |||
| 61 | + | p := uint64(idxHeaderLen + idxFanoutLen) | |||
| 62 | + | x.names = data[p : p+n*20] | |||
| 63 | + | p += n * 20 | |||
| 64 | + | p += n * 4 // crc32 | |||
| 65 | + | x.offs = data[p : p+n*4] | |||
| 66 | + | p += n * 4 | |||
| 67 | + | x.large = data[p : p+extra] | |||
| 68 | + | p += extra | |||
| 69 | + | x.packHash = data[p : p+20] | |||
| 70 | + | return x, nil | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // id returns the id at position i. | |||
| 74 | + | func (x *idxFile) id(i uint32) []byte { return x.names[uint64(i)*20 : uint64(i)*20+20] } | |||
| 75 | + | ||||
| 76 | + | // find returns the position of id. | |||
| 77 | + | func (x *idxFile) find(id *ObjectID) (uint32, bool) { | |||
| 78 | + | lo, hi := x.bounds(id[0]) | |||
| 79 | + | pos := x.lowerBound(id[:], lo, hi) | |||
| 80 | + | if pos < hi && bytes.Equal(x.id(pos), id[:]) { | |||
| 81 | + | return pos, true | |||
| 82 | + | } | |||
| 83 | + | return 0, false | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // bounds returns the range of positions whose first byte is b. | |||
| 87 | + | func (x *idxFile) bounds(b byte) (lo, hi uint32) { | |||
| 88 | + | if b > 0 { | |||
| 89 | + | lo = x.fanout[b-1] | |||
| 90 | + | } | |||
| 91 | + | return lo, x.fanout[b] | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // lowerBound returns the first position in [lo, hi) whose id is not less | |||
| 95 | + | // than key, comparing len(key) bytes. | |||
| 96 | + | func (x *idxFile) lowerBound(key []byte, lo, hi uint32) uint32 { | |||
| 97 | + | for lo < hi { | |||
| 98 | + | mid := lo + (hi-lo)/2 | |||
| 99 | + | if bytes.Compare(x.id(mid)[:len(key)], key) < 0 { | |||
| 100 | + | lo = mid + 1 | |||
| 101 | + | } else { | |||
| 102 | + | hi = mid | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | return lo | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // offset returns the offset in the pack of the object at position i. | |||
| 109 | + | func (x *idxFile) offset(i uint32) (uint64, error) { | |||
| 110 | + | v := binary.BigEndian.Uint32(x.offs[uint64(i)*4:]) | |||
| 111 | + | if v&0x80000000 == 0 { | |||
| 112 | + | return uint64(v), nil | |||
| 113 | + | } | |||
| 114 | + | k := uint64(v & 0x7fffffff) | |||
| 115 | + | if (k+1)*8 > uint64(len(x.large)) { | |||
| 116 | + | return 0, corrupt("idx: large offset %d out of %d", k, len(x.large)/8) | |||
| 117 | + | } | |||
| 118 | + | return binary.BigEndian.Uint64(x.large[k*8:]), nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // prefixMatches calls fn with each id that starts with the nibble prefix | |||
| 122 | + | // (nibbles hex digits, packed two per byte in key), stopping when fn returns | |||
| 123 | + | // false. | |||
| 124 | + | func (x *idxFile) prefixMatches(key []byte, nibbles int, fn func(id ObjectID) bool) { | |||
| 125 | + | var lo, hi uint32 | |||
| 126 | + | var first, last byte | |||
| 127 | + | whole := nibbles / 2 | |||
| 128 | + | first, last = key[0], key[0] | |||
| 129 | + | if nibbles == 1 { | |||
| 130 | + | first, last = key[0]&0xf0, key[0]|0x0f | |||
| 131 | + | } | |||
| 132 | + | lo, _ = x.bounds(first) | |||
| 133 | + | _, hi = x.bounds(last) | |||
| 134 | + | var pad [20]byte | |||
| 135 | + | copy(pad[:], key[:(nibbles+1)/2]) | |||
| 136 | + | if nibbles%2 == 1 { | |||
| 137 | + | pad[whole] &= 0xf0 | |||
| 138 | + | } | |||
| 139 | + | pos := x.lowerBound(pad[:(nibbles+1)/2], lo, hi) | |||
| 140 | + | for ; pos < hi; pos++ { | |||
| 141 | + | id := x.id(pos) | |||
| 142 | + | if !hasNibblePrefix(id, key, nibbles) { | |||
| 143 | + | return | |||
| 144 | + | } | |||
| 145 | + | var o ObjectID | |||
| 146 | + | copy(o[:], id) | |||
| 147 | + | if !fn(o) { | |||
| 148 | + | return | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // hasNibblePrefix reports whether id starts with the first n hex digits | |||
| 154 | + | // encoded in key. | |||
| 155 | + | func hasNibblePrefix(id, key []byte, n int) bool { | |||
| 156 | + | whole := n / 2 | |||
| 157 | + | if !bytes.Equal(id[:whole], key[:whole]) { | |||
| 158 | + | return false | |||
| 159 | + | } | |||
| 160 | + | if n%2 == 1 { | |||
| 161 | + | return id[whole]&0xf0 == key[whole]&0xf0 | |||
| 162 | + | } | |||
| 163 | + | return true | |||
| 164 | + | } | |||
A inflate.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bufio" | |
| 7 | + | "bytes" | |
| 8 | + | "compress/zlib" | |
| 9 | + | "errors" | |
| 10 | + | "io" | |
| 11 | + | "sync" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // buffer is a pooled byte slice. Pooling the pointer keeps Put free of | |
| 15 | + | // allocations. | |
| 16 | + | type buffer struct{ b []byte } | |
| 17 | + | ||
| 18 | + | // maxPooledBuffer is the largest buffer that goes back to the pool, so a | |
| 19 | + | // single big blob does not pin memory. | |
| 20 | + | const maxPooledBuffer = 4 << 20 | |
| 21 | + | ||
| 22 | + | var bufPool = sync.Pool{New: func() any { return &buffer{b: make([]byte, 0, 4096)} }} | |
| 23 | + | ||
| 24 | + | func getBuf() *buffer { return bufPool.Get().(*buffer) } | |
| 25 | + | ||
| 26 | + | func putBuf(b *buffer) { | |
| 27 | + | if b == nil || cap(b.b) > maxPooledBuffer { | |
| 28 | + | return | |
| 29 | + | } | |
| 30 | + | b.b = b.b[:0] | |
| 31 | + | bufPool.Put(b) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // grow returns b with length n, reusing its capacity when possible. The | |
| 35 | + | // content is not preserved. | |
| 36 | + | func grow(b []byte, n int) []byte { | |
| 37 | + | if cap(b) >= n { | |
| 38 | + | return b[:n] | |
| 39 | + | } | |
| 40 | + | return make([]byte, n, n+n/4) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // memInflater inflates a zlib stream held in memory. | |
| 44 | + | type memInflater struct { | |
| 45 | + | zr io.ReadCloser | |
| 46 | + | br bytes.Reader | |
| 47 | + | scratch [maxLooseHeader]byte // for reads through the io.Reader interface, which escape | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | var memPool = sync.Pool{New: func() any { return new(memInflater) }} | |
| 51 | + | ||
| 52 | + | // getMem returns an inflater positioned at the start of src. | |
| 53 | + | func getMem(src []byte) (*memInflater, error) { | |
| 54 | + | m := memPool.Get().(*memInflater) | |
| 55 | + | m.br.Reset(src) | |
| 56 | + | var err error | |
| 57 | + | if m.zr == nil { | |
| 58 | + | m.zr, err = zlib.NewReader(&m.br) | |
| 59 | + | } else { | |
| 60 | + | err = m.zr.(zlib.Resetter).Reset(&m.br, nil) | |
| 61 | + | } | |
| 62 | + | if err != nil { | |
| 63 | + | putMem(m) | |
| 64 | + | return nil, corrupt("zlib header: %v", err) | |
| 65 | + | } | |
| 66 | + | return m, nil | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func putMem(m *memInflater) { | |
| 70 | + | m.br.Reset(nil) | |
| 71 | + | memPool.Put(m) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // fileInflater inflates a zlib stream read from a file. | |
| 75 | + | type fileInflater struct { | |
| 76 | + | zr io.ReadCloser | |
| 77 | + | br *bufio.Reader | |
| 78 | + | scratch [maxLooseHeader]byte | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | var filePool = sync.Pool{New: func() any { return &fileInflater{br: bufio.NewReaderSize(nil, 8192)} }} | |
| 82 | + | ||
| 83 | + | func getFile(r io.Reader) (*fileInflater, error) { | |
| 84 | + | f := filePool.Get().(*fileInflater) | |
| 85 | + | f.br.Reset(r) | |
| 86 | + | var err error | |
| 87 | + | if f.zr == nil { | |
| 88 | + | f.zr, err = zlib.NewReader(f.br) | |
| 89 | + | } else { | |
| 90 | + | err = f.zr.(zlib.Resetter).Reset(f.br, nil) | |
| 91 | + | } | |
| 92 | + | if err != nil { | |
| 93 | + | putFile(f) | |
| 94 | + | return nil, corrupt("zlib header: %v", err) | |
| 95 | + | } | |
| 96 | + | return f, nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func putFile(f *fileInflater) { | |
| 100 | + | f.br.Reset(nil) | |
| 101 | + | filePool.Put(f) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // inflateExact fills dst from zr and checks that the stream ends right | |
| 105 | + | // after, so a stream shorter or longer than declared is corrupt. | |
| 106 | + | func inflateExact(zr io.Reader, dst []byte, probe []byte) error { | |
| 107 | + | if _, err := io.ReadFull(zr, dst); err != nil { | |
| 108 | + | return corrupt("inflate: %v", err) | |
| 109 | + | } | |
| 110 | + | n, err := zr.Read(probe[:1]) | |
| 111 | + | if n != 0 { | |
| 112 | + | return corrupt("inflate: object longer than its declared size") | |
| 113 | + | } | |
| 114 | + | if !errors.Is(err, io.EOF) { | |
| 115 | + | return corrupt("inflate: %v", err) | |
| 116 | + | } | |
| 117 | + | return nil | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bufio" | |||
| 7 | + | "bytes" | |||
| 8 | + | "compress/zlib" | |||
| 9 | + | "errors" | |||
| 10 | + | "io" | |||
| 11 | + | "sync" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // buffer is a pooled byte slice. Pooling the pointer keeps Put free of | |||
| 15 | + | // allocations. | |||
| 16 | + | type buffer struct{ b []byte } | |||
| 17 | + | ||||
| 18 | + | // maxPooledBuffer is the largest buffer that goes back to the pool, so a | |||
| 19 | + | // single big blob does not pin memory. | |||
| 20 | + | const maxPooledBuffer = 4 << 20 | |||
| 21 | + | ||||
| 22 | + | var bufPool = sync.Pool{New: func() any { return &buffer{b: make([]byte, 0, 4096)} }} | |||
| 23 | + | ||||
| 24 | + | func getBuf() *buffer { return bufPool.Get().(*buffer) } | |||
| 25 | + | ||||
| 26 | + | func putBuf(b *buffer) { | |||
| 27 | + | if b == nil || cap(b.b) > maxPooledBuffer { | |||
| 28 | + | return | |||
| 29 | + | } | |||
| 30 | + | b.b = b.b[:0] | |||
| 31 | + | bufPool.Put(b) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // grow returns b with length n, reusing its capacity when possible. The | |||
| 35 | + | // content is not preserved. | |||
| 36 | + | func grow(b []byte, n int) []byte { | |||
| 37 | + | if cap(b) >= n { | |||
| 38 | + | return b[:n] | |||
| 39 | + | } | |||
| 40 | + | return make([]byte, n, n+n/4) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // memInflater inflates a zlib stream held in memory. | |||
| 44 | + | type memInflater struct { | |||
| 45 | + | zr io.ReadCloser | |||
| 46 | + | br bytes.Reader | |||
| 47 | + | scratch [maxLooseHeader]byte // for reads through the io.Reader interface, which escape | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | var memPool = sync.Pool{New: func() any { return new(memInflater) }} | |||
| 51 | + | ||||
| 52 | + | // getMem returns an inflater positioned at the start of src. | |||
| 53 | + | func getMem(src []byte) (*memInflater, error) { | |||
| 54 | + | m := memPool.Get().(*memInflater) | |||
| 55 | + | m.br.Reset(src) | |||
| 56 | + | var err error | |||
| 57 | + | if m.zr == nil { | |||
| 58 | + | m.zr, err = zlib.NewReader(&m.br) | |||
| 59 | + | } else { | |||
| 60 | + | err = m.zr.(zlib.Resetter).Reset(&m.br, nil) | |||
| 61 | + | } | |||
| 62 | + | if err != nil { | |||
| 63 | + | putMem(m) | |||
| 64 | + | return nil, corrupt("zlib header: %v", err) | |||
| 65 | + | } | |||
| 66 | + | return m, nil | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | func putMem(m *memInflater) { | |||
| 70 | + | m.br.Reset(nil) | |||
| 71 | + | memPool.Put(m) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // fileInflater inflates a zlib stream read from a file. | |||
| 75 | + | type fileInflater struct { | |||
| 76 | + | zr io.ReadCloser | |||
| 77 | + | br *bufio.Reader | |||
| 78 | + | scratch [maxLooseHeader]byte | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | var filePool = sync.Pool{New: func() any { return &fileInflater{br: bufio.NewReaderSize(nil, 8192)} }} | |||
| 82 | + | ||||
| 83 | + | func getFile(r io.Reader) (*fileInflater, error) { | |||
| 84 | + | f := filePool.Get().(*fileInflater) | |||
| 85 | + | f.br.Reset(r) | |||
| 86 | + | var err error | |||
| 87 | + | if f.zr == nil { | |||
| 88 | + | f.zr, err = zlib.NewReader(f.br) | |||
| 89 | + | } else { | |||
| 90 | + | err = f.zr.(zlib.Resetter).Reset(f.br, nil) | |||
| 91 | + | } | |||
| 92 | + | if err != nil { | |||
| 93 | + | putFile(f) | |||
| 94 | + | return nil, corrupt("zlib header: %v", err) | |||
| 95 | + | } | |||
| 96 | + | return f, nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func putFile(f *fileInflater) { | |||
| 100 | + | f.br.Reset(nil) | |||
| 101 | + | filePool.Put(f) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // inflateExact fills dst from zr and checks that the stream ends right | |||
| 105 | + | // after, so a stream shorter or longer than declared is corrupt. | |||
| 106 | + | func inflateExact(zr io.Reader, dst []byte, probe []byte) error { | |||
| 107 | + | if _, err := io.ReadFull(zr, dst); err != nil { | |||
| 108 | + | return corrupt("inflate: %v", err) | |||
| 109 | + | } | |||
| 110 | + | n, err := zr.Read(probe[:1]) | |||
| 111 | + | if n != 0 { | |||
| 112 | + | return corrupt("inflate: object longer than its declared size") | |||
| 113 | + | } | |||
| 114 | + | if !errors.Is(err, io.EOF) { | |||
| 115 | + | return corrupt("inflate: %v", err) | |||
| 116 | + | } | |||
| 117 | + | return nil | |||
| 118 | + | } | |||
A loose.go +276 -0
| @@ -0,0 +1,276 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "io" | |
| 8 | + | "io/fs" | |
| 9 | + | "os" | |
| 10 | + | "strings" | |
| 11 | + | "syscall" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // maxLooseHeader bounds "<type> <size>\x00" of a loose object. | |
| 15 | + | const maxLooseHeader = 32 | |
| 16 | + | ||
| 17 | + | func (r *Repo) loosePath(id *ObjectID) string { | |
| 18 | + | var hex [40]byte | |
| 19 | + | id.AppendHex(hex[:0]) | |
| 20 | + | var arr [256]byte | |
| 21 | + | b := append(arr[:0], r.dir...) | |
| 22 | + | b = append(b, "/objects/"...) | |
| 23 | + | b = append(b, hex[:2]...) | |
| 24 | + | b = append(b, '/') | |
| 25 | + | b = append(b, hex[2:]...) | |
| 26 | + | return string(b) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // looseObject is an opened loose object whose header was read. | |
| 30 | + | type looseObject struct { | |
| 31 | + | f *os.File | |
| 32 | + | fi *fileInflater | |
| 33 | + | typ Type | |
| 34 | + | size uint64 | |
| 35 | + | lead [maxLooseHeader]byte // content bytes read along with the header | |
| 36 | + | nlea int | |
| 37 | + | disk int64 // size of the file | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // openLoose opens a loose object and reads its header. It returns | |
| 41 | + | // ErrNotFound when the object is not loose. | |
| 42 | + | func (r *Repo) openLoose(id *ObjectID) (*looseObject, error) { | |
| 43 | + | f, err := os.Open(r.loosePath(id)) | |
| 44 | + | if err != nil { | |
| 45 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 46 | + | return nil, ErrNotFound | |
| 47 | + | } | |
| 48 | + | return nil, err | |
| 49 | + | } | |
| 50 | + | l := &looseObject{f: f} | |
| 51 | + | if l.fi, err = getFile(f); err != nil { | |
| 52 | + | _ = f.Close() | |
| 53 | + | return nil, wrapLoose(id, err) | |
| 54 | + | } | |
| 55 | + | if err := l.readHeader(); err != nil { | |
| 56 | + | l.close() | |
| 57 | + | return nil, wrapLoose(id, err) | |
| 58 | + | } | |
| 59 | + | // A small file cannot hold a big object (deflate expands at most 1032 | |
| 60 | + | // times): check it before allocating for the content. The stat is only | |
| 61 | + | // worth its cost for big objects. | |
| 62 | + | if l.size > statThreshold { | |
| 63 | + | st, err := f.Stat() | |
| 64 | + | if err != nil { | |
| 65 | + | l.close() | |
| 66 | + | return nil, err | |
| 67 | + | } | |
| 68 | + | l.disk = st.Size() | |
| 69 | + | if l.size/1032 > uint64(l.disk) { | |
| 70 | + | l.close() | |
| 71 | + | return nil, corrupt("loose object %s: %d bytes in a file of %d", id.String(), l.size, l.disk) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | return l, nil | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // statThreshold is the object size over which the size of the loose file is | |
| 78 | + | // checked against the size the header announces. | |
| 79 | + | const statThreshold = 1 << 16 | |
| 80 | + | ||
| 81 | + | func wrapLoose(id *ObjectID, err error) error { | |
| 82 | + | if errors.Is(err, ErrCorrupt) { | |
| 83 | + | return corrupt("loose object %s: %v", id.String(), err) | |
| 84 | + | } | |
| 85 | + | return err | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func (l *looseObject) close() { | |
| 89 | + | if l.fi != nil { | |
| 90 | + | putFile(l.fi) | |
| 91 | + | l.fi = nil | |
| 92 | + | } | |
| 93 | + | if l.f != nil { | |
| 94 | + | _ = l.f.Close() | |
| 95 | + | l.f = nil | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func (l *looseObject) readHeader() error { | |
| 100 | + | tmp := l.fi.scratch[:] | |
| 101 | + | have := 0 | |
| 102 | + | nul := -1 | |
| 103 | + | for nul < 0 { | |
| 104 | + | if have == len(tmp) { | |
| 105 | + | return corrupt("header too long") | |
| 106 | + | } | |
| 107 | + | n, err := l.fi.zr.Read(tmp[have:]) | |
| 108 | + | for i := have; i < have+n; i++ { | |
| 109 | + | if tmp[i] == 0 { | |
| 110 | + | nul = i | |
| 111 | + | break | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | have += n | |
| 115 | + | if nul < 0 && err != nil { | |
| 116 | + | return corrupt("truncated header: %v", err) | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | typ, size, err := parseObjectHeader(tmp[:nul]) | |
| 120 | + | if err != nil { | |
| 121 | + | return err | |
| 122 | + | } | |
| 123 | + | l.typ, l.size = typ, size | |
| 124 | + | l.nlea = copy(l.lead[:], tmp[nul+1:have]) | |
| 125 | + | return nil | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // parseObjectHeader parses "<type> <decimal size>". | |
| 129 | + | func parseObjectHeader(h []byte) (Type, uint64, error) { | |
| 130 | + | sp := -1 | |
| 131 | + | for i, c := range h { | |
| 132 | + | if c == ' ' { | |
| 133 | + | sp = i | |
| 134 | + | break | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | if sp < 0 { | |
| 138 | + | return 0, 0, corrupt("bad object header") | |
| 139 | + | } | |
| 140 | + | typ, ok := parseTypeName(h[:sp]) | |
| 141 | + | if !ok { | |
| 142 | + | return 0, 0, corrupt("unknown object type %q", truncate(string(h[:sp]))) | |
| 143 | + | } | |
| 144 | + | digits := h[sp+1:] | |
| 145 | + | if len(digits) == 0 || len(digits) > 18 { | |
| 146 | + | return 0, 0, corrupt("bad object size") | |
| 147 | + | } | |
| 148 | + | var size uint64 | |
| 149 | + | for _, c := range digits { | |
| 150 | + | if c < '0' || c > '9' { | |
| 151 | + | return 0, 0, corrupt("bad object size") | |
| 152 | + | } | |
| 153 | + | size = size*10 + uint64(c-'0') | |
| 154 | + | } | |
| 155 | + | return typ, size, nil | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // readLoose reads a whole loose object into dst. | |
| 159 | + | func (r *Repo) readLoose(id *ObjectID, dst []byte) (Type, []byte, error) { | |
| 160 | + | l, err := r.openLoose(id) | |
| 161 | + | if err != nil { | |
| 162 | + | return 0, nil, err | |
| 163 | + | } | |
| 164 | + | defer l.close() | |
| 165 | + | if l.size > uint64(r.opts.maxObject) { | |
| 166 | + | return 0, nil, tooLarge(l.size, r.opts.maxObject) | |
| 167 | + | } | |
| 168 | + | if uint64(l.nlea) > l.size { | |
| 169 | + | return 0, nil, corrupt("loose object %s: longer than its header says", id.String()) | |
| 170 | + | } | |
| 171 | + | dst = grow(dst, int(l.size)) | |
| 172 | + | copy(dst, l.lead[:l.nlea]) | |
| 173 | + | if err := inflateExact(l.fi.zr, dst[l.nlea:], l.fi.scratch[:]); err != nil { | |
| 174 | + | return 0, nil, wrapLoose(id, err) | |
| 175 | + | } | |
| 176 | + | return l.typ, dst, nil | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // headerPrefix is how much of a loose file is given to the inflater to find | |
| 180 | + | // the header. Go's flate decodes a whole 32 KiB window before returning the | |
| 181 | + | // first byte, so feeding it the whole file would inflate most small objects | |
| 182 | + | // for nothing. The first block header and a few literals fit in 1 KiB. | |
| 183 | + | const headerPrefix = 1024 | |
| 184 | + | ||
| 185 | + | // looseHeader reads the type and size of a loose object, inflating as little | |
| 186 | + | // as possible. | |
| 187 | + | func (r *Repo) looseHeader(id *ObjectID) (Type, int64, error) { | |
| 188 | + | f, err := os.Open(r.loosePath(id)) | |
| 189 | + | if err != nil { | |
| 190 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 191 | + | return 0, 0, ErrNotFound | |
| 192 | + | } | |
| 193 | + | return 0, 0, err | |
| 194 | + | } | |
| 195 | + | pb := getBuf() | |
| 196 | + | pb.b = grow(pb.b, headerPrefix) | |
| 197 | + | n, rerr := io.ReadFull(f, pb.b) | |
| 198 | + | _ = f.Close() | |
| 199 | + | if rerr != nil && !errors.Is(rerr, io.ErrUnexpectedEOF) && !errors.Is(rerr, io.EOF) { | |
| 200 | + | putBuf(pb) | |
| 201 | + | return 0, 0, rerr | |
| 202 | + | } | |
| 203 | + | typ, size, ok := headerFromPrefix(pb.b[:n]) | |
| 204 | + | putBuf(pb) | |
| 205 | + | if ok { | |
| 206 | + | return typ, int64(size), nil | |
| 207 | + | } | |
| 208 | + | // Unusual or damaged: take the full path, which reports precise errors. | |
| 209 | + | l, err := r.openLoose(id) | |
| 210 | + | if err != nil { | |
| 211 | + | return 0, 0, err | |
| 212 | + | } | |
| 213 | + | defer l.close() | |
| 214 | + | return l.typ, int64(l.size), nil | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // headerFromPrefix parses the object header from the beginning of a | |
| 218 | + | // compressed loose object. ok is false if the header cannot be found there. | |
| 219 | + | func headerFromPrefix(src []byte) (typ Type, size uint64, ok bool) { | |
| 220 | + | m, err := getMem(src) | |
| 221 | + | if err != nil { | |
| 222 | + | return 0, 0, false | |
| 223 | + | } | |
| 224 | + | defer putMem(m) | |
| 225 | + | have := 0 | |
| 226 | + | for have < len(m.scratch) { | |
| 227 | + | n, err := m.zr.Read(m.scratch[have:]) | |
| 228 | + | for i := have; i < have+n; i++ { | |
| 229 | + | if m.scratch[i] == 0 { | |
| 230 | + | typ, size, perr := parseObjectHeader(m.scratch[:i]) | |
| 231 | + | return typ, size, perr == nil | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | have += n | |
| 235 | + | if err != nil { | |
| 236 | + | return 0, 0, false | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | return 0, 0, false | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // looseWithPrefix calls fn for the loose objects whose id starts with the | |
| 243 | + | // hex digits in hex (at least 2 of them), until fn returns false. | |
| 244 | + | func (r *Repo) looseWithPrefix(hex string, fn func(ObjectID) bool) error { | |
| 245 | + | dir := r.dir + "/objects/" + hex[:2] | |
| 246 | + | f, err := os.Open(dir) | |
| 247 | + | if err != nil { | |
| 248 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 249 | + | return nil | |
| 250 | + | } | |
| 251 | + | return err | |
| 252 | + | } | |
| 253 | + | defer func() { _ = f.Close() }() | |
| 254 | + | rest := hex[2:] | |
| 255 | + | for { | |
| 256 | + | names, err := f.Readdirnames(256) | |
| 257 | + | for _, n := range names { | |
| 258 | + | if len(n) != 38 || !strings.HasPrefix(n, rest) { | |
| 259 | + | continue | |
| 260 | + | } | |
| 261 | + | id, ok := parseIDBytes([]byte(hex[:2] + n)) | |
| 262 | + | if !ok { | |
| 263 | + | continue // "tmp_obj_*" and other files | |
| 264 | + | } | |
| 265 | + | if !fn(id) { | |
| 266 | + | return nil | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | if errors.Is(err, io.EOF) { | |
| 270 | + | return nil | |
| 271 | + | } | |
| 272 | + | if err != nil { | |
| 273 | + | return err | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | } | |
| @@ -0,0 +1,276 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "io" | |||
| 8 | + | "io/fs" | |||
| 9 | + | "os" | |||
| 10 | + | "strings" | |||
| 11 | + | "syscall" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // maxLooseHeader bounds "<type> <size>\x00" of a loose object. | |||
| 15 | + | const maxLooseHeader = 32 | |||
| 16 | + | ||||
| 17 | + | func (r *Repo) loosePath(id *ObjectID) string { | |||
| 18 | + | var hex [40]byte | |||
| 19 | + | id.AppendHex(hex[:0]) | |||
| 20 | + | var arr [256]byte | |||
| 21 | + | b := append(arr[:0], r.dir...) | |||
| 22 | + | b = append(b, "/objects/"...) | |||
| 23 | + | b = append(b, hex[:2]...) | |||
| 24 | + | b = append(b, '/') | |||
| 25 | + | b = append(b, hex[2:]...) | |||
| 26 | + | return string(b) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // looseObject is an opened loose object whose header was read. | |||
| 30 | + | type looseObject struct { | |||
| 31 | + | f *os.File | |||
| 32 | + | fi *fileInflater | |||
| 33 | + | typ Type | |||
| 34 | + | size uint64 | |||
| 35 | + | lead [maxLooseHeader]byte // content bytes read along with the header | |||
| 36 | + | nlea int | |||
| 37 | + | disk int64 // size of the file | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // openLoose opens a loose object and reads its header. It returns | |||
| 41 | + | // ErrNotFound when the object is not loose. | |||
| 42 | + | func (r *Repo) openLoose(id *ObjectID) (*looseObject, error) { | |||
| 43 | + | f, err := os.Open(r.loosePath(id)) | |||
| 44 | + | if err != nil { | |||
| 45 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |||
| 46 | + | return nil, ErrNotFound | |||
| 47 | + | } | |||
| 48 | + | return nil, err | |||
| 49 | + | } | |||
| 50 | + | l := &looseObject{f: f} | |||
| 51 | + | if l.fi, err = getFile(f); err != nil { | |||
| 52 | + | _ = f.Close() | |||
| 53 | + | return nil, wrapLoose(id, err) | |||
| 54 | + | } | |||
| 55 | + | if err := l.readHeader(); err != nil { | |||
| 56 | + | l.close() | |||
| 57 | + | return nil, wrapLoose(id, err) | |||
| 58 | + | } | |||
| 59 | + | // A small file cannot hold a big object (deflate expands at most 1032 | |||
| 60 | + | // times): check it before allocating for the content. The stat is only | |||
| 61 | + | // worth its cost for big objects. | |||
| 62 | + | if l.size > statThreshold { | |||
| 63 | + | st, err := f.Stat() | |||
| 64 | + | if err != nil { | |||
| 65 | + | l.close() | |||
| 66 | + | return nil, err | |||
| 67 | + | } | |||
| 68 | + | l.disk = st.Size() | |||
| 69 | + | if l.size/1032 > uint64(l.disk) { | |||
| 70 | + | l.close() | |||
| 71 | + | return nil, corrupt("loose object %s: %d bytes in a file of %d", id.String(), l.size, l.disk) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | return l, nil | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // statThreshold is the object size over which the size of the loose file is | |||
| 78 | + | // checked against the size the header announces. | |||
| 79 | + | const statThreshold = 1 << 16 | |||
| 80 | + | ||||
| 81 | + | func wrapLoose(id *ObjectID, err error) error { | |||
| 82 | + | if errors.Is(err, ErrCorrupt) { | |||
| 83 | + | return corrupt("loose object %s: %v", id.String(), err) | |||
| 84 | + | } | |||
| 85 | + | return err | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func (l *looseObject) close() { | |||
| 89 | + | if l.fi != nil { | |||
| 90 | + | putFile(l.fi) | |||
| 91 | + | l.fi = nil | |||
| 92 | + | } | |||
| 93 | + | if l.f != nil { | |||
| 94 | + | _ = l.f.Close() | |||
| 95 | + | l.f = nil | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func (l *looseObject) readHeader() error { | |||
| 100 | + | tmp := l.fi.scratch[:] | |||
| 101 | + | have := 0 | |||
| 102 | + | nul := -1 | |||
| 103 | + | for nul < 0 { | |||
| 104 | + | if have == len(tmp) { | |||
| 105 | + | return corrupt("header too long") | |||
| 106 | + | } | |||
| 107 | + | n, err := l.fi.zr.Read(tmp[have:]) | |||
| 108 | + | for i := have; i < have+n; i++ { | |||
| 109 | + | if tmp[i] == 0 { | |||
| 110 | + | nul = i | |||
| 111 | + | break | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | have += n | |||
| 115 | + | if nul < 0 && err != nil { | |||
| 116 | + | return corrupt("truncated header: %v", err) | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | typ, size, err := parseObjectHeader(tmp[:nul]) | |||
| 120 | + | if err != nil { | |||
| 121 | + | return err | |||
| 122 | + | } | |||
| 123 | + | l.typ, l.size = typ, size | |||
| 124 | + | l.nlea = copy(l.lead[:], tmp[nul+1:have]) | |||
| 125 | + | return nil | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // parseObjectHeader parses "<type> <decimal size>". | |||
| 129 | + | func parseObjectHeader(h []byte) (Type, uint64, error) { | |||
| 130 | + | sp := -1 | |||
| 131 | + | for i, c := range h { | |||
| 132 | + | if c == ' ' { | |||
| 133 | + | sp = i | |||
| 134 | + | break | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | if sp < 0 { | |||
| 138 | + | return 0, 0, corrupt("bad object header") | |||
| 139 | + | } | |||
| 140 | + | typ, ok := parseTypeName(h[:sp]) | |||
| 141 | + | if !ok { | |||
| 142 | + | return 0, 0, corrupt("unknown object type %q", truncate(string(h[:sp]))) | |||
| 143 | + | } | |||
| 144 | + | digits := h[sp+1:] | |||
| 145 | + | if len(digits) == 0 || len(digits) > 18 { | |||
| 146 | + | return 0, 0, corrupt("bad object size") | |||
| 147 | + | } | |||
| 148 | + | var size uint64 | |||
| 149 | + | for _, c := range digits { | |||
| 150 | + | if c < '0' || c > '9' { | |||
| 151 | + | return 0, 0, corrupt("bad object size") | |||
| 152 | + | } | |||
| 153 | + | size = size*10 + uint64(c-'0') | |||
| 154 | + | } | |||
| 155 | + | return typ, size, nil | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // readLoose reads a whole loose object into dst. | |||
| 159 | + | func (r *Repo) readLoose(id *ObjectID, dst []byte) (Type, []byte, error) { | |||
| 160 | + | l, err := r.openLoose(id) | |||
| 161 | + | if err != nil { | |||
| 162 | + | return 0, nil, err | |||
| 163 | + | } | |||
| 164 | + | defer l.close() | |||
| 165 | + | if l.size > uint64(r.opts.maxObject) { | |||
| 166 | + | return 0, nil, tooLarge(l.size, r.opts.maxObject) | |||
| 167 | + | } | |||
| 168 | + | if uint64(l.nlea) > l.size { | |||
| 169 | + | return 0, nil, corrupt("loose object %s: longer than its header says", id.String()) | |||
| 170 | + | } | |||
| 171 | + | dst = grow(dst, int(l.size)) | |||
| 172 | + | copy(dst, l.lead[:l.nlea]) | |||
| 173 | + | if err := inflateExact(l.fi.zr, dst[l.nlea:], l.fi.scratch[:]); err != nil { | |||
| 174 | + | return 0, nil, wrapLoose(id, err) | |||
| 175 | + | } | |||
| 176 | + | return l.typ, dst, nil | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // headerPrefix is how much of a loose file is given to the inflater to find | |||
| 180 | + | // the header. Go's flate decodes a whole 32 KiB window before returning the | |||
| 181 | + | // first byte, so feeding it the whole file would inflate most small objects | |||
| 182 | + | // for nothing. The first block header and a few literals fit in 1 KiB. | |||
| 183 | + | const headerPrefix = 1024 | |||
| 184 | + | ||||
| 185 | + | // looseHeader reads the type and size of a loose object, inflating as little | |||
| 186 | + | // as possible. | |||
| 187 | + | func (r *Repo) looseHeader(id *ObjectID) (Type, int64, error) { | |||
| 188 | + | f, err := os.Open(r.loosePath(id)) | |||
| 189 | + | if err != nil { | |||
| 190 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |||
| 191 | + | return 0, 0, ErrNotFound | |||
| 192 | + | } | |||
| 193 | + | return 0, 0, err | |||
| 194 | + | } | |||
| 195 | + | pb := getBuf() | |||
| 196 | + | pb.b = grow(pb.b, headerPrefix) | |||
| 197 | + | n, rerr := io.ReadFull(f, pb.b) | |||
| 198 | + | _ = f.Close() | |||
| 199 | + | if rerr != nil && !errors.Is(rerr, io.ErrUnexpectedEOF) && !errors.Is(rerr, io.EOF) { | |||
| 200 | + | putBuf(pb) | |||
| 201 | + | return 0, 0, rerr | |||
| 202 | + | } | |||
| 203 | + | typ, size, ok := headerFromPrefix(pb.b[:n]) | |||
| 204 | + | putBuf(pb) | |||
| 205 | + | if ok { | |||
| 206 | + | return typ, int64(size), nil | |||
| 207 | + | } | |||
| 208 | + | // Unusual or damaged: take the full path, which reports precise errors. | |||
| 209 | + | l, err := r.openLoose(id) | |||
| 210 | + | if err != nil { | |||
| 211 | + | return 0, 0, err | |||
| 212 | + | } | |||
| 213 | + | defer l.close() | |||
| 214 | + | return l.typ, int64(l.size), nil | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // headerFromPrefix parses the object header from the beginning of a | |||
| 218 | + | // compressed loose object. ok is false if the header cannot be found there. | |||
| 219 | + | func headerFromPrefix(src []byte) (typ Type, size uint64, ok bool) { | |||
| 220 | + | m, err := getMem(src) | |||
| 221 | + | if err != nil { | |||
| 222 | + | return 0, 0, false | |||
| 223 | + | } | |||
| 224 | + | defer putMem(m) | |||
| 225 | + | have := 0 | |||
| 226 | + | for have < len(m.scratch) { | |||
| 227 | + | n, err := m.zr.Read(m.scratch[have:]) | |||
| 228 | + | for i := have; i < have+n; i++ { | |||
| 229 | + | if m.scratch[i] == 0 { | |||
| 230 | + | typ, size, perr := parseObjectHeader(m.scratch[:i]) | |||
| 231 | + | return typ, size, perr == nil | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | have += n | |||
| 235 | + | if err != nil { | |||
| 236 | + | return 0, 0, false | |||
| 237 | + | } | |||
| 238 | + | } | |||
| 239 | + | return 0, 0, false | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | // looseWithPrefix calls fn for the loose objects whose id starts with the | |||
| 243 | + | // hex digits in hex (at least 2 of them), until fn returns false. | |||
| 244 | + | func (r *Repo) looseWithPrefix(hex string, fn func(ObjectID) bool) error { | |||
| 245 | + | dir := r.dir + "/objects/" + hex[:2] | |||
| 246 | + | f, err := os.Open(dir) | |||
| 247 | + | if err != nil { | |||
| 248 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |||
| 249 | + | return nil | |||
| 250 | + | } | |||
| 251 | + | return err | |||
| 252 | + | } | |||
| 253 | + | defer func() { _ = f.Close() }() | |||
| 254 | + | rest := hex[2:] | |||
| 255 | + | for { | |||
| 256 | + | names, err := f.Readdirnames(256) | |||
| 257 | + | for _, n := range names { | |||
| 258 | + | if len(n) != 38 || !strings.HasPrefix(n, rest) { | |||
| 259 | + | continue | |||
| 260 | + | } | |||
| 261 | + | id, ok := parseIDBytes([]byte(hex[:2] + n)) | |||
| 262 | + | if !ok { | |||
| 263 | + | continue // "tmp_obj_*" and other files | |||
| 264 | + | } | |||
| 265 | + | if !fn(id) { | |||
| 266 | + | return nil | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | if errors.Is(err, io.EOF) { | |||
| 270 | + | return nil | |||
| 271 | + | } | |||
| 272 | + | if err != nil { | |||
| 273 | + | return err | |||
| 274 | + | } | |||
| 275 | + | } | |||
| 276 | + | } | |||
A mmap_linux.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | // adviseRandom tells the kernel that lookups are random accesses, so it | |
| 10 | + | // should not read ahead. The advice is best effort. | |
| 11 | + | func adviseRandom(data []byte) { _ = syscall.Madvise(data, syscall.MADV_RANDOM) } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | // adviseRandom tells the kernel that lookups are random accesses, so it | |||
| 10 | + | // should not read ahead. The advice is best effort. | |||
| 11 | + | func adviseRandom(data []byte) { _ = syscall.Madvise(data, syscall.MADV_RANDOM) } | |||
A mmap_notlinux.go +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build darwin || freebsd | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | func adviseRandom([]byte) {} | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build darwin || freebsd | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | func adviseRandom([]byte) {} | |||
A mmap_other.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build !(linux || darwin || freebsd) | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "io" | |
| 9 | + | "os" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // mapFile reads a whole file in memory, where mmap is not available. | |
| 13 | + | func mapFile(f *os.File, size int64) ([]byte, func() error, error) { | |
| 14 | + | if size <= 0 || int64(int(size)) != size { | |
| 15 | + | return nil, nil, corrupt("%s: unusable size %d", f.Name(), size) | |
| 16 | + | } | |
| 17 | + | data := make([]byte, size) | |
| 18 | + | if _, err := io.ReadFull(f, data); err != nil { | |
| 19 | + | return nil, nil, err | |
| 20 | + | } | |
| 21 | + | return data, func() error { return nil }, nil | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func fileIno(os.FileInfo) uint64 { return 0 } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build !(linux || darwin || freebsd) | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "io" | |||
| 9 | + | "os" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // mapFile reads a whole file in memory, where mmap is not available. | |||
| 13 | + | func mapFile(f *os.File, size int64) ([]byte, func() error, error) { | |||
| 14 | + | if size <= 0 || int64(int(size)) != size { | |||
| 15 | + | return nil, nil, corrupt("%s: unusable size %d", f.Name(), size) | |||
| 16 | + | } | |||
| 17 | + | data := make([]byte, size) | |||
| 18 | + | if _, err := io.ReadFull(f, data); err != nil { | |||
| 19 | + | return nil, nil, err | |||
| 20 | + | } | |||
| 21 | + | return data, func() error { return nil }, nil | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func fileIno(os.FileInfo) uint64 { return 0 } | |||
A mmap_unix.go +31 -0
| @@ -0,0 +1,31 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux || darwin || freebsd | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "os" | |
| 9 | + | "syscall" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // mapFile maps a whole file read-only. The returned function unmaps it. | |
| 13 | + | func mapFile(f *os.File, size int64) ([]byte, func() error, error) { | |
| 14 | + | if size <= 0 || int64(int(size)) != size { | |
| 15 | + | return nil, nil, corrupt("%s: unusable size %d", f.Name(), size) | |
| 16 | + | } | |
| 17 | + | data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) | |
| 18 | + | if err != nil { | |
| 19 | + | return nil, nil, &os.PathError{Op: "mmap", Path: f.Name(), Err: err} | |
| 20 | + | } | |
| 21 | + | adviseRandom(data) | |
| 22 | + | return data, func() error { return syscall.Munmap(data) }, nil | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // fileIno returns the inode number of a stat result, 0 when unknown. | |
| 26 | + | func fileIno(fi os.FileInfo) uint64 { | |
| 27 | + | if st, ok := fi.Sys().(*syscall.Stat_t); ok { | |
| 28 | + | return uint64(st.Ino) //nolint:unconvert // the type differs per OS | |
| 29 | + | } | |
| 30 | + | return 0 | |
| 31 | + | } | |
| @@ -0,0 +1,31 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux || darwin || freebsd | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "os" | |||
| 9 | + | "syscall" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // mapFile maps a whole file read-only. The returned function unmaps it. | |||
| 13 | + | func mapFile(f *os.File, size int64) ([]byte, func() error, error) { | |||
| 14 | + | if size <= 0 || int64(int(size)) != size { | |||
| 15 | + | return nil, nil, corrupt("%s: unusable size %d", f.Name(), size) | |||
| 16 | + | } | |||
| 17 | + | data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) | |||
| 18 | + | if err != nil { | |||
| 19 | + | return nil, nil, &os.PathError{Op: "mmap", Path: f.Name(), Err: err} | |||
| 20 | + | } | |||
| 21 | + | adviseRandom(data) | |||
| 22 | + | return data, func() error { return syscall.Munmap(data) }, nil | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // fileIno returns the inode number of a stat result, 0 when unknown. | |||
| 26 | + | func fileIno(fi os.FileInfo) uint64 { | |||
| 27 | + | if st, ok := fi.Sys().(*syscall.Stat_t); ok { | |||
| 28 | + | return uint64(st.Ino) //nolint:unconvert // the type differs per OS | |||
| 29 | + | } | |||
| 30 | + | return 0 | |||
| 31 | + | } | |||
A object.go +287 -0
| @@ -0,0 +1,287 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "fmt" | |
| 8 | + | "strings" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // finder looks an object up in a snapshot of the packs and, when it is | |
| 12 | + | // missing, once more after reloading the pack list: a repack or a push may | |
| 13 | + | // have added the pack that holds it. | |
| 14 | + | type finder struct { | |
| 15 | + | r *Repo | |
| 16 | + | s *snapshot | |
| 17 | + | retried bool | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func (r *Repo) finder() (finder, error) { | |
| 21 | + | s, err := r.acquire() | |
| 22 | + | return finder{r: r, s: s}, err | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func (f *finder) close() { f.s.release() } | |
| 26 | + | ||
| 27 | + | func (f *finder) pack(id *ObjectID) (*pack, uint64, error) { return f.s.find(id) } | |
| 28 | + | ||
| 29 | + | // again reloads the pack list if it changed and reports whether the lookup | |
| 30 | + | // is worth repeating, that is whether the current snapshot differs from the | |
| 31 | + | // one this finder used. Another goroutine may already have installed the new | |
| 32 | + | // list, so the answer of reloadPacks alone is not enough. It returns true at | |
| 33 | + | // most once. | |
| 34 | + | func (f *finder) again() bool { | |
| 35 | + | if f.retried { | |
| 36 | + | return false | |
| 37 | + | } | |
| 38 | + | f.retried = true | |
| 39 | + | if _, err := f.r.reloadPacks(false); err != nil { | |
| 40 | + | return false | |
| 41 | + | } | |
| 42 | + | s, err := f.r.acquire() | |
| 43 | + | if err != nil { | |
| 44 | + | return false | |
| 45 | + | } | |
| 46 | + | if s == f.s { | |
| 47 | + | s.release() | |
| 48 | + | return false | |
| 49 | + | } | |
| 50 | + | f.s.release() | |
| 51 | + | f.s = s | |
| 52 | + | return true | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // HasObject reports whether the object exists. | |
| 56 | + | func (r *Repo) HasObject(id ObjectID) bool { | |
| 57 | + | f, err := r.finder() | |
| 58 | + | if err != nil { | |
| 59 | + | return false | |
| 60 | + | } | |
| 61 | + | defer f.close() | |
| 62 | + | for { | |
| 63 | + | if p, _, err := f.pack(&id); err != nil || p != nil { | |
| 64 | + | return err == nil | |
| 65 | + | } | |
| 66 | + | if exists(r.loosePath(&id)) { | |
| 67 | + | return true | |
| 68 | + | } | |
| 69 | + | if !f.again() { | |
| 70 | + | return false | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // ObjectHeader returns the type and the size of an object without reading its | |
| 76 | + | // content. For a deltified object the size is the result size announced by | |
| 77 | + | // the delta header. It returns ErrNotFound if the object does not exist. | |
| 78 | + | func (r *Repo) ObjectHeader(id ObjectID) (Type, int64, error) { | |
| 79 | + | f, err := r.finder() | |
| 80 | + | if err != nil { | |
| 81 | + | return 0, 0, err | |
| 82 | + | } | |
| 83 | + | defer f.close() | |
| 84 | + | for { | |
| 85 | + | p, off, err := f.pack(&id) | |
| 86 | + | if err != nil { | |
| 87 | + | return 0, 0, err | |
| 88 | + | } | |
| 89 | + | if p != nil { | |
| 90 | + | typ, size, err := p.header(off) | |
| 91 | + | if err != nil { | |
| 92 | + | return 0, 0, wrapPack(p.name, err) | |
| 93 | + | } | |
| 94 | + | return typ, size, nil | |
| 95 | + | } | |
| 96 | + | typ, size, err := r.looseHeader(&id) | |
| 97 | + | if err == nil { | |
| 98 | + | return typ, size, nil | |
| 99 | + | } | |
| 100 | + | if !errors.Is(err, ErrNotFound) { | |
| 101 | + | return 0, 0, err | |
| 102 | + | } | |
| 103 | + | if !f.again() { | |
| 104 | + | return 0, 0, notFound("object %s", id.String()) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // ReadObject reads the whole content of an object. The content is appended | |
| 110 | + | // to dst[:0], which is grown if needed; pass the previous result to reuse its | |
| 111 | + | // storage. Objects over the WithMaxObjectSize bound fail with ErrTooLarge. | |
| 112 | + | func (r *Repo) ReadObject(id ObjectID, dst []byte) (Type, []byte, error) { | |
| 113 | + | f, err := r.finder() | |
| 114 | + | if err != nil { | |
| 115 | + | return 0, nil, err | |
| 116 | + | } | |
| 117 | + | defer f.close() | |
| 118 | + | for { | |
| 119 | + | p, off, err := f.pack(&id) | |
| 120 | + | if err != nil { | |
| 121 | + | return 0, nil, err | |
| 122 | + | } | |
| 123 | + | if p != nil { | |
| 124 | + | typ, data, err := r.readPacked(p, off, dst) | |
| 125 | + | if err != nil { | |
| 126 | + | return 0, nil, wrapPack(p.name, err) | |
| 127 | + | } | |
| 128 | + | return typ, data, nil | |
| 129 | + | } | |
| 130 | + | typ, data, err := r.readLoose(&id, dst) | |
| 131 | + | if err == nil { | |
| 132 | + | return typ, data, nil | |
| 133 | + | } | |
| 134 | + | if !errors.Is(err, ErrNotFound) { | |
| 135 | + | return 0, nil, err | |
| 136 | + | } | |
| 137 | + | if !f.again() { | |
| 138 | + | return 0, nil, notFound("object %s", id.String()) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // readTyped reads an object that must have type want into a pooled buffer. | |
| 144 | + | // The caller returns the buffer with putBuf. | |
| 145 | + | func (r *Repo) readTyped(id ObjectID, want Type) (*buffer, error) { | |
| 146 | + | b := getBuf() | |
| 147 | + | typ, data, err := r.ReadObject(id, b.b) | |
| 148 | + | if err != nil { | |
| 149 | + | putBuf(b) | |
| 150 | + | return nil, err | |
| 151 | + | } | |
| 152 | + | b.b = data | |
| 153 | + | if typ != want { | |
| 154 | + | putBuf(b) | |
| 155 | + | return nil, wrongType(id, typ, want) | |
| 156 | + | } | |
| 157 | + | return b, nil | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // Peel follows annotated tags from id until it reaches an object that is not | |
| 161 | + | // a tag, and returns that object and its type. Chains of tags longer than 64 | |
| 162 | + | // are corrupt. | |
| 163 | + | func (r *Repo) Peel(id ObjectID) (ObjectID, Type, error) { | |
| 164 | + | for range maxPeel { | |
| 165 | + | typ, _, err := r.ObjectHeader(id) | |
| 166 | + | if err != nil { | |
| 167 | + | return ObjectID{}, 0, err | |
| 168 | + | } | |
| 169 | + | if typ != TypeTag { | |
| 170 | + | return id, typ, nil | |
| 171 | + | } | |
| 172 | + | b, err := r.readTyped(id, TypeTag) | |
| 173 | + | if err != nil { | |
| 174 | + | return ObjectID{}, 0, err | |
| 175 | + | } | |
| 176 | + | var t Tag | |
| 177 | + | err = ParseTag(b.b, &t) | |
| 178 | + | next := t.Object | |
| 179 | + | putBuf(b) | |
| 180 | + | if err != nil { | |
| 181 | + | return ObjectID{}, 0, fmt.Errorf("tag %s: %w", id, err) | |
| 182 | + | } | |
| 183 | + | id = next | |
| 184 | + | } | |
| 185 | + | return ObjectID{}, 0, corrupt("more than %d nested tags", maxPeel) | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | const maxPeel = 64 | |
| 189 | + | ||
| 190 | + | // PeelToCommit follows annotated tags from id and returns the commit they | |
| 191 | + | // designate. It returns ErrWrongType if the result is not a commit (a tag of | |
| 192 | + | // a tree, for example). | |
| 193 | + | func (r *Repo) PeelToCommit(id ObjectID) (ObjectID, error) { | |
| 194 | + | got, typ, err := r.Peel(id) | |
| 195 | + | if err != nil { | |
| 196 | + | return ObjectID{}, err | |
| 197 | + | } | |
| 198 | + | if typ != TypeCommit { | |
| 199 | + | return ObjectID{}, fmt.Errorf("%w: %s is a %s, not a commit", ErrWrongType, got, typ) | |
| 200 | + | } | |
| 201 | + | return got, nil | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // minPrefix is the shortest abbreviation accepted, as in Git. | |
| 205 | + | const minPrefix = 4 | |
| 206 | + | ||
| 207 | + | // ResolvePrefix returns the object whose id starts with the hexadecimal | |
| 208 | + | // digits in hex (4 to 40, either case). It returns ErrNotFound when none | |
| 209 | + | // matches and ErrAmbiguous when several do, whatever their type; see | |
| 210 | + | // ResolveCommitPrefix. A full id must exist. | |
| 211 | + | func (r *Repo) ResolvePrefix(hex string) (ObjectID, error) { | |
| 212 | + | return r.resolvePrefix(hex, nil) | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // ResolveCommitPrefix is like ResolvePrefix but only considers the objects | |
| 216 | + | // that designate a commit: commits and tags that peel to a commit. It is what | |
| 217 | + | // "git rev-parse <hex>^{commit}" does. | |
| 218 | + | func (r *Repo) ResolveCommitPrefix(hex string) (ObjectID, error) { | |
| 219 | + | return r.resolvePrefix(hex, func(id ObjectID) bool { | |
| 220 | + | _, err := r.PeelToCommit(id) | |
| 221 | + | return err == nil | |
| 222 | + | }) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (r *Repo) resolvePrefix(hex string, accept func(ObjectID) bool) (ObjectID, error) { | |
| 226 | + | n := len(hex) | |
| 227 | + | if n < minPrefix || n > 40 { | |
| 228 | + | return ObjectID{}, fmt.Errorf("%w: %q must have %d to 40 hexadecimal digits", ErrInvalidID, truncate(hex), minPrefix) | |
| 229 | + | } | |
| 230 | + | var key [20]byte | |
| 231 | + | for i := 0; i < n; i++ { | |
| 232 | + | v := oidHexVal[hex[i]] | |
| 233 | + | if v > 0x0f { | |
| 234 | + | return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(hex)) | |
| 235 | + | } | |
| 236 | + | if i%2 == 0 { | |
| 237 | + | key[i/2] = v << 4 | |
| 238 | + | } else { | |
| 239 | + | key[i/2] |= v | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | hex = strings.ToLower(hex) | |
| 243 | + | ||
| 244 | + | f, err := r.finder() | |
| 245 | + | if err != nil { | |
| 246 | + | return ObjectID{}, err | |
| 247 | + | } | |
| 248 | + | defer f.close() | |
| 249 | + | for { | |
| 250 | + | var found [2]ObjectID | |
| 251 | + | count := 0 | |
| 252 | + | add := func(id ObjectID) bool { | |
| 253 | + | if count > 0 && found[0] == id { | |
| 254 | + | return true | |
| 255 | + | } | |
| 256 | + | if count > 1 && found[1] == id { | |
| 257 | + | return true | |
| 258 | + | } | |
| 259 | + | if accept != nil && !accept(id) { | |
| 260 | + | return true | |
| 261 | + | } | |
| 262 | + | found[count] = id | |
| 263 | + | count++ | |
| 264 | + | return count < 2 | |
| 265 | + | } | |
| 266 | + | for _, p := range f.s.packs { | |
| 267 | + | p.idx.prefixMatches(key[:], n, add) | |
| 268 | + | if count >= 2 { | |
| 269 | + | break | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | if count < 2 { | |
| 273 | + | if err := r.looseWithPrefix(hex, add); err != nil { | |
| 274 | + | return ObjectID{}, err | |
| 275 | + | } | |
| 276 | + | } | |
| 277 | + | switch { | |
| 278 | + | case count >= 2: | |
| 279 | + | return ObjectID{}, fmt.Errorf("%w: %s matches several objects", ErrAmbiguous, hex) | |
| 280 | + | case count == 1: | |
| 281 | + | return found[0], nil | |
| 282 | + | } | |
| 283 | + | if !f.again() { | |
| 284 | + | return ObjectID{}, notFound("no object starts with %s", hex) | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | } | |
| @@ -0,0 +1,287 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "fmt" | |||
| 8 | + | "strings" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // finder looks an object up in a snapshot of the packs and, when it is | |||
| 12 | + | // missing, once more after reloading the pack list: a repack or a push may | |||
| 13 | + | // have added the pack that holds it. | |||
| 14 | + | type finder struct { | |||
| 15 | + | r *Repo | |||
| 16 | + | s *snapshot | |||
| 17 | + | retried bool | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func (r *Repo) finder() (finder, error) { | |||
| 21 | + | s, err := r.acquire() | |||
| 22 | + | return finder{r: r, s: s}, err | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func (f *finder) close() { f.s.release() } | |||
| 26 | + | ||||
| 27 | + | func (f *finder) pack(id *ObjectID) (*pack, uint64, error) { return f.s.find(id) } | |||
| 28 | + | ||||
| 29 | + | // again reloads the pack list if it changed and reports whether the lookup | |||
| 30 | + | // is worth repeating, that is whether the current snapshot differs from the | |||
| 31 | + | // one this finder used. Another goroutine may already have installed the new | |||
| 32 | + | // list, so the answer of reloadPacks alone is not enough. It returns true at | |||
| 33 | + | // most once. | |||
| 34 | + | func (f *finder) again() bool { | |||
| 35 | + | if f.retried { | |||
| 36 | + | return false | |||
| 37 | + | } | |||
| 38 | + | f.retried = true | |||
| 39 | + | if _, err := f.r.reloadPacks(false); err != nil { | |||
| 40 | + | return false | |||
| 41 | + | } | |||
| 42 | + | s, err := f.r.acquire() | |||
| 43 | + | if err != nil { | |||
| 44 | + | return false | |||
| 45 | + | } | |||
| 46 | + | if s == f.s { | |||
| 47 | + | s.release() | |||
| 48 | + | return false | |||
| 49 | + | } | |||
| 50 | + | f.s.release() | |||
| 51 | + | f.s = s | |||
| 52 | + | return true | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // HasObject reports whether the object exists. | |||
| 56 | + | func (r *Repo) HasObject(id ObjectID) bool { | |||
| 57 | + | f, err := r.finder() | |||
| 58 | + | if err != nil { | |||
| 59 | + | return false | |||
| 60 | + | } | |||
| 61 | + | defer f.close() | |||
| 62 | + | for { | |||
| 63 | + | if p, _, err := f.pack(&id); err != nil || p != nil { | |||
| 64 | + | return err == nil | |||
| 65 | + | } | |||
| 66 | + | if exists(r.loosePath(&id)) { | |||
| 67 | + | return true | |||
| 68 | + | } | |||
| 69 | + | if !f.again() { | |||
| 70 | + | return false | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // ObjectHeader returns the type and the size of an object without reading its | |||
| 76 | + | // content. For a deltified object the size is the result size announced by | |||
| 77 | + | // the delta header. It returns ErrNotFound if the object does not exist. | |||
| 78 | + | func (r *Repo) ObjectHeader(id ObjectID) (Type, int64, error) { | |||
| 79 | + | f, err := r.finder() | |||
| 80 | + | if err != nil { | |||
| 81 | + | return 0, 0, err | |||
| 82 | + | } | |||
| 83 | + | defer f.close() | |||
| 84 | + | for { | |||
| 85 | + | p, off, err := f.pack(&id) | |||
| 86 | + | if err != nil { | |||
| 87 | + | return 0, 0, err | |||
| 88 | + | } | |||
| 89 | + | if p != nil { | |||
| 90 | + | typ, size, err := p.header(off) | |||
| 91 | + | if err != nil { | |||
| 92 | + | return 0, 0, wrapPack(p.name, err) | |||
| 93 | + | } | |||
| 94 | + | return typ, size, nil | |||
| 95 | + | } | |||
| 96 | + | typ, size, err := r.looseHeader(&id) | |||
| 97 | + | if err == nil { | |||
| 98 | + | return typ, size, nil | |||
| 99 | + | } | |||
| 100 | + | if !errors.Is(err, ErrNotFound) { | |||
| 101 | + | return 0, 0, err | |||
| 102 | + | } | |||
| 103 | + | if !f.again() { | |||
| 104 | + | return 0, 0, notFound("object %s", id.String()) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // ReadObject reads the whole content of an object. The content is appended | |||
| 110 | + | // to dst[:0], which is grown if needed; pass the previous result to reuse its | |||
| 111 | + | // storage. Objects over the WithMaxObjectSize bound fail with ErrTooLarge. | |||
| 112 | + | func (r *Repo) ReadObject(id ObjectID, dst []byte) (Type, []byte, error) { | |||
| 113 | + | f, err := r.finder() | |||
| 114 | + | if err != nil { | |||
| 115 | + | return 0, nil, err | |||
| 116 | + | } | |||
| 117 | + | defer f.close() | |||
| 118 | + | for { | |||
| 119 | + | p, off, err := f.pack(&id) | |||
| 120 | + | if err != nil { | |||
| 121 | + | return 0, nil, err | |||
| 122 | + | } | |||
| 123 | + | if p != nil { | |||
| 124 | + | typ, data, err := r.readPacked(p, off, dst) | |||
| 125 | + | if err != nil { | |||
| 126 | + | return 0, nil, wrapPack(p.name, err) | |||
| 127 | + | } | |||
| 128 | + | return typ, data, nil | |||
| 129 | + | } | |||
| 130 | + | typ, data, err := r.readLoose(&id, dst) | |||
| 131 | + | if err == nil { | |||
| 132 | + | return typ, data, nil | |||
| 133 | + | } | |||
| 134 | + | if !errors.Is(err, ErrNotFound) { | |||
| 135 | + | return 0, nil, err | |||
| 136 | + | } | |||
| 137 | + | if !f.again() { | |||
| 138 | + | return 0, nil, notFound("object %s", id.String()) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // readTyped reads an object that must have type want into a pooled buffer. | |||
| 144 | + | // The caller returns the buffer with putBuf. | |||
| 145 | + | func (r *Repo) readTyped(id ObjectID, want Type) (*buffer, error) { | |||
| 146 | + | b := getBuf() | |||
| 147 | + | typ, data, err := r.ReadObject(id, b.b) | |||
| 148 | + | if err != nil { | |||
| 149 | + | putBuf(b) | |||
| 150 | + | return nil, err | |||
| 151 | + | } | |||
| 152 | + | b.b = data | |||
| 153 | + | if typ != want { | |||
| 154 | + | putBuf(b) | |||
| 155 | + | return nil, wrongType(id, typ, want) | |||
| 156 | + | } | |||
| 157 | + | return b, nil | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | // Peel follows annotated tags from id until it reaches an object that is not | |||
| 161 | + | // a tag, and returns that object and its type. Chains of tags longer than 64 | |||
| 162 | + | // are corrupt. | |||
| 163 | + | func (r *Repo) Peel(id ObjectID) (ObjectID, Type, error) { | |||
| 164 | + | for range maxPeel { | |||
| 165 | + | typ, _, err := r.ObjectHeader(id) | |||
| 166 | + | if err != nil { | |||
| 167 | + | return ObjectID{}, 0, err | |||
| 168 | + | } | |||
| 169 | + | if typ != TypeTag { | |||
| 170 | + | return id, typ, nil | |||
| 171 | + | } | |||
| 172 | + | b, err := r.readTyped(id, TypeTag) | |||
| 173 | + | if err != nil { | |||
| 174 | + | return ObjectID{}, 0, err | |||
| 175 | + | } | |||
| 176 | + | var t Tag | |||
| 177 | + | err = ParseTag(b.b, &t) | |||
| 178 | + | next := t.Object | |||
| 179 | + | putBuf(b) | |||
| 180 | + | if err != nil { | |||
| 181 | + | return ObjectID{}, 0, fmt.Errorf("tag %s: %w", id, err) | |||
| 182 | + | } | |||
| 183 | + | id = next | |||
| 184 | + | } | |||
| 185 | + | return ObjectID{}, 0, corrupt("more than %d nested tags", maxPeel) | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | const maxPeel = 64 | |||
| 189 | + | ||||
| 190 | + | // PeelToCommit follows annotated tags from id and returns the commit they | |||
| 191 | + | // designate. It returns ErrWrongType if the result is not a commit (a tag of | |||
| 192 | + | // a tree, for example). | |||
| 193 | + | func (r *Repo) PeelToCommit(id ObjectID) (ObjectID, error) { | |||
| 194 | + | got, typ, err := r.Peel(id) | |||
| 195 | + | if err != nil { | |||
| 196 | + | return ObjectID{}, err | |||
| 197 | + | } | |||
| 198 | + | if typ != TypeCommit { | |||
| 199 | + | return ObjectID{}, fmt.Errorf("%w: %s is a %s, not a commit", ErrWrongType, got, typ) | |||
| 200 | + | } | |||
| 201 | + | return got, nil | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // minPrefix is the shortest abbreviation accepted, as in Git. | |||
| 205 | + | const minPrefix = 4 | |||
| 206 | + | ||||
| 207 | + | // ResolvePrefix returns the object whose id starts with the hexadecimal | |||
| 208 | + | // digits in hex (4 to 40, either case). It returns ErrNotFound when none | |||
| 209 | + | // matches and ErrAmbiguous when several do, whatever their type; see | |||
| 210 | + | // ResolveCommitPrefix. A full id must exist. | |||
| 211 | + | func (r *Repo) ResolvePrefix(hex string) (ObjectID, error) { | |||
| 212 | + | return r.resolvePrefix(hex, nil) | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // ResolveCommitPrefix is like ResolvePrefix but only considers the objects | |||
| 216 | + | // that designate a commit: commits and tags that peel to a commit. It is what | |||
| 217 | + | // "git rev-parse <hex>^{commit}" does. | |||
| 218 | + | func (r *Repo) ResolveCommitPrefix(hex string) (ObjectID, error) { | |||
| 219 | + | return r.resolvePrefix(hex, func(id ObjectID) bool { | |||
| 220 | + | _, err := r.PeelToCommit(id) | |||
| 221 | + | return err == nil | |||
| 222 | + | }) | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | func (r *Repo) resolvePrefix(hex string, accept func(ObjectID) bool) (ObjectID, error) { | |||
| 226 | + | n := len(hex) | |||
| 227 | + | if n < minPrefix || n > 40 { | |||
| 228 | + | return ObjectID{}, fmt.Errorf("%w: %q must have %d to 40 hexadecimal digits", ErrInvalidID, truncate(hex), minPrefix) | |||
| 229 | + | } | |||
| 230 | + | var key [20]byte | |||
| 231 | + | for i := 0; i < n; i++ { | |||
| 232 | + | v := oidHexVal[hex[i]] | |||
| 233 | + | if v > 0x0f { | |||
| 234 | + | return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(hex)) | |||
| 235 | + | } | |||
| 236 | + | if i%2 == 0 { | |||
| 237 | + | key[i/2] = v << 4 | |||
| 238 | + | } else { | |||
| 239 | + | key[i/2] |= v | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | hex = strings.ToLower(hex) | |||
| 243 | + | ||||
| 244 | + | f, err := r.finder() | |||
| 245 | + | if err != nil { | |||
| 246 | + | return ObjectID{}, err | |||
| 247 | + | } | |||
| 248 | + | defer f.close() | |||
| 249 | + | for { | |||
| 250 | + | var found [2]ObjectID | |||
| 251 | + | count := 0 | |||
| 252 | + | add := func(id ObjectID) bool { | |||
| 253 | + | if count > 0 && found[0] == id { | |||
| 254 | + | return true | |||
| 255 | + | } | |||
| 256 | + | if count > 1 && found[1] == id { | |||
| 257 | + | return true | |||
| 258 | + | } | |||
| 259 | + | if accept != nil && !accept(id) { | |||
| 260 | + | return true | |||
| 261 | + | } | |||
| 262 | + | found[count] = id | |||
| 263 | + | count++ | |||
| 264 | + | return count < 2 | |||
| 265 | + | } | |||
| 266 | + | for _, p := range f.s.packs { | |||
| 267 | + | p.idx.prefixMatches(key[:], n, add) | |||
| 268 | + | if count >= 2 { | |||
| 269 | + | break | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | if count < 2 { | |||
| 273 | + | if err := r.looseWithPrefix(hex, add); err != nil { | |||
| 274 | + | return ObjectID{}, err | |||
| 275 | + | } | |||
| 276 | + | } | |||
| 277 | + | switch { | |||
| 278 | + | case count >= 2: | |||
| 279 | + | return ObjectID{}, fmt.Errorf("%w: %s matches several objects", ErrAmbiguous, hex) | |||
| 280 | + | case count == 1: | |||
| 281 | + | return found[0], nil | |||
| 282 | + | } | |||
| 283 | + | if !f.again() { | |||
| 284 | + | return ObjectID{}, notFound("no object starts with %s", hex) | |||
| 285 | + | } | |||
| 286 | + | } | |||
| 287 | + | } | |||
A pack.go +447 -0
| @@ -0,0 +1,447 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "encoding/binary" | |
| 8 | + | "io" | |
| 9 | + | "os" | |
| 10 | + | "sync/atomic" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const ( | |
| 14 | + | packHeaderLen = 12 | |
| 15 | + | packTrailerLen = 20 | |
| 16 | + | ||
| 17 | + | objOfsDelta = 6 | |
| 18 | + | objRefDelta = 7 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // pack is an open pack file with its index, both mapped read-only. It is | |
| 22 | + | // shared by the snapshots that list it and unmapped when the last one is | |
| 23 | + | // released. | |
| 24 | + | type pack struct { | |
| 25 | + | id uint32 | |
| 26 | + | name string // "pack-<hash>", without extension | |
| 27 | + | idx *idxFile | |
| 28 | + | data []byte // the whole .pack file | |
| 29 | + | sig fileSig | |
| 30 | + | unmap [2]func() error | |
| 31 | + | refs atomic.Int32 | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (p *pack) retain() { p.refs.Add(1) } | |
| 35 | + | ||
| 36 | + | func (p *pack) release() { | |
| 37 | + | if p.refs.Add(-1) == 0 { | |
| 38 | + | for _, f := range p.unmap { | |
| 39 | + | if f != nil { | |
| 40 | + | _ = f() | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | p.data, p.idx = nil, nil | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // openPack maps and validates dir/<name>.idx and dir/<name>.pack. It returns | |
| 48 | + | // (nil, nil) when a file vanished, which happens while a repack replaces it. | |
| 49 | + | func openPack(dir, name string, id uint32, sig fileSig) (*pack, error) { | |
| 50 | + | idxF, err := os.Open(dir + "/" + name + ".idx") | |
| 51 | + | if err != nil { | |
| 52 | + | return nil, ignoreMissing(err) | |
| 53 | + | } | |
| 54 | + | defer func() { _ = idxF.Close() }() | |
| 55 | + | packF, err := os.Open(dir + "/" + name + ".pack") | |
| 56 | + | if err != nil { | |
| 57 | + | return nil, ignoreMissing(err) | |
| 58 | + | } | |
| 59 | + | defer func() { _ = packF.Close() }() | |
| 60 | + | idxStat, err := idxF.Stat() | |
| 61 | + | if err != nil { | |
| 62 | + | return nil, err | |
| 63 | + | } | |
| 64 | + | packStat, err := packF.Stat() | |
| 65 | + | if err != nil { | |
| 66 | + | return nil, err | |
| 67 | + | } | |
| 68 | + | idxData, unmapIdx, err := mapFile(idxF, idxStat.Size()) | |
| 69 | + | if err != nil { | |
| 70 | + | return nil, err | |
| 71 | + | } | |
| 72 | + | packData, unmapPack, err := mapFile(packF, packStat.Size()) | |
| 73 | + | if err != nil { | |
| 74 | + | _ = unmapIdx() | |
| 75 | + | return nil, err | |
| 76 | + | } | |
| 77 | + | p := &pack{id: id, name: name, data: packData, sig: sig, unmap: [2]func() error{unmapIdx, unmapPack}} | |
| 78 | + | p.refs.Store(1) | |
| 79 | + | if p.idx, err = parseIdx(idxData); err != nil { | |
| 80 | + | p.release() | |
| 81 | + | return nil, wrapPack(name, err) | |
| 82 | + | } | |
| 83 | + | if err := p.validate(); err != nil { | |
| 84 | + | p.release() | |
| 85 | + | return nil, wrapPack(name, err) | |
| 86 | + | } | |
| 87 | + | return p, nil | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // validate checks the pack header and ties the pack to its index. | |
| 91 | + | func (p *pack) validate() error { | |
| 92 | + | if len(p.data) < packHeaderLen+packTrailerLen { | |
| 93 | + | return corrupt("pack too short (%d bytes)", len(p.data)) | |
| 94 | + | } | |
| 95 | + | if string(p.data[:4]) != "PACK" { | |
| 96 | + | return corrupt("bad pack signature") | |
| 97 | + | } | |
| 98 | + | if v := binary.BigEndian.Uint32(p.data[4:8]); v != 2 && v != 3 { | |
| 99 | + | return corrupt("pack version %d", v) | |
| 100 | + | } | |
| 101 | + | if n := binary.BigEndian.Uint32(p.data[8:12]); n != p.idx.n { | |
| 102 | + | return corrupt("pack holds %d objects, index %d", n, p.idx.n) | |
| 103 | + | } | |
| 104 | + | if !bytes.Equal(p.data[len(p.data)-packTrailerLen:], p.idx.packHash) { | |
| 105 | + | return corrupt("pack checksum differs from its index") | |
| 106 | + | } | |
| 107 | + | return nil | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | func wrapPack(name string, err error) error { | |
| 111 | + | return &packError{name: name, err: err} | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | type packError struct { | |
| 115 | + | name string | |
| 116 | + | err error | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (e *packError) Error() string { return e.name + ": " + e.err.Error() } | |
| 120 | + | func (e *packError) Unwrap() error { return e.err } | |
| 121 | + | ||
| 122 | + | // entryHdr is a parsed pack entry header. | |
| 123 | + | type entryHdr struct { | |
| 124 | + | typ uint8 // 1..4, objOfsDelta or objRefDelta | |
| 125 | + | size uint64 // inflated size of the entry data (the delta for deltas) | |
| 126 | + | data uint64 // offset of the zlib stream | |
| 127 | + | baseOff uint64 // objOfsDelta: absolute offset of the base | |
| 128 | + | baseID ObjectID | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func (h *entryHdr) isDelta() bool { return h.typ == objOfsDelta || h.typ == objRefDelta } | |
| 132 | + | ||
| 133 | + | // parseEntryHeader parses the header of the entry at off in a whole pack | |
| 134 | + | // file (header, entries and trailer). It never reads outside data. | |
| 135 | + | func parseEntryHeader(data []byte, off uint64) (h entryHdr, err error) { | |
| 136 | + | end := uint64(len(data)) | |
| 137 | + | if end < packHeaderLen+packTrailerLen { | |
| 138 | + | return h, corrupt("pack too short") | |
| 139 | + | } | |
| 140 | + | end -= packTrailerLen | |
| 141 | + | if off < packHeaderLen || off >= end { | |
| 142 | + | return h, corrupt("entry offset %d outside pack", off) | |
| 143 | + | } | |
| 144 | + | p := off | |
| 145 | + | c := data[p] | |
| 146 | + | p++ | |
| 147 | + | h.typ = (c >> 4) & 7 | |
| 148 | + | h.size = uint64(c & 0x0f) | |
| 149 | + | for shift := uint(4); c&0x80 != 0; shift += 7 { | |
| 150 | + | if p >= end || shift > 56 { | |
| 151 | + | return h, corrupt("bad entry size at %d", off) | |
| 152 | + | } | |
| 153 | + | c = data[p] | |
| 154 | + | p++ | |
| 155 | + | h.size |= uint64(c&0x7f) << shift | |
| 156 | + | } | |
| 157 | + | switch h.typ { | |
| 158 | + | case uint8(TypeCommit), uint8(TypeTree), uint8(TypeBlob), uint8(TypeTag): | |
| 159 | + | case objOfsDelta: | |
| 160 | + | if p >= end { | |
| 161 | + | return h, corrupt("truncated delta offset at %d", off) | |
| 162 | + | } | |
| 163 | + | c = data[p] | |
| 164 | + | p++ | |
| 165 | + | rel := uint64(c & 0x7f) | |
| 166 | + | for c&0x80 != 0 { | |
| 167 | + | if p >= end || rel >= 1<<56 { | |
| 168 | + | return h, corrupt("bad delta offset at %d", off) | |
| 169 | + | } | |
| 170 | + | c = data[p] | |
| 171 | + | p++ | |
| 172 | + | rel = (rel+1)<<7 | uint64(c&0x7f) | |
| 173 | + | } | |
| 174 | + | if rel == 0 || rel > off-packHeaderLen { | |
| 175 | + | return h, corrupt("delta base offset %d outside pack at %d", rel, off) | |
| 176 | + | } | |
| 177 | + | h.baseOff = off - rel | |
| 178 | + | case objRefDelta: | |
| 179 | + | if end-p < 20 { | |
| 180 | + | return h, corrupt("truncated delta base id at %d", off) | |
| 181 | + | } | |
| 182 | + | copy(h.baseID[:], data[p:p+20]) | |
| 183 | + | p += 20 | |
| 184 | + | default: | |
| 185 | + | return h, corrupt("entry type %d at %d", h.typ, off) | |
| 186 | + | } | |
| 187 | + | if p >= end { | |
| 188 | + | return h, corrupt("entry at %d has no data", off) | |
| 189 | + | } | |
| 190 | + | h.data = p | |
| 191 | + | return h, nil | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // stream returns the compressed bytes of the entry. | |
| 195 | + | func (p *pack) stream(h *entryHdr) []byte { | |
| 196 | + | return p.data[h.data : len(p.data)-packTrailerLen] | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // inflateEntry inflates the entry data into dst, which has the exact size | |
| 200 | + | // announced by the header. | |
| 201 | + | func (p *pack) inflateEntry(h *entryHdr, dst []byte) error { | |
| 202 | + | src := p.stream(h) | |
| 203 | + | // Deflate cannot expand a byte into more than 1032 bytes. | |
| 204 | + | if h.size/1032 > uint64(len(src)) { | |
| 205 | + | return corrupt("entry of %d bytes in %d compressed bytes", h.size, len(src)) | |
| 206 | + | } | |
| 207 | + | m, err := getMem(src) | |
| 208 | + | if err != nil { | |
| 209 | + | return err | |
| 210 | + | } | |
| 211 | + | defer putMem(m) | |
| 212 | + | return inflateExact(m.zr, dst, m.scratch[:]) | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // chainEnd follows the chain of delta headers from off without inflating | |
| 216 | + | // anything. It returns the type of the base object. | |
| 217 | + | func (p *pack) chainEnd(off uint64, h entryHdr) (Type, error) { | |
| 218 | + | for depth := 0; ; depth++ { | |
| 219 | + | if !h.isDelta() { | |
| 220 | + | return Type(h.typ), nil | |
| 221 | + | } | |
| 222 | + | if depth >= maxDeltaDepth { | |
| 223 | + | return 0, corrupt("delta chain longer than %d", maxDeltaDepth) | |
| 224 | + | } | |
| 225 | + | var err error | |
| 226 | + | off, err = p.baseOffset(&h, off) | |
| 227 | + | if err != nil { | |
| 228 | + | return 0, err | |
| 229 | + | } | |
| 230 | + | if h, err = parseEntryHeader(p.data, off); err != nil { | |
| 231 | + | return 0, err | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // baseOffset returns the offset of the base of a delta entry. A ref-delta | |
| 237 | + | // base must be in the same pack, as Git requires. | |
| 238 | + | func (p *pack) baseOffset(h *entryHdr, self uint64) (uint64, error) { | |
| 239 | + | if h.typ == objOfsDelta { | |
| 240 | + | return h.baseOff, nil | |
| 241 | + | } | |
| 242 | + | pos, ok := p.idx.find(&h.baseID) | |
| 243 | + | if !ok { | |
| 244 | + | return 0, corrupt("delta base %s not in pack %s", h.baseID.String(), p.name) | |
| 245 | + | } | |
| 246 | + | off, err := p.idx.offset(pos) | |
| 247 | + | if err != nil { | |
| 248 | + | return 0, err | |
| 249 | + | } | |
| 250 | + | if off == self { | |
| 251 | + | return 0, corrupt("delta is its own base at %d", self) | |
| 252 | + | } | |
| 253 | + | return off, nil | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | // header returns the type and size of the object at off. | |
| 257 | + | func (p *pack) header(off uint64) (Type, int64, error) { | |
| 258 | + | h, err := parseEntryHeader(p.data, off) | |
| 259 | + | if err != nil { | |
| 260 | + | return 0, 0, err | |
| 261 | + | } | |
| 262 | + | if !h.isDelta() { | |
| 263 | + | if h.size > 1<<62 { | |
| 264 | + | return 0, 0, corrupt("entry size %d", h.size) | |
| 265 | + | } | |
| 266 | + | return Type(h.typ), int64(h.size), nil | |
| 267 | + | } | |
| 268 | + | typ, err := p.chainEnd(off, h) | |
| 269 | + | if err != nil { | |
| 270 | + | return 0, 0, err | |
| 271 | + | } | |
| 272 | + | // The result size is the second varint of the delta, at its start. | |
| 273 | + | m, err := getMem(p.stream(&h)) | |
| 274 | + | if err != nil { | |
| 275 | + | return 0, 0, err | |
| 276 | + | } | |
| 277 | + | defer putMem(m) | |
| 278 | + | n := min(uint64(len(m.scratch)), h.size) | |
| 279 | + | if _, err := io.ReadFull(m.zr, m.scratch[:n]); err != nil { | |
| 280 | + | return 0, 0, corrupt("delta header: %v", err) | |
| 281 | + | } | |
| 282 | + | _, rs, _, err := deltaHeader(m.scratch[:n]) | |
| 283 | + | if err != nil { | |
| 284 | + | return 0, 0, err | |
| 285 | + | } | |
| 286 | + | if rs > 1<<62 { | |
| 287 | + | return 0, 0, corrupt("delta result size %d", rs) | |
| 288 | + | } | |
| 289 | + | return typ, int64(rs), nil | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | // readPacked resolves the object at off into dst (grown as needed) and | |
| 293 | + | // returns its type and content. | |
| 294 | + | func (r *Repo) readPacked(p *pack, off uint64, dst []byte) (Type, []byte, error) { | |
| 295 | + | h, err := parseEntryHeader(p.data, off) | |
| 296 | + | if err != nil { | |
| 297 | + | return 0, nil, err | |
| 298 | + | } | |
| 299 | + | if !h.isDelta() { | |
| 300 | + | if h.size > uint64(r.opts.maxObject) { | |
| 301 | + | return 0, nil, tooLarge(h.size, r.opts.maxObject) | |
| 302 | + | } | |
| 303 | + | dst = grow(dst, int(h.size)) | |
| 304 | + | if err := p.inflateEntry(&h, dst); err != nil { | |
| 305 | + | return 0, nil, err | |
| 306 | + | } | |
| 307 | + | return Type(h.typ), dst, nil | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // Collect the chain of deltas down to a base that is a plain object or | |
| 311 | + | // a cached result. | |
| 312 | + | type link struct { | |
| 313 | + | hdr entryHdr | |
| 314 | + | off uint64 | |
| 315 | + | } | |
| 316 | + | var arr [64]link | |
| 317 | + | chain := arr[:0] | |
| 318 | + | var ( | |
| 319 | + | baseTyp Type | |
| 320 | + | base []byte | |
| 321 | + | baseFresh bool // base is a private slice that may be cached as is | |
| 322 | + | scratch [2]*buffer | |
| 323 | + | ) | |
| 324 | + | defer func() { | |
| 325 | + | putBuf(scratch[0]) | |
| 326 | + | putBuf(scratch[1]) | |
| 327 | + | }() | |
| 328 | + | cur, curHdr := off, h | |
| 329 | + | for { | |
| 330 | + | if curHdr.isDelta() { | |
| 331 | + | if len(chain) >= maxDeltaDepth { | |
| 332 | + | return 0, nil, corrupt("delta chain longer than %d", maxDeltaDepth) | |
| 333 | + | } | |
| 334 | + | chain = append(chain, link{curHdr, cur}) | |
| 335 | + | next, err := p.baseOffset(&curHdr, cur) | |
| 336 | + | if err != nil { | |
| 337 | + | return 0, nil, err | |
| 338 | + | } | |
| 339 | + | cur = next | |
| 340 | + | if t, data, ok := r.cache.get(cacheKey{p.id, cur}); ok { | |
| 341 | + | baseTyp, base = t, data | |
| 342 | + | break | |
| 343 | + | } | |
| 344 | + | if curHdr, err = parseEntryHeader(p.data, cur); err != nil { | |
| 345 | + | return 0, nil, err | |
| 346 | + | } | |
| 347 | + | continue | |
| 348 | + | } | |
| 349 | + | if curHdr.size > uint64(r.opts.maxObject) { | |
| 350 | + | return 0, nil, tooLarge(curHdr.size, r.opts.maxObject) | |
| 351 | + | } | |
| 352 | + | baseTyp = Type(curHdr.typ) | |
| 353 | + | n := int(curHdr.size) | |
| 354 | + | if r.cache.fits(n) { | |
| 355 | + | base, baseFresh = make([]byte, n), true | |
| 356 | + | } else { | |
| 357 | + | scratch[0] = getBuf() | |
| 358 | + | scratch[0].b = grow(scratch[0].b, n) | |
| 359 | + | base = scratch[0].b | |
| 360 | + | } | |
| 361 | + | if err := p.inflateEntry(&curHdr, base); err != nil { | |
| 362 | + | return 0, nil, err | |
| 363 | + | } | |
| 364 | + | if baseFresh { | |
| 365 | + | r.cache.putOwned(cacheKey{p.id, cur}, baseTyp, base) | |
| 366 | + | } | |
| 367 | + | break | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | delta := getBuf() | |
| 371 | + | defer putBuf(delta) | |
| 372 | + | for i := len(chain) - 1; i >= 0; i-- { | |
| 373 | + | l := chain[i] | |
| 374 | + | if l.hdr.size > uint64(r.opts.maxObject) { | |
| 375 | + | return 0, nil, tooLarge(l.hdr.size, r.opts.maxObject) | |
| 376 | + | } | |
| 377 | + | delta.b = grow(delta.b, int(l.hdr.size)) | |
| 378 | + | if err := p.inflateEntry(&l.hdr, delta.b); err != nil { | |
| 379 | + | return 0, nil, err | |
| 380 | + | } | |
| 381 | + | bs, rs, body, err := deltaHeader(delta.b) | |
| 382 | + | if err != nil { | |
| 383 | + | return 0, nil, err | |
| 384 | + | } | |
| 385 | + | if bs != uint64(len(base)) { | |
| 386 | + | return 0, nil, corrupt("delta at %d expects a base of %d bytes, got %d", l.off, bs, len(base)) | |
| 387 | + | } | |
| 388 | + | if rs > uint64(r.opts.maxObject) { | |
| 389 | + | return 0, nil, tooLarge(rs, r.opts.maxObject) | |
| 390 | + | } | |
| 391 | + | var out []byte | |
| 392 | + | fresh := false | |
| 393 | + | switch { | |
| 394 | + | case i == 0: | |
| 395 | + | out = grow(dst, int(rs)) | |
| 396 | + | case r.cache.fits(int(rs)): | |
| 397 | + | out, fresh = make([]byte, rs), true | |
| 398 | + | default: | |
| 399 | + | // Write into the scratch buffer that does not hold base. | |
| 400 | + | k := 0 | |
| 401 | + | if scratch[0] != nil && len(base) > 0 && len(scratch[0].b) > 0 && &scratch[0].b[0] == &base[0] { | |
| 402 | + | k = 1 | |
| 403 | + | } | |
| 404 | + | if scratch[k] == nil { | |
| 405 | + | scratch[k] = getBuf() | |
| 406 | + | } | |
| 407 | + | scratch[k].b = grow(scratch[k].b, int(rs)) | |
| 408 | + | out = scratch[k].b | |
| 409 | + | } | |
| 410 | + | if err := applyDelta(out, base, delta.b, body); err != nil { | |
| 411 | + | return 0, nil, err | |
| 412 | + | } | |
| 413 | + | if fresh { | |
| 414 | + | r.cache.putOwned(cacheKey{p.id, l.off}, baseTyp, out) | |
| 415 | + | } | |
| 416 | + | base = out | |
| 417 | + | } | |
| 418 | + | return baseTyp, base, nil | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | func tooLarge(size uint64, limit int64) error { | |
| 422 | + | return &sizeError{size: size, limit: limit} | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | type sizeError struct { | |
| 426 | + | size uint64 | |
| 427 | + | limit int64 | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | func (e *sizeError) Error() string { | |
| 431 | + | return ErrTooLarge.Error() + ": " + itoa(e.size) + " bytes, limit " + itoa(uint64(e.limit)) | |
| 432 | + | } | |
| 433 | + | func (e *sizeError) Unwrap() error { return ErrTooLarge } | |
| 434 | + | ||
| 435 | + | func itoa(v uint64) string { | |
| 436 | + | var b [20]byte | |
| 437 | + | i := len(b) | |
| 438 | + | for { | |
| 439 | + | i-- | |
| 440 | + | b[i] = byte('0' + v%10) | |
| 441 | + | v /= 10 | |
| 442 | + | if v == 0 { | |
| 443 | + | break | |
| 444 | + | } | |
| 445 | + | } | |
| 446 | + | return string(b[i:]) | |
| 447 | + | } | |
| @@ -0,0 +1,447 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "encoding/binary" | |||
| 8 | + | "io" | |||
| 9 | + | "os" | |||
| 10 | + | "sync/atomic" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const ( | |||
| 14 | + | packHeaderLen = 12 | |||
| 15 | + | packTrailerLen = 20 | |||
| 16 | + | ||||
| 17 | + | objOfsDelta = 6 | |||
| 18 | + | objRefDelta = 7 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // pack is an open pack file with its index, both mapped read-only. It is | |||
| 22 | + | // shared by the snapshots that list it and unmapped when the last one is | |||
| 23 | + | // released. | |||
| 24 | + | type pack struct { | |||
| 25 | + | id uint32 | |||
| 26 | + | name string // "pack-<hash>", without extension | |||
| 27 | + | idx *idxFile | |||
| 28 | + | data []byte // the whole .pack file | |||
| 29 | + | sig fileSig | |||
| 30 | + | unmap [2]func() error | |||
| 31 | + | refs atomic.Int32 | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (p *pack) retain() { p.refs.Add(1) } | |||
| 35 | + | ||||
| 36 | + | func (p *pack) release() { | |||
| 37 | + | if p.refs.Add(-1) == 0 { | |||
| 38 | + | for _, f := range p.unmap { | |||
| 39 | + | if f != nil { | |||
| 40 | + | _ = f() | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | p.data, p.idx = nil, nil | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // openPack maps and validates dir/<name>.idx and dir/<name>.pack. It returns | |||
| 48 | + | // (nil, nil) when a file vanished, which happens while a repack replaces it. | |||
| 49 | + | func openPack(dir, name string, id uint32, sig fileSig) (*pack, error) { | |||
| 50 | + | idxF, err := os.Open(dir + "/" + name + ".idx") | |||
| 51 | + | if err != nil { | |||
| 52 | + | return nil, ignoreMissing(err) | |||
| 53 | + | } | |||
| 54 | + | defer func() { _ = idxF.Close() }() | |||
| 55 | + | packF, err := os.Open(dir + "/" + name + ".pack") | |||
| 56 | + | if err != nil { | |||
| 57 | + | return nil, ignoreMissing(err) | |||
| 58 | + | } | |||
| 59 | + | defer func() { _ = packF.Close() }() | |||
| 60 | + | idxStat, err := idxF.Stat() | |||
| 61 | + | if err != nil { | |||
| 62 | + | return nil, err | |||
| 63 | + | } | |||
| 64 | + | packStat, err := packF.Stat() | |||
| 65 | + | if err != nil { | |||
| 66 | + | return nil, err | |||
| 67 | + | } | |||
| 68 | + | idxData, unmapIdx, err := mapFile(idxF, idxStat.Size()) | |||
| 69 | + | if err != nil { | |||
| 70 | + | return nil, err | |||
| 71 | + | } | |||
| 72 | + | packData, unmapPack, err := mapFile(packF, packStat.Size()) | |||
| 73 | + | if err != nil { | |||
| 74 | + | _ = unmapIdx() | |||
| 75 | + | return nil, err | |||
| 76 | + | } | |||
| 77 | + | p := &pack{id: id, name: name, data: packData, sig: sig, unmap: [2]func() error{unmapIdx, unmapPack}} | |||
| 78 | + | p.refs.Store(1) | |||
| 79 | + | if p.idx, err = parseIdx(idxData); err != nil { | |||
| 80 | + | p.release() | |||
| 81 | + | return nil, wrapPack(name, err) | |||
| 82 | + | } | |||
| 83 | + | if err := p.validate(); err != nil { | |||
| 84 | + | p.release() | |||
| 85 | + | return nil, wrapPack(name, err) | |||
| 86 | + | } | |||
| 87 | + | return p, nil | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | // validate checks the pack header and ties the pack to its index. | |||
| 91 | + | func (p *pack) validate() error { | |||
| 92 | + | if len(p.data) < packHeaderLen+packTrailerLen { | |||
| 93 | + | return corrupt("pack too short (%d bytes)", len(p.data)) | |||
| 94 | + | } | |||
| 95 | + | if string(p.data[:4]) != "PACK" { | |||
| 96 | + | return corrupt("bad pack signature") | |||
| 97 | + | } | |||
| 98 | + | if v := binary.BigEndian.Uint32(p.data[4:8]); v != 2 && v != 3 { | |||
| 99 | + | return corrupt("pack version %d", v) | |||
| 100 | + | } | |||
| 101 | + | if n := binary.BigEndian.Uint32(p.data[8:12]); n != p.idx.n { | |||
| 102 | + | return corrupt("pack holds %d objects, index %d", n, p.idx.n) | |||
| 103 | + | } | |||
| 104 | + | if !bytes.Equal(p.data[len(p.data)-packTrailerLen:], p.idx.packHash) { | |||
| 105 | + | return corrupt("pack checksum differs from its index") | |||
| 106 | + | } | |||
| 107 | + | return nil | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | func wrapPack(name string, err error) error { | |||
| 111 | + | return &packError{name: name, err: err} | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | type packError struct { | |||
| 115 | + | name string | |||
| 116 | + | err error | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func (e *packError) Error() string { return e.name + ": " + e.err.Error() } | |||
| 120 | + | func (e *packError) Unwrap() error { return e.err } | |||
| 121 | + | ||||
| 122 | + | // entryHdr is a parsed pack entry header. | |||
| 123 | + | type entryHdr struct { | |||
| 124 | + | typ uint8 // 1..4, objOfsDelta or objRefDelta | |||
| 125 | + | size uint64 // inflated size of the entry data (the delta for deltas) | |||
| 126 | + | data uint64 // offset of the zlib stream | |||
| 127 | + | baseOff uint64 // objOfsDelta: absolute offset of the base | |||
| 128 | + | baseID ObjectID | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (h *entryHdr) isDelta() bool { return h.typ == objOfsDelta || h.typ == objRefDelta } | |||
| 132 | + | ||||
| 133 | + | // parseEntryHeader parses the header of the entry at off in a whole pack | |||
| 134 | + | // file (header, entries and trailer). It never reads outside data. | |||
| 135 | + | func parseEntryHeader(data []byte, off uint64) (h entryHdr, err error) { | |||
| 136 | + | end := uint64(len(data)) | |||
| 137 | + | if end < packHeaderLen+packTrailerLen { | |||
| 138 | + | return h, corrupt("pack too short") | |||
| 139 | + | } | |||
| 140 | + | end -= packTrailerLen | |||
| 141 | + | if off < packHeaderLen || off >= end { | |||
| 142 | + | return h, corrupt("entry offset %d outside pack", off) | |||
| 143 | + | } | |||
| 144 | + | p := off | |||
| 145 | + | c := data[p] | |||
| 146 | + | p++ | |||
| 147 | + | h.typ = (c >> 4) & 7 | |||
| 148 | + | h.size = uint64(c & 0x0f) | |||
| 149 | + | for shift := uint(4); c&0x80 != 0; shift += 7 { | |||
| 150 | + | if p >= end || shift > 56 { | |||
| 151 | + | return h, corrupt("bad entry size at %d", off) | |||
| 152 | + | } | |||
| 153 | + | c = data[p] | |||
| 154 | + | p++ | |||
| 155 | + | h.size |= uint64(c&0x7f) << shift | |||
| 156 | + | } | |||
| 157 | + | switch h.typ { | |||
| 158 | + | case uint8(TypeCommit), uint8(TypeTree), uint8(TypeBlob), uint8(TypeTag): | |||
| 159 | + | case objOfsDelta: | |||
| 160 | + | if p >= end { | |||
| 161 | + | return h, corrupt("truncated delta offset at %d", off) | |||
| 162 | + | } | |||
| 163 | + | c = data[p] | |||
| 164 | + | p++ | |||
| 165 | + | rel := uint64(c & 0x7f) | |||
| 166 | + | for c&0x80 != 0 { | |||
| 167 | + | if p >= end || rel >= 1<<56 { | |||
| 168 | + | return h, corrupt("bad delta offset at %d", off) | |||
| 169 | + | } | |||
| 170 | + | c = data[p] | |||
| 171 | + | p++ | |||
| 172 | + | rel = (rel+1)<<7 | uint64(c&0x7f) | |||
| 173 | + | } | |||
| 174 | + | if rel == 0 || rel > off-packHeaderLen { | |||
| 175 | + | return h, corrupt("delta base offset %d outside pack at %d", rel, off) | |||
| 176 | + | } | |||
| 177 | + | h.baseOff = off - rel | |||
| 178 | + | case objRefDelta: | |||
| 179 | + | if end-p < 20 { | |||
| 180 | + | return h, corrupt("truncated delta base id at %d", off) | |||
| 181 | + | } | |||
| 182 | + | copy(h.baseID[:], data[p:p+20]) | |||
| 183 | + | p += 20 | |||
| 184 | + | default: | |||
| 185 | + | return h, corrupt("entry type %d at %d", h.typ, off) | |||
| 186 | + | } | |||
| 187 | + | if p >= end { | |||
| 188 | + | return h, corrupt("entry at %d has no data", off) | |||
| 189 | + | } | |||
| 190 | + | h.data = p | |||
| 191 | + | return h, nil | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // stream returns the compressed bytes of the entry. | |||
| 195 | + | func (p *pack) stream(h *entryHdr) []byte { | |||
| 196 | + | return p.data[h.data : len(p.data)-packTrailerLen] | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // inflateEntry inflates the entry data into dst, which has the exact size | |||
| 200 | + | // announced by the header. | |||
| 201 | + | func (p *pack) inflateEntry(h *entryHdr, dst []byte) error { | |||
| 202 | + | src := p.stream(h) | |||
| 203 | + | // Deflate cannot expand a byte into more than 1032 bytes. | |||
| 204 | + | if h.size/1032 > uint64(len(src)) { | |||
| 205 | + | return corrupt("entry of %d bytes in %d compressed bytes", h.size, len(src)) | |||
| 206 | + | } | |||
| 207 | + | m, err := getMem(src) | |||
| 208 | + | if err != nil { | |||
| 209 | + | return err | |||
| 210 | + | } | |||
| 211 | + | defer putMem(m) | |||
| 212 | + | return inflateExact(m.zr, dst, m.scratch[:]) | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // chainEnd follows the chain of delta headers from off without inflating | |||
| 216 | + | // anything. It returns the type of the base object. | |||
| 217 | + | func (p *pack) chainEnd(off uint64, h entryHdr) (Type, error) { | |||
| 218 | + | for depth := 0; ; depth++ { | |||
| 219 | + | if !h.isDelta() { | |||
| 220 | + | return Type(h.typ), nil | |||
| 221 | + | } | |||
| 222 | + | if depth >= maxDeltaDepth { | |||
| 223 | + | return 0, corrupt("delta chain longer than %d", maxDeltaDepth) | |||
| 224 | + | } | |||
| 225 | + | var err error | |||
| 226 | + | off, err = p.baseOffset(&h, off) | |||
| 227 | + | if err != nil { | |||
| 228 | + | return 0, err | |||
| 229 | + | } | |||
| 230 | + | if h, err = parseEntryHeader(p.data, off); err != nil { | |||
| 231 | + | return 0, err | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // baseOffset returns the offset of the base of a delta entry. A ref-delta | |||
| 237 | + | // base must be in the same pack, as Git requires. | |||
| 238 | + | func (p *pack) baseOffset(h *entryHdr, self uint64) (uint64, error) { | |||
| 239 | + | if h.typ == objOfsDelta { | |||
| 240 | + | return h.baseOff, nil | |||
| 241 | + | } | |||
| 242 | + | pos, ok := p.idx.find(&h.baseID) | |||
| 243 | + | if !ok { | |||
| 244 | + | return 0, corrupt("delta base %s not in pack %s", h.baseID.String(), p.name) | |||
| 245 | + | } | |||
| 246 | + | off, err := p.idx.offset(pos) | |||
| 247 | + | if err != nil { | |||
| 248 | + | return 0, err | |||
| 249 | + | } | |||
| 250 | + | if off == self { | |||
| 251 | + | return 0, corrupt("delta is its own base at %d", self) | |||
| 252 | + | } | |||
| 253 | + | return off, nil | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // header returns the type and size of the object at off. | |||
| 257 | + | func (p *pack) header(off uint64) (Type, int64, error) { | |||
| 258 | + | h, err := parseEntryHeader(p.data, off) | |||
| 259 | + | if err != nil { | |||
| 260 | + | return 0, 0, err | |||
| 261 | + | } | |||
| 262 | + | if !h.isDelta() { | |||
| 263 | + | if h.size > 1<<62 { | |||
| 264 | + | return 0, 0, corrupt("entry size %d", h.size) | |||
| 265 | + | } | |||
| 266 | + | return Type(h.typ), int64(h.size), nil | |||
| 267 | + | } | |||
| 268 | + | typ, err := p.chainEnd(off, h) | |||
| 269 | + | if err != nil { | |||
| 270 | + | return 0, 0, err | |||
| 271 | + | } | |||
| 272 | + | // The result size is the second varint of the delta, at its start. | |||
| 273 | + | m, err := getMem(p.stream(&h)) | |||
| 274 | + | if err != nil { | |||
| 275 | + | return 0, 0, err | |||
| 276 | + | } | |||
| 277 | + | defer putMem(m) | |||
| 278 | + | n := min(uint64(len(m.scratch)), h.size) | |||
| 279 | + | if _, err := io.ReadFull(m.zr, m.scratch[:n]); err != nil { | |||
| 280 | + | return 0, 0, corrupt("delta header: %v", err) | |||
| 281 | + | } | |||
| 282 | + | _, rs, _, err := deltaHeader(m.scratch[:n]) | |||
| 283 | + | if err != nil { | |||
| 284 | + | return 0, 0, err | |||
| 285 | + | } | |||
| 286 | + | if rs > 1<<62 { | |||
| 287 | + | return 0, 0, corrupt("delta result size %d", rs) | |||
| 288 | + | } | |||
| 289 | + | return typ, int64(rs), nil | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | // readPacked resolves the object at off into dst (grown as needed) and | |||
| 293 | + | // returns its type and content. | |||
| 294 | + | func (r *Repo) readPacked(p *pack, off uint64, dst []byte) (Type, []byte, error) { | |||
| 295 | + | h, err := parseEntryHeader(p.data, off) | |||
| 296 | + | if err != nil { | |||
| 297 | + | return 0, nil, err | |||
| 298 | + | } | |||
| 299 | + | if !h.isDelta() { | |||
| 300 | + | if h.size > uint64(r.opts.maxObject) { | |||
| 301 | + | return 0, nil, tooLarge(h.size, r.opts.maxObject) | |||
| 302 | + | } | |||
| 303 | + | dst = grow(dst, int(h.size)) | |||
| 304 | + | if err := p.inflateEntry(&h, dst); err != nil { | |||
| 305 | + | return 0, nil, err | |||
| 306 | + | } | |||
| 307 | + | return Type(h.typ), dst, nil | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | // Collect the chain of deltas down to a base that is a plain object or | |||
| 311 | + | // a cached result. | |||
| 312 | + | type link struct { | |||
| 313 | + | hdr entryHdr | |||
| 314 | + | off uint64 | |||
| 315 | + | } | |||
| 316 | + | var arr [64]link | |||
| 317 | + | chain := arr[:0] | |||
| 318 | + | var ( | |||
| 319 | + | baseTyp Type | |||
| 320 | + | base []byte | |||
| 321 | + | baseFresh bool // base is a private slice that may be cached as is | |||
| 322 | + | scratch [2]*buffer | |||
| 323 | + | ) | |||
| 324 | + | defer func() { | |||
| 325 | + | putBuf(scratch[0]) | |||
| 326 | + | putBuf(scratch[1]) | |||
| 327 | + | }() | |||
| 328 | + | cur, curHdr := off, h | |||
| 329 | + | for { | |||
| 330 | + | if curHdr.isDelta() { | |||
| 331 | + | if len(chain) >= maxDeltaDepth { | |||
| 332 | + | return 0, nil, corrupt("delta chain longer than %d", maxDeltaDepth) | |||
| 333 | + | } | |||
| 334 | + | chain = append(chain, link{curHdr, cur}) | |||
| 335 | + | next, err := p.baseOffset(&curHdr, cur) | |||
| 336 | + | if err != nil { | |||
| 337 | + | return 0, nil, err | |||
| 338 | + | } | |||
| 339 | + | cur = next | |||
| 340 | + | if t, data, ok := r.cache.get(cacheKey{p.id, cur}); ok { | |||
| 341 | + | baseTyp, base = t, data | |||
| 342 | + | break | |||
| 343 | + | } | |||
| 344 | + | if curHdr, err = parseEntryHeader(p.data, cur); err != nil { | |||
| 345 | + | return 0, nil, err | |||
| 346 | + | } | |||
| 347 | + | continue | |||
| 348 | + | } | |||
| 349 | + | if curHdr.size > uint64(r.opts.maxObject) { | |||
| 350 | + | return 0, nil, tooLarge(curHdr.size, r.opts.maxObject) | |||
| 351 | + | } | |||
| 352 | + | baseTyp = Type(curHdr.typ) | |||
| 353 | + | n := int(curHdr.size) | |||
| 354 | + | if r.cache.fits(n) { | |||
| 355 | + | base, baseFresh = make([]byte, n), true | |||
| 356 | + | } else { | |||
| 357 | + | scratch[0] = getBuf() | |||
| 358 | + | scratch[0].b = grow(scratch[0].b, n) | |||
| 359 | + | base = scratch[0].b | |||
| 360 | + | } | |||
| 361 | + | if err := p.inflateEntry(&curHdr, base); err != nil { | |||
| 362 | + | return 0, nil, err | |||
| 363 | + | } | |||
| 364 | + | if baseFresh { | |||
| 365 | + | r.cache.putOwned(cacheKey{p.id, cur}, baseTyp, base) | |||
| 366 | + | } | |||
| 367 | + | break | |||
| 368 | + | } | |||
| 369 | + | ||||
| 370 | + | delta := getBuf() | |||
| 371 | + | defer putBuf(delta) | |||
| 372 | + | for i := len(chain) - 1; i >= 0; i-- { | |||
| 373 | + | l := chain[i] | |||
| 374 | + | if l.hdr.size > uint64(r.opts.maxObject) { | |||
| 375 | + | return 0, nil, tooLarge(l.hdr.size, r.opts.maxObject) | |||
| 376 | + | } | |||
| 377 | + | delta.b = grow(delta.b, int(l.hdr.size)) | |||
| 378 | + | if err := p.inflateEntry(&l.hdr, delta.b); err != nil { | |||
| 379 | + | return 0, nil, err | |||
| 380 | + | } | |||
| 381 | + | bs, rs, body, err := deltaHeader(delta.b) | |||
| 382 | + | if err != nil { | |||
| 383 | + | return 0, nil, err | |||
| 384 | + | } | |||
| 385 | + | if bs != uint64(len(base)) { | |||
| 386 | + | return 0, nil, corrupt("delta at %d expects a base of %d bytes, got %d", l.off, bs, len(base)) | |||
| 387 | + | } | |||
| 388 | + | if rs > uint64(r.opts.maxObject) { | |||
| 389 | + | return 0, nil, tooLarge(rs, r.opts.maxObject) | |||
| 390 | + | } | |||
| 391 | + | var out []byte | |||
| 392 | + | fresh := false | |||
| 393 | + | switch { | |||
| 394 | + | case i == 0: | |||
| 395 | + | out = grow(dst, int(rs)) | |||
| 396 | + | case r.cache.fits(int(rs)): | |||
| 397 | + | out, fresh = make([]byte, rs), true | |||
| 398 | + | default: | |||
| 399 | + | // Write into the scratch buffer that does not hold base. | |||
| 400 | + | k := 0 | |||
| 401 | + | if scratch[0] != nil && len(base) > 0 && len(scratch[0].b) > 0 && &scratch[0].b[0] == &base[0] { | |||
| 402 | + | k = 1 | |||
| 403 | + | } | |||
| 404 | + | if scratch[k] == nil { | |||
| 405 | + | scratch[k] = getBuf() | |||
| 406 | + | } | |||
| 407 | + | scratch[k].b = grow(scratch[k].b, int(rs)) | |||
| 408 | + | out = scratch[k].b | |||
| 409 | + | } | |||
| 410 | + | if err := applyDelta(out, base, delta.b, body); err != nil { | |||
| 411 | + | return 0, nil, err | |||
| 412 | + | } | |||
| 413 | + | if fresh { | |||
| 414 | + | r.cache.putOwned(cacheKey{p.id, l.off}, baseTyp, out) | |||
| 415 | + | } | |||
| 416 | + | base = out | |||
| 417 | + | } | |||
| 418 | + | return baseTyp, base, nil | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | func tooLarge(size uint64, limit int64) error { | |||
| 422 | + | return &sizeError{size: size, limit: limit} | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | type sizeError struct { | |||
| 426 | + | size uint64 | |||
| 427 | + | limit int64 | |||
| 428 | + | } | |||
| 429 | + | ||||
| 430 | + | func (e *sizeError) Error() string { | |||
| 431 | + | return ErrTooLarge.Error() + ": " + itoa(e.size) + " bytes, limit " + itoa(uint64(e.limit)) | |||
| 432 | + | } | |||
| 433 | + | func (e *sizeError) Unwrap() error { return ErrTooLarge } | |||
| 434 | + | ||||
| 435 | + | func itoa(v uint64) string { | |||
| 436 | + | var b [20]byte | |||
| 437 | + | i := len(b) | |||
| 438 | + | for { | |||
| 439 | + | i-- | |||
| 440 | + | b[i] = byte('0' + v%10) | |||
| 441 | + | v /= 10 | |||
| 442 | + | if v == 0 { | |||
| 443 | + | break | |||
| 444 | + | } | |||
| 445 | + | } | |||
| 446 | + | return string(b[i:]) | |||
| 447 | + | } | |||
A refs.go +536 -0
| @@ -0,0 +1,536 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "fmt" | |
| 8 | + | "io" | |
| 9 | + | "io/fs" | |
| 10 | + | "iter" | |
| 11 | + | "os" | |
| 12 | + | "slices" | |
| 13 | + | "strings" | |
| 14 | + | "syscall" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | const ( | |
| 18 | + | // maxRefFile bounds a loose ref file or HEAD. | |
| 19 | + | maxRefFile = 4096 | |
| 20 | + | // maxPackedRefs bounds the packed-refs file that is loaded. | |
| 21 | + | maxPackedRefs = 1 << 30 | |
| 22 | + | // maxSymrefDepth is the number of symbolic refs followed, as in Git. | |
| 23 | + | maxSymrefDepth = 5 | |
| 24 | + | ) | |
| 25 | + | ||
| 26 | + | // Ref is a reference. | |
| 27 | + | type Ref struct { | |
| 28 | + | // Name is the full name, such as "refs/heads/main". | |
| 29 | + | Name string | |
| 30 | + | // ID is the object the ref designates, after following symbolic refs. | |
| 31 | + | // It may be an annotated tag; see Repo.PeelRef. | |
| 32 | + | ID ObjectID | |
| 33 | + | // Symref is the name of the ref this one points to when it is a symbolic | |
| 34 | + | // ref, empty otherwise. | |
| 35 | + | Symref string | |
| 36 | + | // Peeled is the object that ID designates once annotated tags are | |
| 37 | + | // followed. It is only set when PeeledKnown, which is the case for refs | |
| 38 | + | // read from packed-refs that carry peeled values. | |
| 39 | + | Peeled ObjectID | |
| 40 | + | PeeledKnown bool | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // packedRef is an entry of packed-refs. | |
| 44 | + | type packedRef struct { | |
| 45 | + | name string | |
| 46 | + | id ObjectID | |
| 47 | + | peeled ObjectID | |
| 48 | + | peeledKnown bool | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // packedRefs is an immutable parsed packed-refs file, sorted by name. | |
| 52 | + | type packedRefs struct { | |
| 53 | + | sig fileSig | |
| 54 | + | refs []packedRef | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func (p *packedRefs) find(name string) (packedRef, bool) { | |
| 58 | + | i, ok := slices.BinarySearchFunc(p.refs, name, func(e packedRef, n string) int { | |
| 59 | + | return strings.Compare(e.name, n) | |
| 60 | + | }) | |
| 61 | + | if !ok { | |
| 62 | + | return packedRef{}, false | |
| 63 | + | } | |
| 64 | + | return p.refs[i], true | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // loadPacked returns the current content of packed-refs, reparsing the file | |
| 68 | + | // when its stat data changed (or always with force). | |
| 69 | + | func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | |
| 70 | + | path := r.dir + "/packed-refs" | |
| 71 | + | sig, err := statSig(path) | |
| 72 | + | if err != nil { | |
| 73 | + | return nil, err | |
| 74 | + | } | |
| 75 | + | if cur := r.packed.Load(); !force && cur != nil && cur.sig.same(sig) { | |
| 76 | + | return cur, nil | |
| 77 | + | } | |
| 78 | + | r.pmu.Lock() | |
| 79 | + | defer r.pmu.Unlock() | |
| 80 | + | f, err := os.Open(path) | |
| 81 | + | if errors.Is(err, fs.ErrNotExist) { | |
| 82 | + | p := &packedRefs{} | |
| 83 | + | r.packed.Store(p) | |
| 84 | + | return p, nil | |
| 85 | + | } | |
| 86 | + | if err != nil { | |
| 87 | + | return nil, err | |
| 88 | + | } | |
| 89 | + | defer func() { _ = f.Close() }() | |
| 90 | + | st, err := f.Stat() | |
| 91 | + | if err != nil { | |
| 92 | + | return nil, err | |
| 93 | + | } | |
| 94 | + | if st.Size() > maxPackedRefs { | |
| 95 | + | return nil, unsupported("packed-refs larger than %d bytes", maxPackedRefs) | |
| 96 | + | } | |
| 97 | + | data := make([]byte, st.Size()) | |
| 98 | + | if _, err := io.ReadFull(f, data); err != nil { | |
| 99 | + | return nil, err | |
| 100 | + | } | |
| 101 | + | p, err := parsePackedRefs(string(data)) | |
| 102 | + | if err != nil { | |
| 103 | + | return nil, err | |
| 104 | + | } | |
| 105 | + | if i, _ := slices.BinarySearchFunc(p.refs, "refs/replace/", func(e packedRef, n string) int { | |
| 106 | + | return strings.Compare(e.name, n) | |
| 107 | + | }); i < len(p.refs) && strings.HasPrefix(p.refs[i].name, "refs/replace/") { | |
| 108 | + | return nil, unsupported("replace refs (refs/replace in packed-refs)") | |
| 109 | + | } | |
| 110 | + | p.sig = sigOf(st) | |
| 111 | + | r.packed.Store(p) | |
| 112 | + | return p, nil | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // parsePackedRefs parses a packed-refs file. The ref names are substrings of | |
| 116 | + | // data. | |
| 117 | + | func parsePackedRefs(data string) (*packedRefs, error) { | |
| 118 | + | p := &packedRefs{} | |
| 119 | + | fullyPeeled, peeled := false, false | |
| 120 | + | first := true | |
| 121 | + | for len(data) > 0 { | |
| 122 | + | var line string | |
| 123 | + | line, data, _ = strings.Cut(data, "\n") | |
| 124 | + | line = strings.TrimSuffix(line, "\r") | |
| 125 | + | if line == "" { | |
| 126 | + | continue | |
| 127 | + | } | |
| 128 | + | switch line[0] { | |
| 129 | + | case '#': | |
| 130 | + | if first && strings.HasPrefix(line, "# pack-refs with:") { | |
| 131 | + | for _, tr := range strings.Fields(line[len("# pack-refs with:"):]) { | |
| 132 | + | switch tr { | |
| 133 | + | case "fully-peeled": | |
| 134 | + | fullyPeeled = true | |
| 135 | + | case "peeled": | |
| 136 | + | peeled = true | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | first = false | |
| 141 | + | continue | |
| 142 | + | case '^': | |
| 143 | + | if len(p.refs) == 0 { | |
| 144 | + | return nil, corrupt("packed-refs: peeled line without a ref") | |
| 145 | + | } | |
| 146 | + | id, ok := parseIDBytes([]byte(line[1:])) | |
| 147 | + | if !ok { | |
| 148 | + | return nil, corrupt("packed-refs: bad peeled line") | |
| 149 | + | } | |
| 150 | + | last := &p.refs[len(p.refs)-1] | |
| 151 | + | last.peeled, last.peeledKnown = id, true | |
| 152 | + | continue | |
| 153 | + | } | |
| 154 | + | first = false | |
| 155 | + | hex, name, ok := strings.Cut(line, " ") | |
| 156 | + | if !ok || len(hex) != 40 || name == "" { | |
| 157 | + | return nil, corrupt("packed-refs: bad line %q", truncate(line)) | |
| 158 | + | } | |
| 159 | + | id, ok := parseIDBytes([]byte(hex)) | |
| 160 | + | if !ok { | |
| 161 | + | return nil, corrupt("packed-refs: bad id in line %q", truncate(line)) | |
| 162 | + | } | |
| 163 | + | e := packedRef{name: name, id: id} | |
| 164 | + | // Without a ^ line, a ref is known not to be a tag when the file | |
| 165 | + | // is fully peeled (or, for the old "peeled" trait, outside tags). | |
| 166 | + | if fullyPeeled || (peeled && strings.HasPrefix(name, "refs/tags/")) { | |
| 167 | + | e.peeled, e.peeledKnown = id, true | |
| 168 | + | } | |
| 169 | + | p.refs = append(p.refs, e) | |
| 170 | + | } | |
| 171 | + | cmp := func(a, b packedRef) int { return strings.Compare(a.name, b.name) } | |
| 172 | + | if !slices.IsSortedFunc(p.refs, cmp) { | |
| 173 | + | slices.SortStableFunc(p.refs, cmp) | |
| 174 | + | } | |
| 175 | + | return p, nil | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // validRefName rejects names that could leave the repository directory or | |
| 179 | + | // that Git itself refuses. It is a subset of git check-ref-format. | |
| 180 | + | func validRefName(name string) bool { | |
| 181 | + | if !strings.HasPrefix(name, "refs/") { | |
| 182 | + | return false | |
| 183 | + | } | |
| 184 | + | if name == "" || len(name) > 1024 || name[0] == '/' || name[len(name)-1] == '/' || strings.HasSuffix(name, ".lock") { | |
| 185 | + | return false | |
| 186 | + | } | |
| 187 | + | if strings.Contains(name, "..") || strings.Contains(name, "//") || strings.Contains(name, "@{") { | |
| 188 | + | return false | |
| 189 | + | } | |
| 190 | + | for _, c := range []byte(name) { | |
| 191 | + | if c < 0x20 || c == 0x7f || c == '\\' || c == ' ' || c == '~' || c == '^' || c == ':' || c == '?' || c == '*' || c == '[' { | |
| 192 | + | return false | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | for comp := range strings.SplitSeq(name, "/") { | |
| 196 | + | if comp == "" || comp[0] == '.' || strings.HasSuffix(comp, ".lock") { | |
| 197 | + | return false | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | return true | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // looseRef is the content of a loose ref file. | |
| 204 | + | type looseRef struct { | |
| 205 | + | id ObjectID | |
| 206 | + | symref string | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | // readLooseRef reads a loose ref. found is false when there is no such file | |
| 210 | + | // (or it is a directory). | |
| 211 | + | func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | |
| 212 | + | f, err := os.Open(r.dir + "/" + name) | |
| 213 | + | if err != nil { | |
| 214 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 215 | + | return lr, false, nil | |
| 216 | + | } | |
| 217 | + | return lr, false, err | |
| 218 | + | } | |
| 219 | + | defer func() { _ = f.Close() }() | |
| 220 | + | pb := getBuf() | |
| 221 | + | defer putBuf(pb) | |
| 222 | + | pb.b = grow(pb.b, maxRefFile+1) | |
| 223 | + | buf := pb.b | |
| 224 | + | n, err := io.ReadFull(f, buf) | |
| 225 | + | if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { | |
| 226 | + | err = nil | |
| 227 | + | } | |
| 228 | + | if err != nil { | |
| 229 | + | var pe *fs.PathError | |
| 230 | + | if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) { | |
| 231 | + | return lr, false, nil | |
| 232 | + | } | |
| 233 | + | return lr, false, err | |
| 234 | + | } | |
| 235 | + | if n > maxRefFile { | |
| 236 | + | return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile) | |
| 237 | + | } | |
| 238 | + | lr, err = parseRefContent(buf[:n]) | |
| 239 | + | if err != nil { | |
| 240 | + | return lr, true, fmt.Errorf("ref %s: %w", name, err) | |
| 241 | + | } | |
| 242 | + | return lr, true, nil | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | func parseRefContent(b []byte) (looseRef, error) { | |
| 246 | + | s := strings.TrimRight(string(b), " \t\r\n") | |
| 247 | + | if target, ok := strings.CutPrefix(s, "ref:"); ok { | |
| 248 | + | target = strings.TrimSpace(target) | |
| 249 | + | if !validRef(target) { | |
| 250 | + | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target)) | |
| 251 | + | } | |
| 252 | + | return looseRef{symref: target}, nil | |
| 253 | + | } | |
| 254 | + | if len(s) >= 40 { | |
| 255 | + | if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') { | |
| 256 | + | return looseRef{id: id}, nil | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | return looseRef{}, corrupt("bad ref content %q", truncate(s)) | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // validRootRef reports whether name is a pseudo-ref of the Git directory such | |
| 263 | + | // as HEAD or ORIG_HEAD: upper case letters and underscores only. | |
| 264 | + | func validRootRef(name string) bool { | |
| 265 | + | if name == "" || len(name) > 64 { | |
| 266 | + | return false | |
| 267 | + | } | |
| 268 | + | for _, c := range []byte(name) { | |
| 269 | + | if (c < 'A' || c > 'Z') && c != '_' { | |
| 270 | + | return false | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | return true | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | // validRef accepts the names lookupRef may read. | |
| 277 | + | func validRef(name string) bool { return validRefName(name) || validRootRef(name) } | |
| 278 | + | ||
| 279 | + | // lookupRef resolves name through symbolic refs. loose refs win over packed | |
| 280 | + | // ones. The loose file is read before packed-refs, so a concurrent pack-refs | |
| 281 | + | // (which writes packed-refs first and deletes the loose file after) is never | |
| 282 | + | // missed. | |
| 283 | + | func (r *Repo) lookupRef(name string) (Ref, bool, error) { | |
| 284 | + | if !validRef(name) { | |
| 285 | + | return Ref{}, false, nil | |
| 286 | + | } | |
| 287 | + | ref := Ref{Name: name} | |
| 288 | + | cur := name | |
| 289 | + | for depth := 0; ; depth++ { | |
| 290 | + | if depth > maxSymrefDepth { | |
| 291 | + | return Ref{}, false, corrupt("ref %s: symbolic ref loop", name) | |
| 292 | + | } | |
| 293 | + | lr, found, err := r.readLooseRef(cur) | |
| 294 | + | if err != nil { | |
| 295 | + | return Ref{}, false, err | |
| 296 | + | } | |
| 297 | + | if found && lr.symref != "" { | |
| 298 | + | if depth == 0 { | |
| 299 | + | ref.Symref = lr.symref | |
| 300 | + | } | |
| 301 | + | if !validRef(lr.symref) { | |
| 302 | + | return Ref{}, false, nil | |
| 303 | + | } | |
| 304 | + | cur = lr.symref | |
| 305 | + | continue | |
| 306 | + | } | |
| 307 | + | if found { | |
| 308 | + | ref.ID = lr.id | |
| 309 | + | return ref, true, nil | |
| 310 | + | } | |
| 311 | + | p, err := r.loadPacked(false) | |
| 312 | + | if err != nil { | |
| 313 | + | return Ref{}, false, err | |
| 314 | + | } | |
| 315 | + | e, ok := p.find(cur) | |
| 316 | + | if !ok { | |
| 317 | + | return Ref{}, false, nil | |
| 318 | + | } | |
| 319 | + | ref.ID = e.id | |
| 320 | + | if depth == 0 { | |
| 321 | + | ref.Peeled, ref.PeeledKnown = e.peeled, e.peeledKnown | |
| 322 | + | } | |
| 323 | + | return ref, true, nil | |
| 324 | + | } | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // ResolveRef returns the object the ref with this full name (such as | |
| 328 | + | // "refs/heads/main", "refs/tags/v1" or "HEAD") designates, following | |
| 329 | + | // symbolic refs. The result may be an annotated tag; see PeelToCommit. It | |
| 330 | + | // returns ErrNotFound if the ref does not exist. | |
| 331 | + | func (r *Repo) ResolveRef(name string) (ObjectID, error) { | |
| 332 | + | ref, ok, err := r.lookupRef(name) | |
| 333 | + | if err != nil { | |
| 334 | + | return ObjectID{}, err | |
| 335 | + | } | |
| 336 | + | if !ok { | |
| 337 | + | return ObjectID{}, notFound("ref %s", name) | |
| 338 | + | } | |
| 339 | + | return ref.ID, nil | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | // LookupRef is like ResolveRef but returns the whole Ref. | |
| 343 | + | func (r *Repo) LookupRef(name string) (Ref, error) { | |
| 344 | + | ref, ok, err := r.lookupRef(name) | |
| 345 | + | if err != nil { | |
| 346 | + | return Ref{}, err | |
| 347 | + | } | |
| 348 | + | if !ok { | |
| 349 | + | return Ref{}, notFound("ref %s", name) | |
| 350 | + | } | |
| 351 | + | return ref, nil | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | // Head returns the full name of the ref HEAD points to ("refs/heads/main") | |
| 355 | + | // and the object it designates. An unborn branch (the target does not exist | |
| 356 | + | // yet) is not an error: id is then the zero ObjectID. For a detached HEAD | |
| 357 | + | // target is empty and id is the commit. | |
| 358 | + | func (r *Repo) Head() (target string, id ObjectID, err error) { | |
| 359 | + | lr, found, err := r.readLooseRef("HEAD") | |
| 360 | + | if err != nil { | |
| 361 | + | return "", ObjectID{}, err | |
| 362 | + | } | |
| 363 | + | if !found { | |
| 364 | + | return "", ObjectID{}, notFound("HEAD") | |
| 365 | + | } | |
| 366 | + | if lr.symref == "" { | |
| 367 | + | return "", lr.id, nil | |
| 368 | + | } | |
| 369 | + | ref, ok, err := r.lookupRef(lr.symref) | |
| 370 | + | if err != nil { | |
| 371 | + | return "", ObjectID{}, err | |
| 372 | + | } | |
| 373 | + | if !ok { | |
| 374 | + | return lr.symref, ObjectID{}, nil | |
| 375 | + | } | |
| 376 | + | return lr.symref, ref.ID, nil | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // Refs iterates over the refs whose full name starts with prefix, such as | |
| 380 | + | // "refs/heads/" or "refs/tags/" ("" for all of them), sorted by full name. | |
| 381 | + | // Loose refs override packed ones; the peeled lines of packed-refs are used. | |
| 382 | + | // Symbolic refs are resolved; a ref that cannot be read is skipped. HEAD is | |
| 383 | + | // not part of it, see Head. An error ends the iteration. | |
| 384 | + | func (r *Repo) Refs(prefix string) iter.Seq2[Ref, error] { | |
| 385 | + | return func(yield func(Ref, error) bool) { | |
| 386 | + | refs, err := r.collectRefs(prefix) | |
| 387 | + | if err != nil { | |
| 388 | + | yield(Ref{}, err) | |
| 389 | + | return | |
| 390 | + | } | |
| 391 | + | for _, ref := range refs { | |
| 392 | + | if !yield(ref, nil) { | |
| 393 | + | return | |
| 394 | + | } | |
| 395 | + | } | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | func (r *Repo) collectRefs(prefix string) ([]Ref, error) { | |
| 400 | + | // Loose refs first, packed-refs after: see lookupRef. | |
| 401 | + | var dir string | |
| 402 | + | switch { | |
| 403 | + | case strings.HasPrefix(prefix, "refs/"): | |
| 404 | + | dir = prefix[:strings.LastIndexByte(prefix, '/')] | |
| 405 | + | case strings.HasPrefix("refs/", prefix): | |
| 406 | + | dir = "refs" | |
| 407 | + | default: | |
| 408 | + | return nil, nil | |
| 409 | + | } | |
| 410 | + | var loose []string | |
| 411 | + | if err := r.walkLoose(dir, prefix, &loose, 0); err != nil { | |
| 412 | + | return nil, err | |
| 413 | + | } | |
| 414 | + | p, err := r.loadPacked(false) | |
| 415 | + | if err != nil { | |
| 416 | + | return nil, err | |
| 417 | + | } | |
| 418 | + | names := loose | |
| 419 | + | start, _ := slices.BinarySearchFunc(p.refs, prefix, func(e packedRef, pre string) int { | |
| 420 | + | if strings.HasPrefix(e.name, pre) { | |
| 421 | + | return 0 | |
| 422 | + | } | |
| 423 | + | return strings.Compare(e.name, pre) | |
| 424 | + | }) | |
| 425 | + | // BinarySearchFunc with a prefix comparator may land inside the range: | |
| 426 | + | // walk back to its first element. | |
| 427 | + | for start > 0 && strings.HasPrefix(p.refs[start-1].name, prefix) { | |
| 428 | + | start-- | |
| 429 | + | } | |
| 430 | + | for i := start; i < len(p.refs) && strings.HasPrefix(p.refs[i].name, prefix); i++ { | |
| 431 | + | names = append(names, p.refs[i].name) | |
| 432 | + | } | |
| 433 | + | slices.Sort(names) | |
| 434 | + | names = slices.Compact(names) | |
| 435 | + | ||
| 436 | + | out := make([]Ref, 0, len(names)) | |
| 437 | + | for _, n := range names { | |
| 438 | + | ref, ok, err := r.lookupRef(n) | |
| 439 | + | if err != nil { | |
| 440 | + | if errors.Is(err, ErrCorrupt) { | |
| 441 | + | continue // Git ignores broken refs when listing | |
| 442 | + | } | |
| 443 | + | return nil, err | |
| 444 | + | } | |
| 445 | + | if ok { | |
| 446 | + | ref.Name = n | |
| 447 | + | out = append(out, ref) | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | return out, nil | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | // maxRefDirDepth bounds the recursion into refs directories. | |
| 454 | + | const maxRefDirDepth = 64 | |
| 455 | + | ||
| 456 | + | func (r *Repo) walkLoose(dir, prefix string, out *[]string, depth int) error { | |
| 457 | + | if depth > maxRefDirDepth { | |
| 458 | + | return nil | |
| 459 | + | } | |
| 460 | + | entries, err := os.ReadDir(r.dir + "/" + dir) | |
| 461 | + | if err != nil { | |
| 462 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 463 | + | return nil | |
| 464 | + | } | |
| 465 | + | return err | |
| 466 | + | } | |
| 467 | + | for _, e := range entries { | |
| 468 | + | name := dir + "/" + e.Name() | |
| 469 | + | if e.IsDir() { | |
| 470 | + | // Descend only where the prefix can match. | |
| 471 | + | if strings.HasPrefix(name+"/", prefix) || strings.HasPrefix(prefix, name+"/") { | |
| 472 | + | if err := r.walkLoose(name, prefix, out, depth+1); err != nil { | |
| 473 | + | return err | |
| 474 | + | } | |
| 475 | + | } | |
| 476 | + | continue | |
| 477 | + | } | |
| 478 | + | if strings.HasPrefix(name, prefix) && validRefName(name) { | |
| 479 | + | *out = append(*out, name) | |
| 480 | + | } | |
| 481 | + | } | |
| 482 | + | return nil | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // PeelRef returns the commit a ref designates, following annotated tags. It | |
| 486 | + | // uses the peeled value of packed-refs when the ref carries one. It returns | |
| 487 | + | // ErrWrongType if the ref designates something else than a commit. | |
| 488 | + | func (r *Repo) PeelRef(ref Ref) (ObjectID, error) { | |
| 489 | + | if ref.PeeledKnown { | |
| 490 | + | typ, _, err := r.ObjectHeader(ref.Peeled) | |
| 491 | + | if err != nil { | |
| 492 | + | return ObjectID{}, err | |
| 493 | + | } | |
| 494 | + | if typ != TypeCommit { | |
| 495 | + | return ObjectID{}, wrongType(ref.Peeled, typ, TypeCommit) | |
| 496 | + | } | |
| 497 | + | return ref.Peeled, nil | |
| 498 | + | } | |
| 499 | + | return r.PeelToCommit(ref.ID) | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | // Resolve turns a revision name into an object id the way "git rev-parse" | |
| 503 | + | // does for plain names: a full 40 digit id, "HEAD", a full ref name, a short | |
| 504 | + | // ref name tried as "<n>", "refs/<n>", "refs/tags/<n>", "refs/heads/<n>", | |
| 505 | + | // "refs/remotes/<n>" and "refs/remotes/<n>/HEAD" in that order, and finally | |
| 506 | + | // an abbreviated id of 4 to 39 digits. Revision operators (~, ^, @{}) are not | |
| 507 | + | // supported. The result is not peeled; use PeelToCommit. | |
| 508 | + | func (r *Repo) Resolve(rev string) (ObjectID, error) { | |
| 509 | + | if rev == "" { | |
| 510 | + | return ObjectID{}, notFound("empty revision") | |
| 511 | + | } | |
| 512 | + | if len(rev) == 40 { | |
| 513 | + | if id, err := ParseID(rev); err == nil { | |
| 514 | + | if !r.HasObject(id) { | |
| 515 | + | return ObjectID{}, notFound("object %s", id) | |
| 516 | + | } | |
| 517 | + | return id, nil | |
| 518 | + | } | |
| 519 | + | } | |
| 520 | + | for _, pat := range [...]string{"%s", "refs/%s", "refs/tags/%s", "refs/heads/%s", "refs/remotes/%s", "refs/remotes/%s/HEAD"} { | |
| 521 | + | ref, ok, err := r.lookupRef(fmt.Sprintf(pat, rev)) | |
| 522 | + | if err != nil { | |
| 523 | + | return ObjectID{}, err | |
| 524 | + | } | |
| 525 | + | if ok { | |
| 526 | + | return ref.ID, nil | |
| 527 | + | } | |
| 528 | + | } | |
| 529 | + | if len(rev) >= minPrefix && len(rev) < 40 { | |
| 530 | + | id, err := r.ResolvePrefix(rev) | |
| 531 | + | if err == nil || !errors.Is(err, ErrInvalidID) { | |
| 532 | + | return id, err | |
| 533 | + | } | |
| 534 | + | } | |
| 535 | + | return ObjectID{}, notFound("revision %q", truncate(rev)) | |
| 536 | + | } | |
| @@ -0,0 +1,536 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "fmt" | |||
| 8 | + | "io" | |||
| 9 | + | "io/fs" | |||
| 10 | + | "iter" | |||
| 11 | + | "os" | |||
| 12 | + | "slices" | |||
| 13 | + | "strings" | |||
| 14 | + | "syscall" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | const ( | |||
| 18 | + | // maxRefFile bounds a loose ref file or HEAD. | |||
| 19 | + | maxRefFile = 4096 | |||
| 20 | + | // maxPackedRefs bounds the packed-refs file that is loaded. | |||
| 21 | + | maxPackedRefs = 1 << 30 | |||
| 22 | + | // maxSymrefDepth is the number of symbolic refs followed, as in Git. | |||
| 23 | + | maxSymrefDepth = 5 | |||
| 24 | + | ) | |||
| 25 | + | ||||
| 26 | + | // Ref is a reference. | |||
| 27 | + | type Ref struct { | |||
| 28 | + | // Name is the full name, such as "refs/heads/main". | |||
| 29 | + | Name string | |||
| 30 | + | // ID is the object the ref designates, after following symbolic refs. | |||
| 31 | + | // It may be an annotated tag; see Repo.PeelRef. | |||
| 32 | + | ID ObjectID | |||
| 33 | + | // Symref is the name of the ref this one points to when it is a symbolic | |||
| 34 | + | // ref, empty otherwise. | |||
| 35 | + | Symref string | |||
| 36 | + | // Peeled is the object that ID designates once annotated tags are | |||
| 37 | + | // followed. It is only set when PeeledKnown, which is the case for refs | |||
| 38 | + | // read from packed-refs that carry peeled values. | |||
| 39 | + | Peeled ObjectID | |||
| 40 | + | PeeledKnown bool | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // packedRef is an entry of packed-refs. | |||
| 44 | + | type packedRef struct { | |||
| 45 | + | name string | |||
| 46 | + | id ObjectID | |||
| 47 | + | peeled ObjectID | |||
| 48 | + | peeledKnown bool | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // packedRefs is an immutable parsed packed-refs file, sorted by name. | |||
| 52 | + | type packedRefs struct { | |||
| 53 | + | sig fileSig | |||
| 54 | + | refs []packedRef | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func (p *packedRefs) find(name string) (packedRef, bool) { | |||
| 58 | + | i, ok := slices.BinarySearchFunc(p.refs, name, func(e packedRef, n string) int { | |||
| 59 | + | return strings.Compare(e.name, n) | |||
| 60 | + | }) | |||
| 61 | + | if !ok { | |||
| 62 | + | return packedRef{}, false | |||
| 63 | + | } | |||
| 64 | + | return p.refs[i], true | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // loadPacked returns the current content of packed-refs, reparsing the file | |||
| 68 | + | // when its stat data changed (or always with force). | |||
| 69 | + | func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | |||
| 70 | + | path := r.dir + "/packed-refs" | |||
| 71 | + | sig, err := statSig(path) | |||
| 72 | + | if err != nil { | |||
| 73 | + | return nil, err | |||
| 74 | + | } | |||
| 75 | + | if cur := r.packed.Load(); !force && cur != nil && cur.sig.same(sig) { | |||
| 76 | + | return cur, nil | |||
| 77 | + | } | |||
| 78 | + | r.pmu.Lock() | |||
| 79 | + | defer r.pmu.Unlock() | |||
| 80 | + | f, err := os.Open(path) | |||
| 81 | + | if errors.Is(err, fs.ErrNotExist) { | |||
| 82 | + | p := &packedRefs{} | |||
| 83 | + | r.packed.Store(p) | |||
| 84 | + | return p, nil | |||
| 85 | + | } | |||
| 86 | + | if err != nil { | |||
| 87 | + | return nil, err | |||
| 88 | + | } | |||
| 89 | + | defer func() { _ = f.Close() }() | |||
| 90 | + | st, err := f.Stat() | |||
| 91 | + | if err != nil { | |||
| 92 | + | return nil, err | |||
| 93 | + | } | |||
| 94 | + | if st.Size() > maxPackedRefs { | |||
| 95 | + | return nil, unsupported("packed-refs larger than %d bytes", maxPackedRefs) | |||
| 96 | + | } | |||
| 97 | + | data := make([]byte, st.Size()) | |||
| 98 | + | if _, err := io.ReadFull(f, data); err != nil { | |||
| 99 | + | return nil, err | |||
| 100 | + | } | |||
| 101 | + | p, err := parsePackedRefs(string(data)) | |||
| 102 | + | if err != nil { | |||
| 103 | + | return nil, err | |||
| 104 | + | } | |||
| 105 | + | if i, _ := slices.BinarySearchFunc(p.refs, "refs/replace/", func(e packedRef, n string) int { | |||
| 106 | + | return strings.Compare(e.name, n) | |||
| 107 | + | }); i < len(p.refs) && strings.HasPrefix(p.refs[i].name, "refs/replace/") { | |||
| 108 | + | return nil, unsupported("replace refs (refs/replace in packed-refs)") | |||
| 109 | + | } | |||
| 110 | + | p.sig = sigOf(st) | |||
| 111 | + | r.packed.Store(p) | |||
| 112 | + | return p, nil | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // parsePackedRefs parses a packed-refs file. The ref names are substrings of | |||
| 116 | + | // data. | |||
| 117 | + | func parsePackedRefs(data string) (*packedRefs, error) { | |||
| 118 | + | p := &packedRefs{} | |||
| 119 | + | fullyPeeled, peeled := false, false | |||
| 120 | + | first := true | |||
| 121 | + | for len(data) > 0 { | |||
| 122 | + | var line string | |||
| 123 | + | line, data, _ = strings.Cut(data, "\n") | |||
| 124 | + | line = strings.TrimSuffix(line, "\r") | |||
| 125 | + | if line == "" { | |||
| 126 | + | continue | |||
| 127 | + | } | |||
| 128 | + | switch line[0] { | |||
| 129 | + | case '#': | |||
| 130 | + | if first && strings.HasPrefix(line, "# pack-refs with:") { | |||
| 131 | + | for _, tr := range strings.Fields(line[len("# pack-refs with:"):]) { | |||
| 132 | + | switch tr { | |||
| 133 | + | case "fully-peeled": | |||
| 134 | + | fullyPeeled = true | |||
| 135 | + | case "peeled": | |||
| 136 | + | peeled = true | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | first = false | |||
| 141 | + | continue | |||
| 142 | + | case '^': | |||
| 143 | + | if len(p.refs) == 0 { | |||
| 144 | + | return nil, corrupt("packed-refs: peeled line without a ref") | |||
| 145 | + | } | |||
| 146 | + | id, ok := parseIDBytes([]byte(line[1:])) | |||
| 147 | + | if !ok { | |||
| 148 | + | return nil, corrupt("packed-refs: bad peeled line") | |||
| 149 | + | } | |||
| 150 | + | last := &p.refs[len(p.refs)-1] | |||
| 151 | + | last.peeled, last.peeledKnown = id, true | |||
| 152 | + | continue | |||
| 153 | + | } | |||
| 154 | + | first = false | |||
| 155 | + | hex, name, ok := strings.Cut(line, " ") | |||
| 156 | + | if !ok || len(hex) != 40 || name == "" { | |||
| 157 | + | return nil, corrupt("packed-refs: bad line %q", truncate(line)) | |||
| 158 | + | } | |||
| 159 | + | id, ok := parseIDBytes([]byte(hex)) | |||
| 160 | + | if !ok { | |||
| 161 | + | return nil, corrupt("packed-refs: bad id in line %q", truncate(line)) | |||
| 162 | + | } | |||
| 163 | + | e := packedRef{name: name, id: id} | |||
| 164 | + | // Without a ^ line, a ref is known not to be a tag when the file | |||
| 165 | + | // is fully peeled (or, for the old "peeled" trait, outside tags). | |||
| 166 | + | if fullyPeeled || (peeled && strings.HasPrefix(name, "refs/tags/")) { | |||
| 167 | + | e.peeled, e.peeledKnown = id, true | |||
| 168 | + | } | |||
| 169 | + | p.refs = append(p.refs, e) | |||
| 170 | + | } | |||
| 171 | + | cmp := func(a, b packedRef) int { return strings.Compare(a.name, b.name) } | |||
| 172 | + | if !slices.IsSortedFunc(p.refs, cmp) { | |||
| 173 | + | slices.SortStableFunc(p.refs, cmp) | |||
| 174 | + | } | |||
| 175 | + | return p, nil | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // validRefName rejects names that could leave the repository directory or | |||
| 179 | + | // that Git itself refuses. It is a subset of git check-ref-format. | |||
| 180 | + | func validRefName(name string) bool { | |||
| 181 | + | if !strings.HasPrefix(name, "refs/") { | |||
| 182 | + | return false | |||
| 183 | + | } | |||
| 184 | + | if name == "" || len(name) > 1024 || name[0] == '/' || name[len(name)-1] == '/' || strings.HasSuffix(name, ".lock") { | |||
| 185 | + | return false | |||
| 186 | + | } | |||
| 187 | + | if strings.Contains(name, "..") || strings.Contains(name, "//") || strings.Contains(name, "@{") { | |||
| 188 | + | return false | |||
| 189 | + | } | |||
| 190 | + | for _, c := range []byte(name) { | |||
| 191 | + | if c < 0x20 || c == 0x7f || c == '\\' || c == ' ' || c == '~' || c == '^' || c == ':' || c == '?' || c == '*' || c == '[' { | |||
| 192 | + | return false | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | for comp := range strings.SplitSeq(name, "/") { | |||
| 196 | + | if comp == "" || comp[0] == '.' || strings.HasSuffix(comp, ".lock") { | |||
| 197 | + | return false | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | return true | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // looseRef is the content of a loose ref file. | |||
| 204 | + | type looseRef struct { | |||
| 205 | + | id ObjectID | |||
| 206 | + | symref string | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | // readLooseRef reads a loose ref. found is false when there is no such file | |||
| 210 | + | // (or it is a directory). | |||
| 211 | + | func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | |||
| 212 | + | f, err := os.Open(r.dir + "/" + name) | |||
| 213 | + | if err != nil { | |||
| 214 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |||
| 215 | + | return lr, false, nil | |||
| 216 | + | } | |||
| 217 | + | return lr, false, err | |||
| 218 | + | } | |||
| 219 | + | defer func() { _ = f.Close() }() | |||
| 220 | + | pb := getBuf() | |||
| 221 | + | defer putBuf(pb) | |||
| 222 | + | pb.b = grow(pb.b, maxRefFile+1) | |||
| 223 | + | buf := pb.b | |||
| 224 | + | n, err := io.ReadFull(f, buf) | |||
| 225 | + | if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { | |||
| 226 | + | err = nil | |||
| 227 | + | } | |||
| 228 | + | if err != nil { | |||
| 229 | + | var pe *fs.PathError | |||
| 230 | + | if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) { | |||
| 231 | + | return lr, false, nil | |||
| 232 | + | } | |||
| 233 | + | return lr, false, err | |||
| 234 | + | } | |||
| 235 | + | if n > maxRefFile { | |||
| 236 | + | return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile) | |||
| 237 | + | } | |||
| 238 | + | lr, err = parseRefContent(buf[:n]) | |||
| 239 | + | if err != nil { | |||
| 240 | + | return lr, true, fmt.Errorf("ref %s: %w", name, err) | |||
| 241 | + | } | |||
| 242 | + | return lr, true, nil | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | func parseRefContent(b []byte) (looseRef, error) { | |||
| 246 | + | s := strings.TrimRight(string(b), " \t\r\n") | |||
| 247 | + | if target, ok := strings.CutPrefix(s, "ref:"); ok { | |||
| 248 | + | target = strings.TrimSpace(target) | |||
| 249 | + | if !validRef(target) { | |||
| 250 | + | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target)) | |||
| 251 | + | } | |||
| 252 | + | return looseRef{symref: target}, nil | |||
| 253 | + | } | |||
| 254 | + | if len(s) >= 40 { | |||
| 255 | + | if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') { | |||
| 256 | + | return looseRef{id: id}, nil | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | return looseRef{}, corrupt("bad ref content %q", truncate(s)) | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // validRootRef reports whether name is a pseudo-ref of the Git directory such | |||
| 263 | + | // as HEAD or ORIG_HEAD: upper case letters and underscores only. | |||
| 264 | + | func validRootRef(name string) bool { | |||
| 265 | + | if name == "" || len(name) > 64 { | |||
| 266 | + | return false | |||
| 267 | + | } | |||
| 268 | + | for _, c := range []byte(name) { | |||
| 269 | + | if (c < 'A' || c > 'Z') && c != '_' { | |||
| 270 | + | return false | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | return true | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | // validRef accepts the names lookupRef may read. | |||
| 277 | + | func validRef(name string) bool { return validRefName(name) || validRootRef(name) } | |||
| 278 | + | ||||
| 279 | + | // lookupRef resolves name through symbolic refs. loose refs win over packed | |||
| 280 | + | // ones. The loose file is read before packed-refs, so a concurrent pack-refs | |||
| 281 | + | // (which writes packed-refs first and deletes the loose file after) is never | |||
| 282 | + | // missed. | |||
| 283 | + | func (r *Repo) lookupRef(name string) (Ref, bool, error) { | |||
| 284 | + | if !validRef(name) { | |||
| 285 | + | return Ref{}, false, nil | |||
| 286 | + | } | |||
| 287 | + | ref := Ref{Name: name} | |||
| 288 | + | cur := name | |||
| 289 | + | for depth := 0; ; depth++ { | |||
| 290 | + | if depth > maxSymrefDepth { | |||
| 291 | + | return Ref{}, false, corrupt("ref %s: symbolic ref loop", name) | |||
| 292 | + | } | |||
| 293 | + | lr, found, err := r.readLooseRef(cur) | |||
| 294 | + | if err != nil { | |||
| 295 | + | return Ref{}, false, err | |||
| 296 | + | } | |||
| 297 | + | if found && lr.symref != "" { | |||
| 298 | + | if depth == 0 { | |||
| 299 | + | ref.Symref = lr.symref | |||
| 300 | + | } | |||
| 301 | + | if !validRef(lr.symref) { | |||
| 302 | + | return Ref{}, false, nil | |||
| 303 | + | } | |||
| 304 | + | cur = lr.symref | |||
| 305 | + | continue | |||
| 306 | + | } | |||
| 307 | + | if found { | |||
| 308 | + | ref.ID = lr.id | |||
| 309 | + | return ref, true, nil | |||
| 310 | + | } | |||
| 311 | + | p, err := r.loadPacked(false) | |||
| 312 | + | if err != nil { | |||
| 313 | + | return Ref{}, false, err | |||
| 314 | + | } | |||
| 315 | + | e, ok := p.find(cur) | |||
| 316 | + | if !ok { | |||
| 317 | + | return Ref{}, false, nil | |||
| 318 | + | } | |||
| 319 | + | ref.ID = e.id | |||
| 320 | + | if depth == 0 { | |||
| 321 | + | ref.Peeled, ref.PeeledKnown = e.peeled, e.peeledKnown | |||
| 322 | + | } | |||
| 323 | + | return ref, true, nil | |||
| 324 | + | } | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | // ResolveRef returns the object the ref with this full name (such as | |||
| 328 | + | // "refs/heads/main", "refs/tags/v1" or "HEAD") designates, following | |||
| 329 | + | // symbolic refs. The result may be an annotated tag; see PeelToCommit. It | |||
| 330 | + | // returns ErrNotFound if the ref does not exist. | |||
| 331 | + | func (r *Repo) ResolveRef(name string) (ObjectID, error) { | |||
| 332 | + | ref, ok, err := r.lookupRef(name) | |||
| 333 | + | if err != nil { | |||
| 334 | + | return ObjectID{}, err | |||
| 335 | + | } | |||
| 336 | + | if !ok { | |||
| 337 | + | return ObjectID{}, notFound("ref %s", name) | |||
| 338 | + | } | |||
| 339 | + | return ref.ID, nil | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // LookupRef is like ResolveRef but returns the whole Ref. | |||
| 343 | + | func (r *Repo) LookupRef(name string) (Ref, error) { | |||
| 344 | + | ref, ok, err := r.lookupRef(name) | |||
| 345 | + | if err != nil { | |||
| 346 | + | return Ref{}, err | |||
| 347 | + | } | |||
| 348 | + | if !ok { | |||
| 349 | + | return Ref{}, notFound("ref %s", name) | |||
| 350 | + | } | |||
| 351 | + | return ref, nil | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | // Head returns the full name of the ref HEAD points to ("refs/heads/main") | |||
| 355 | + | // and the object it designates. An unborn branch (the target does not exist | |||
| 356 | + | // yet) is not an error: id is then the zero ObjectID. For a detached HEAD | |||
| 357 | + | // target is empty and id is the commit. | |||
| 358 | + | func (r *Repo) Head() (target string, id ObjectID, err error) { | |||
| 359 | + | lr, found, err := r.readLooseRef("HEAD") | |||
| 360 | + | if err != nil { | |||
| 361 | + | return "", ObjectID{}, err | |||
| 362 | + | } | |||
| 363 | + | if !found { | |||
| 364 | + | return "", ObjectID{}, notFound("HEAD") | |||
| 365 | + | } | |||
| 366 | + | if lr.symref == "" { | |||
| 367 | + | return "", lr.id, nil | |||
| 368 | + | } | |||
| 369 | + | ref, ok, err := r.lookupRef(lr.symref) | |||
| 370 | + | if err != nil { | |||
| 371 | + | return "", ObjectID{}, err | |||
| 372 | + | } | |||
| 373 | + | if !ok { | |||
| 374 | + | return lr.symref, ObjectID{}, nil | |||
| 375 | + | } | |||
| 376 | + | return lr.symref, ref.ID, nil | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // Refs iterates over the refs whose full name starts with prefix, such as | |||
| 380 | + | // "refs/heads/" or "refs/tags/" ("" for all of them), sorted by full name. | |||
| 381 | + | // Loose refs override packed ones; the peeled lines of packed-refs are used. | |||
| 382 | + | // Symbolic refs are resolved; a ref that cannot be read is skipped. HEAD is | |||
| 383 | + | // not part of it, see Head. An error ends the iteration. | |||
| 384 | + | func (r *Repo) Refs(prefix string) iter.Seq2[Ref, error] { | |||
| 385 | + | return func(yield func(Ref, error) bool) { | |||
| 386 | + | refs, err := r.collectRefs(prefix) | |||
| 387 | + | if err != nil { | |||
| 388 | + | yield(Ref{}, err) | |||
| 389 | + | return | |||
| 390 | + | } | |||
| 391 | + | for _, ref := range refs { | |||
| 392 | + | if !yield(ref, nil) { | |||
| 393 | + | return | |||
| 394 | + | } | |||
| 395 | + | } | |||
| 396 | + | } | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | func (r *Repo) collectRefs(prefix string) ([]Ref, error) { | |||
| 400 | + | // Loose refs first, packed-refs after: see lookupRef. | |||
| 401 | + | var dir string | |||
| 402 | + | switch { | |||
| 403 | + | case strings.HasPrefix(prefix, "refs/"): | |||
| 404 | + | dir = prefix[:strings.LastIndexByte(prefix, '/')] | |||
| 405 | + | case strings.HasPrefix("refs/", prefix): | |||
| 406 | + | dir = "refs" | |||
| 407 | + | default: | |||
| 408 | + | return nil, nil | |||
| 409 | + | } | |||
| 410 | + | var loose []string | |||
| 411 | + | if err := r.walkLoose(dir, prefix, &loose, 0); err != nil { | |||
| 412 | + | return nil, err | |||
| 413 | + | } | |||
| 414 | + | p, err := r.loadPacked(false) | |||
| 415 | + | if err != nil { | |||
| 416 | + | return nil, err | |||
| 417 | + | } | |||
| 418 | + | names := loose | |||
| 419 | + | start, _ := slices.BinarySearchFunc(p.refs, prefix, func(e packedRef, pre string) int { | |||
| 420 | + | if strings.HasPrefix(e.name, pre) { | |||
| 421 | + | return 0 | |||
| 422 | + | } | |||
| 423 | + | return strings.Compare(e.name, pre) | |||
| 424 | + | }) | |||
| 425 | + | // BinarySearchFunc with a prefix comparator may land inside the range: | |||
| 426 | + | // walk back to its first element. | |||
| 427 | + | for start > 0 && strings.HasPrefix(p.refs[start-1].name, prefix) { | |||
| 428 | + | start-- | |||
| 429 | + | } | |||
| 430 | + | for i := start; i < len(p.refs) && strings.HasPrefix(p.refs[i].name, prefix); i++ { | |||
| 431 | + | names = append(names, p.refs[i].name) | |||
| 432 | + | } | |||
| 433 | + | slices.Sort(names) | |||
| 434 | + | names = slices.Compact(names) | |||
| 435 | + | ||||
| 436 | + | out := make([]Ref, 0, len(names)) | |||
| 437 | + | for _, n := range names { | |||
| 438 | + | ref, ok, err := r.lookupRef(n) | |||
| 439 | + | if err != nil { | |||
| 440 | + | if errors.Is(err, ErrCorrupt) { | |||
| 441 | + | continue // Git ignores broken refs when listing | |||
| 442 | + | } | |||
| 443 | + | return nil, err | |||
| 444 | + | } | |||
| 445 | + | if ok { | |||
| 446 | + | ref.Name = n | |||
| 447 | + | out = append(out, ref) | |||
| 448 | + | } | |||
| 449 | + | } | |||
| 450 | + | return out, nil | |||
| 451 | + | } | |||
| 452 | + | ||||
| 453 | + | // maxRefDirDepth bounds the recursion into refs directories. | |||
| 454 | + | const maxRefDirDepth = 64 | |||
| 455 | + | ||||
| 456 | + | func (r *Repo) walkLoose(dir, prefix string, out *[]string, depth int) error { | |||
| 457 | + | if depth > maxRefDirDepth { | |||
| 458 | + | return nil | |||
| 459 | + | } | |||
| 460 | + | entries, err := os.ReadDir(r.dir + "/" + dir) | |||
| 461 | + | if err != nil { | |||
| 462 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |||
| 463 | + | return nil | |||
| 464 | + | } | |||
| 465 | + | return err | |||
| 466 | + | } | |||
| 467 | + | for _, e := range entries { | |||
| 468 | + | name := dir + "/" + e.Name() | |||
| 469 | + | if e.IsDir() { | |||
| 470 | + | // Descend only where the prefix can match. | |||
| 471 | + | if strings.HasPrefix(name+"/", prefix) || strings.HasPrefix(prefix, name+"/") { | |||
| 472 | + | if err := r.walkLoose(name, prefix, out, depth+1); err != nil { | |||
| 473 | + | return err | |||
| 474 | + | } | |||
| 475 | + | } | |||
| 476 | + | continue | |||
| 477 | + | } | |||
| 478 | + | if strings.HasPrefix(name, prefix) && validRefName(name) { | |||
| 479 | + | *out = append(*out, name) | |||
| 480 | + | } | |||
| 481 | + | } | |||
| 482 | + | return nil | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // PeelRef returns the commit a ref designates, following annotated tags. It | |||
| 486 | + | // uses the peeled value of packed-refs when the ref carries one. It returns | |||
| 487 | + | // ErrWrongType if the ref designates something else than a commit. | |||
| 488 | + | func (r *Repo) PeelRef(ref Ref) (ObjectID, error) { | |||
| 489 | + | if ref.PeeledKnown { | |||
| 490 | + | typ, _, err := r.ObjectHeader(ref.Peeled) | |||
| 491 | + | if err != nil { | |||
| 492 | + | return ObjectID{}, err | |||
| 493 | + | } | |||
| 494 | + | if typ != TypeCommit { | |||
| 495 | + | return ObjectID{}, wrongType(ref.Peeled, typ, TypeCommit) | |||
| 496 | + | } | |||
| 497 | + | return ref.Peeled, nil | |||
| 498 | + | } | |||
| 499 | + | return r.PeelToCommit(ref.ID) | |||
| 500 | + | } | |||
| 501 | + | ||||
| 502 | + | // Resolve turns a revision name into an object id the way "git rev-parse" | |||
| 503 | + | // does for plain names: a full 40 digit id, "HEAD", a full ref name, a short | |||
| 504 | + | // ref name tried as "<n>", "refs/<n>", "refs/tags/<n>", "refs/heads/<n>", | |||
| 505 | + | // "refs/remotes/<n>" and "refs/remotes/<n>/HEAD" in that order, and finally | |||
| 506 | + | // an abbreviated id of 4 to 39 digits. Revision operators (~, ^, @{}) are not | |||
| 507 | + | // supported. The result is not peeled; use PeelToCommit. | |||
| 508 | + | func (r *Repo) Resolve(rev string) (ObjectID, error) { | |||
| 509 | + | if rev == "" { | |||
| 510 | + | return ObjectID{}, notFound("empty revision") | |||
| 511 | + | } | |||
| 512 | + | if len(rev) == 40 { | |||
| 513 | + | if id, err := ParseID(rev); err == nil { | |||
| 514 | + | if !r.HasObject(id) { | |||
| 515 | + | return ObjectID{}, notFound("object %s", id) | |||
| 516 | + | } | |||
| 517 | + | return id, nil | |||
| 518 | + | } | |||
| 519 | + | } | |||
| 520 | + | for _, pat := range [...]string{"%s", "refs/%s", "refs/tags/%s", "refs/heads/%s", "refs/remotes/%s", "refs/remotes/%s/HEAD"} { | |||
| 521 | + | ref, ok, err := r.lookupRef(fmt.Sprintf(pat, rev)) | |||
| 522 | + | if err != nil { | |||
| 523 | + | return ObjectID{}, err | |||
| 524 | + | } | |||
| 525 | + | if ok { | |||
| 526 | + | return ref.ID, nil | |||
| 527 | + | } | |||
| 528 | + | } | |||
| 529 | + | if len(rev) >= minPrefix && len(rev) < 40 { | |||
| 530 | + | id, err := r.ResolvePrefix(rev) | |||
| 531 | + | if err == nil || !errors.Is(err, ErrInvalidID) { | |||
| 532 | + | return id, err | |||
| 533 | + | } | |||
| 534 | + | } | |||
| 535 | + | return ObjectID{}, notFound("revision %q", truncate(rev)) | |||
| 536 | + | } | |||
A repo.go +204 -0
| @@ -0,0 +1,204 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "io/fs" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "sync" | |
| 11 | + | "sync/atomic" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // Default bounds. See WithMaxObjectSize and WithDeltaCacheSize. | |
| 15 | + | const ( | |
| 16 | + | defaultMaxObjectSize = 128 << 20 | |
| 17 | + | defaultDeltaCacheSize = 32 << 20 | |
| 18 | + | // maxDeltaDepth bounds the length of a delta chain. Git itself writes | |
| 19 | + | // chains of at most 4095 and 50 by default. | |
| 20 | + | maxDeltaDepth = 10000 | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | type options struct { | |
| 24 | + | maxObject int64 | |
| 25 | + | cacheBytes int64 | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Option configures Open. | |
| 29 | + | type Option func(*options) | |
| 30 | + | ||
| 31 | + | // WithMaxObjectSize bounds the size of an object that is held in memory: | |
| 32 | + | // commits, trees, tags and blobs that are stored as deltas. Bigger ones fail | |
| 33 | + | // with ErrTooLarge. Undeltified blobs stream and are not bounded. The default | |
| 34 | + | // is 128 MiB. | |
| 35 | + | func WithMaxObjectSize(n int64) Option { | |
| 36 | + | return func(o *options) { | |
| 37 | + | if n > 0 { | |
| 38 | + | o.maxObject = n | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // WithDeltaCacheSize sets the budget in bytes of the cache of delta bases | |
| 44 | + | // (LRU). The default is 32 MiB. Zero or less disables the cache. | |
| 45 | + | func WithDeltaCacheSize(n int64) Option { | |
| 46 | + | return func(o *options) { o.cacheBytes = n } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Repo is an open Git repository. It is read-only and safe for concurrent | |
| 50 | + | // use by multiple goroutines. | |
| 51 | + | // | |
| 52 | + | // Loose refs and loose objects are always read live. The list of packs and | |
| 53 | + | // the content of packed-refs are loaded snapshots: a missing object triggers | |
| 54 | + | // one rescan of the pack directory, packed-refs is stat-checked on each ref | |
| 55 | + | // read, and Refresh reloads everything explicitly. | |
| 56 | + | type Repo struct { | |
| 57 | + | dir string | |
| 58 | + | opts options | |
| 59 | + | ||
| 60 | + | snap atomic.Pointer[snapshot] | |
| 61 | + | mu sync.Mutex // serializes pack list reloads and Close | |
| 62 | + | closed bool | |
| 63 | + | nextPack atomic.Uint32 | |
| 64 | + | ||
| 65 | + | cache *deltaCache | |
| 66 | + | ||
| 67 | + | packed atomic.Pointer[packedRefs] | |
| 68 | + | pmu sync.Mutex // serializes packed-refs reloads | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // Open opens the repository whose Git directory is dir: a bare repository | |
| 72 | + | // or a ".git" directory. If dir contains a ".git" directory, that one is | |
| 73 | + | // used. | |
| 74 | + | // | |
| 75 | + | // Open returns an error wrapping ErrNotFound when dir is not a repository | |
| 76 | + | // and one wrapping ErrUnsupported (with the reason) when the repository uses | |
| 77 | + | // a feature the package does not read: an object format other than sha1, a | |
| 78 | + | // ref storage other than files, other extensions, a shallow file, | |
| 79 | + | // info/grafts, replace refs, alternates, a linked worktree, a partial clone, | |
| 80 | + | // or a multi-pack-index whose packs lack their .idx file. A multi-pack-index | |
| 81 | + | // is otherwise ignored: the packs are read through their own .idx files. | |
| 82 | + | func Open(dir string, opts ...Option) (*Repo, error) { | |
| 83 | + | gitDir, err := findGitDir(dir) | |
| 84 | + | if err != nil { | |
| 85 | + | return nil, err | |
| 86 | + | } | |
| 87 | + | o := options{maxObject: defaultMaxObjectSize, cacheBytes: defaultDeltaCacheSize} | |
| 88 | + | for _, fn := range opts { | |
| 89 | + | fn(&o) | |
| 90 | + | } | |
| 91 | + | if err := checkConfig(filepath.Join(gitDir, "config")); err != nil { | |
| 92 | + | return nil, err | |
| 93 | + | } | |
| 94 | + | if err := checkLayout(gitDir); err != nil { | |
| 95 | + | return nil, err | |
| 96 | + | } | |
| 97 | + | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)} | |
| 98 | + | r.snap.Store(newSnapshot(nil)) | |
| 99 | + | if _, err := r.reloadPacks(true); err != nil { | |
| 100 | + | _ = r.Close() | |
| 101 | + | return nil, err | |
| 102 | + | } | |
| 103 | + | if _, err := r.loadPacked(true); err != nil { | |
| 104 | + | _ = r.Close() | |
| 105 | + | return nil, err | |
| 106 | + | } | |
| 107 | + | return r, nil | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // Dir returns the Git directory that was opened. | |
| 111 | + | func (r *Repo) Dir() string { return r.dir } | |
| 112 | + | ||
| 113 | + | // Close releases the repository. Readers still streaming a blob keep their | |
| 114 | + | // packs mapped until they are closed. Close is idempotent. | |
| 115 | + | func (r *Repo) Close() error { | |
| 116 | + | r.mu.Lock() | |
| 117 | + | defer r.mu.Unlock() | |
| 118 | + | if r.closed { | |
| 119 | + | return nil | |
| 120 | + | } | |
| 121 | + | r.closed = true | |
| 122 | + | if s := r.snap.Swap(nil); s != nil { | |
| 123 | + | s.release() | |
| 124 | + | } | |
| 125 | + | return nil | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Refresh reloads the list of packs and packed-refs when they changed, and | |
| 129 | + | // checks again that the repository is still supported (it returns an error | |
| 130 | + | // wrapping ErrUnsupported when, for example, a replace ref was pushed). Packs | |
| 131 | + | // that disappeared stay mapped until the readers using them are done, so a | |
| 132 | + | // concurrent "git repack -ad" never breaks a read in flight. | |
| 133 | + | func (r *Repo) Refresh() error { | |
| 134 | + | if err := checkLayout(r.dir); err != nil { | |
| 135 | + | return err | |
| 136 | + | } | |
| 137 | + | if _, err := r.reloadPacks(false); err != nil { | |
| 138 | + | return err | |
| 139 | + | } | |
| 140 | + | _, err := r.loadPacked(false) | |
| 141 | + | return err | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func findGitDir(dir string) (string, error) { | |
| 145 | + | for range 2 { | |
| 146 | + | if isGitDir(dir) { | |
| 147 | + | return dir, nil | |
| 148 | + | } | |
| 149 | + | sub := filepath.Join(dir, ".git") | |
| 150 | + | fi, err := os.Stat(sub) | |
| 151 | + | switch { | |
| 152 | + | case err != nil: | |
| 153 | + | return "", notFound("%s is not a git repository", dir) | |
| 154 | + | case fi.IsDir(): | |
| 155 | + | dir = sub | |
| 156 | + | default: | |
| 157 | + | return "", unsupported("%s is a gitfile (linked worktree or submodule)", sub) | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | return "", notFound("%s is not a git repository", dir) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func isGitDir(dir string) bool { | |
| 164 | + | for _, n := range []string{"HEAD", "objects", "refs"} { | |
| 165 | + | if _, err := os.Stat(filepath.Join(dir, n)); err != nil { | |
| 166 | + | return false | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | return true | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func exists(path string) bool { | |
| 173 | + | _, err := os.Lstat(path) | |
| 174 | + | return err == nil | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // checkLayout rejects the repository layouts that change what objects or | |
| 178 | + | // refs mean and that the package does not implement. | |
| 179 | + | func checkLayout(dir string) error { | |
| 180 | + | for _, c := range []struct{ path, why string }{ | |
| 181 | + | {"shallow", "shallow repository"}, | |
| 182 | + | {"info/grafts", "grafts file"}, | |
| 183 | + | {"commondir", "linked worktree"}, | |
| 184 | + | {"objects/info/alternates", "alternate object stores"}, | |
| 185 | + | } { | |
| 186 | + | if exists(filepath.Join(dir, c.path)) { | |
| 187 | + | return unsupported("%s (%s)", c.why, c.path) | |
| 188 | + | } | |
| 189 | + | } | |
| 190 | + | f, err := os.Open(filepath.Join(dir, "refs", "replace")) | |
| 191 | + | if err == nil { | |
| 192 | + | names, rerr := f.Readdirnames(1) | |
| 193 | + | _ = f.Close() | |
| 194 | + | if rerr == nil && len(names) > 0 { | |
| 195 | + | return unsupported("replace refs (refs/replace)") | |
| 196 | + | } | |
| 197 | + | } else if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) { | |
| 198 | + | var pe *fs.PathError | |
| 199 | + | if !errors.As(err, &pe) { | |
| 200 | + | return err | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | return nil | |
| 204 | + | } | |
| @@ -0,0 +1,204 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "io/fs" | |||
| 8 | + | "os" | |||
| 9 | + | "path/filepath" | |||
| 10 | + | "sync" | |||
| 11 | + | "sync/atomic" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // Default bounds. See WithMaxObjectSize and WithDeltaCacheSize. | |||
| 15 | + | const ( | |||
| 16 | + | defaultMaxObjectSize = 128 << 20 | |||
| 17 | + | defaultDeltaCacheSize = 32 << 20 | |||
| 18 | + | // maxDeltaDepth bounds the length of a delta chain. Git itself writes | |||
| 19 | + | // chains of at most 4095 and 50 by default. | |||
| 20 | + | maxDeltaDepth = 10000 | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | type options struct { | |||
| 24 | + | maxObject int64 | |||
| 25 | + | cacheBytes int64 | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Option configures Open. | |||
| 29 | + | type Option func(*options) | |||
| 30 | + | ||||
| 31 | + | // WithMaxObjectSize bounds the size of an object that is held in memory: | |||
| 32 | + | // commits, trees, tags and blobs that are stored as deltas. Bigger ones fail | |||
| 33 | + | // with ErrTooLarge. Undeltified blobs stream and are not bounded. The default | |||
| 34 | + | // is 128 MiB. | |||
| 35 | + | func WithMaxObjectSize(n int64) Option { | |||
| 36 | + | return func(o *options) { | |||
| 37 | + | if n > 0 { | |||
| 38 | + | o.maxObject = n | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // WithDeltaCacheSize sets the budget in bytes of the cache of delta bases | |||
| 44 | + | // (LRU). The default is 32 MiB. Zero or less disables the cache. | |||
| 45 | + | func WithDeltaCacheSize(n int64) Option { | |||
| 46 | + | return func(o *options) { o.cacheBytes = n } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // Repo is an open Git repository. It is read-only and safe for concurrent | |||
| 50 | + | // use by multiple goroutines. | |||
| 51 | + | // | |||
| 52 | + | // Loose refs and loose objects are always read live. The list of packs and | |||
| 53 | + | // the content of packed-refs are loaded snapshots: a missing object triggers | |||
| 54 | + | // one rescan of the pack directory, packed-refs is stat-checked on each ref | |||
| 55 | + | // read, and Refresh reloads everything explicitly. | |||
| 56 | + | type Repo struct { | |||
| 57 | + | dir string | |||
| 58 | + | opts options | |||
| 59 | + | ||||
| 60 | + | snap atomic.Pointer[snapshot] | |||
| 61 | + | mu sync.Mutex // serializes pack list reloads and Close | |||
| 62 | + | closed bool | |||
| 63 | + | nextPack atomic.Uint32 | |||
| 64 | + | ||||
| 65 | + | cache *deltaCache | |||
| 66 | + | ||||
| 67 | + | packed atomic.Pointer[packedRefs] | |||
| 68 | + | pmu sync.Mutex // serializes packed-refs reloads | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // Open opens the repository whose Git directory is dir: a bare repository | |||
| 72 | + | // or a ".git" directory. If dir contains a ".git" directory, that one is | |||
| 73 | + | // used. | |||
| 74 | + | // | |||
| 75 | + | // Open returns an error wrapping ErrNotFound when dir is not a repository | |||
| 76 | + | // and one wrapping ErrUnsupported (with the reason) when the repository uses | |||
| 77 | + | // a feature the package does not read: an object format other than sha1, a | |||
| 78 | + | // ref storage other than files, other extensions, a shallow file, | |||
| 79 | + | // info/grafts, replace refs, alternates, a linked worktree, a partial clone, | |||
| 80 | + | // or a multi-pack-index whose packs lack their .idx file. A multi-pack-index | |||
| 81 | + | // is otherwise ignored: the packs are read through their own .idx files. | |||
| 82 | + | func Open(dir string, opts ...Option) (*Repo, error) { | |||
| 83 | + | gitDir, err := findGitDir(dir) | |||
| 84 | + | if err != nil { | |||
| 85 | + | return nil, err | |||
| 86 | + | } | |||
| 87 | + | o := options{maxObject: defaultMaxObjectSize, cacheBytes: defaultDeltaCacheSize} | |||
| 88 | + | for _, fn := range opts { | |||
| 89 | + | fn(&o) | |||
| 90 | + | } | |||
| 91 | + | if err := checkConfig(filepath.Join(gitDir, "config")); err != nil { | |||
| 92 | + | return nil, err | |||
| 93 | + | } | |||
| 94 | + | if err := checkLayout(gitDir); err != nil { | |||
| 95 | + | return nil, err | |||
| 96 | + | } | |||
| 97 | + | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)} | |||
| 98 | + | r.snap.Store(newSnapshot(nil)) | |||
| 99 | + | if _, err := r.reloadPacks(true); err != nil { | |||
| 100 | + | _ = r.Close() | |||
| 101 | + | return nil, err | |||
| 102 | + | } | |||
| 103 | + | if _, err := r.loadPacked(true); err != nil { | |||
| 104 | + | _ = r.Close() | |||
| 105 | + | return nil, err | |||
| 106 | + | } | |||
| 107 | + | return r, nil | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // Dir returns the Git directory that was opened. | |||
| 111 | + | func (r *Repo) Dir() string { return r.dir } | |||
| 112 | + | ||||
| 113 | + | // Close releases the repository. Readers still streaming a blob keep their | |||
| 114 | + | // packs mapped until they are closed. Close is idempotent. | |||
| 115 | + | func (r *Repo) Close() error { | |||
| 116 | + | r.mu.Lock() | |||
| 117 | + | defer r.mu.Unlock() | |||
| 118 | + | if r.closed { | |||
| 119 | + | return nil | |||
| 120 | + | } | |||
| 121 | + | r.closed = true | |||
| 122 | + | if s := r.snap.Swap(nil); s != nil { | |||
| 123 | + | s.release() | |||
| 124 | + | } | |||
| 125 | + | return nil | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // Refresh reloads the list of packs and packed-refs when they changed, and | |||
| 129 | + | // checks again that the repository is still supported (it returns an error | |||
| 130 | + | // wrapping ErrUnsupported when, for example, a replace ref was pushed). Packs | |||
| 131 | + | // that disappeared stay mapped until the readers using them are done, so a | |||
| 132 | + | // concurrent "git repack -ad" never breaks a read in flight. | |||
| 133 | + | func (r *Repo) Refresh() error { | |||
| 134 | + | if err := checkLayout(r.dir); err != nil { | |||
| 135 | + | return err | |||
| 136 | + | } | |||
| 137 | + | if _, err := r.reloadPacks(false); err != nil { | |||
| 138 | + | return err | |||
| 139 | + | } | |||
| 140 | + | _, err := r.loadPacked(false) | |||
| 141 | + | return err | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func findGitDir(dir string) (string, error) { | |||
| 145 | + | for range 2 { | |||
| 146 | + | if isGitDir(dir) { | |||
| 147 | + | return dir, nil | |||
| 148 | + | } | |||
| 149 | + | sub := filepath.Join(dir, ".git") | |||
| 150 | + | fi, err := os.Stat(sub) | |||
| 151 | + | switch { | |||
| 152 | + | case err != nil: | |||
| 153 | + | return "", notFound("%s is not a git repository", dir) | |||
| 154 | + | case fi.IsDir(): | |||
| 155 | + | dir = sub | |||
| 156 | + | default: | |||
| 157 | + | return "", unsupported("%s is a gitfile (linked worktree or submodule)", sub) | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | return "", notFound("%s is not a git repository", dir) | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func isGitDir(dir string) bool { | |||
| 164 | + | for _, n := range []string{"HEAD", "objects", "refs"} { | |||
| 165 | + | if _, err := os.Stat(filepath.Join(dir, n)); err != nil { | |||
| 166 | + | return false | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | return true | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func exists(path string) bool { | |||
| 173 | + | _, err := os.Lstat(path) | |||
| 174 | + | return err == nil | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // checkLayout rejects the repository layouts that change what objects or | |||
| 178 | + | // refs mean and that the package does not implement. | |||
| 179 | + | func checkLayout(dir string) error { | |||
| 180 | + | for _, c := range []struct{ path, why string }{ | |||
| 181 | + | {"shallow", "shallow repository"}, | |||
| 182 | + | {"info/grafts", "grafts file"}, | |||
| 183 | + | {"commondir", "linked worktree"}, | |||
| 184 | + | {"objects/info/alternates", "alternate object stores"}, | |||
| 185 | + | } { | |||
| 186 | + | if exists(filepath.Join(dir, c.path)) { | |||
| 187 | + | return unsupported("%s (%s)", c.why, c.path) | |||
| 188 | + | } | |||
| 189 | + | } | |||
| 190 | + | f, err := os.Open(filepath.Join(dir, "refs", "replace")) | |||
| 191 | + | if err == nil { | |||
| 192 | + | names, rerr := f.Readdirnames(1) | |||
| 193 | + | _ = f.Close() | |||
| 194 | + | if rerr == nil && len(names) > 0 { | |||
| 195 | + | return unsupported("replace refs (refs/replace)") | |||
| 196 | + | } | |||
| 197 | + | } else if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) { | |||
| 198 | + | var pe *fs.PathError | |||
| 199 | + | if !errors.As(err, &pe) { | |||
| 200 | + | return err | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | return nil | |||
| 204 | + | } | |||
A snapshot.go +287 -0
| @@ -0,0 +1,287 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/binary" | |
| 7 | + | "errors" | |
| 8 | + | "io/fs" | |
| 9 | + | "os" | |
| 10 | + | "slices" | |
| 11 | + | "strings" | |
| 12 | + | "sync/atomic" | |
| 13 | + | "time" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // fileSig identifies a version of a file or directory by stat data. | |
| 17 | + | type fileSig struct { | |
| 18 | + | exists bool | |
| 19 | + | size int64 | |
| 20 | + | mtime int64 // unix nanoseconds | |
| 21 | + | ino uint64 | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func sigOf(fi os.FileInfo) fileSig { | |
| 25 | + | return fileSig{exists: true, size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func statSig(path string) (fileSig, error) { | |
| 29 | + | fi, err := os.Stat(path) | |
| 30 | + | if errors.Is(err, fs.ErrNotExist) { | |
| 31 | + | return fileSig{}, nil | |
| 32 | + | } | |
| 33 | + | if err != nil { | |
| 34 | + | return fileSig{}, err | |
| 35 | + | } | |
| 36 | + | return sigOf(fi), nil | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // racyWindow is how long after its modification time a file may still change | |
| 40 | + | // without its stat data changing, because of coarse file system clocks. | |
| 41 | + | const racyWindow = 2 * time.Second | |
| 42 | + | ||
| 43 | + | // same reports whether b is the same version as a and can be trusted to be | |
| 44 | + | // unchanged. | |
| 45 | + | func (a fileSig) same(b fileSig) bool { | |
| 46 | + | if a != b { | |
| 47 | + | return false | |
| 48 | + | } | |
| 49 | + | return !a.exists || time.Since(time.Unix(0, a.mtime)) > racyWindow | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func ignoreMissing(err error) error { | |
| 53 | + | if errors.Is(err, fs.ErrNotExist) { | |
| 54 | + | return nil | |
| 55 | + | } | |
| 56 | + | return err | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // snapshot is an immutable list of packs, shared by readers. It is released | |
| 60 | + | // by each reader and by the Repo; the last release drops its packs. | |
| 61 | + | type snapshot struct { | |
| 62 | + | refs atomic.Int32 | |
| 63 | + | packs []*pack // newest first | |
| 64 | + | dirSig fileSig // of objects/pack when the list was built | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func newSnapshot(packs []*pack) *snapshot { | |
| 68 | + | s := &snapshot{packs: packs} | |
| 69 | + | s.refs.Store(1) | |
| 70 | + | return s | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func (s *snapshot) release() { | |
| 74 | + | if s.refs.Add(-1) == 0 { | |
| 75 | + | for _, p := range s.packs { | |
| 76 | + | p.release() | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // find looks id up in the packs. | |
| 82 | + | func (s *snapshot) find(id *ObjectID) (*pack, uint64, error) { | |
| 83 | + | for _, p := range s.packs { | |
| 84 | + | if pos, ok := p.idx.find(id); ok { | |
| 85 | + | off, err := p.idx.offset(pos) | |
| 86 | + | if err != nil { | |
| 87 | + | return nil, 0, wrapPack(p.name, err) | |
| 88 | + | } | |
| 89 | + | return p, off, nil | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | return nil, 0, nil | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // acquire returns the current snapshot, which the caller must release. | |
| 96 | + | func (r *Repo) acquire() (*snapshot, error) { | |
| 97 | + | for { | |
| 98 | + | s := r.snap.Load() | |
| 99 | + | if s == nil { | |
| 100 | + | return nil, ErrClosed | |
| 101 | + | } | |
| 102 | + | n := s.refs.Load() | |
| 103 | + | if n > 0 && s.refs.CompareAndSwap(n, n+1) { | |
| 104 | + | return s, nil | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (r *Repo) packDir() string { return r.dir + "/objects/pack" } | |
| 110 | + | ||
| 111 | + | // reloadPacks rescans objects/pack and installs a new snapshot when the | |
| 112 | + | // directory changed, or always with force. It reports whether the list of | |
| 113 | + | // packs changed. | |
| 114 | + | func (r *Repo) reloadPacks(force bool) (bool, error) { | |
| 115 | + | r.mu.Lock() | |
| 116 | + | defer r.mu.Unlock() | |
| 117 | + | if r.closed { | |
| 118 | + | return false, ErrClosed | |
| 119 | + | } | |
| 120 | + | dir := r.packDir() | |
| 121 | + | cur := r.snap.Load() | |
| 122 | + | dsig, err := statSig(dir) | |
| 123 | + | if err != nil { | |
| 124 | + | return false, err | |
| 125 | + | } | |
| 126 | + | if !force && cur.dirSig.same(dsig) { | |
| 127 | + | return false, nil | |
| 128 | + | } | |
| 129 | + | entries, err := os.ReadDir(dir) | |
| 130 | + | if err != nil && !errors.Is(err, fs.ErrNotExist) { | |
| 131 | + | return false, err | |
| 132 | + | } | |
| 133 | + | var idxNames []string | |
| 134 | + | hasMidx := false | |
| 135 | + | for _, e := range entries { | |
| 136 | + | n := e.Name() | |
| 137 | + | switch { | |
| 138 | + | case n == "multi-pack-index": | |
| 139 | + | hasMidx = true | |
| 140 | + | case n == "multi-pack-index.d": | |
| 141 | + | return false, unsupported("incremental multi-pack-index (objects/pack/multi-pack-index.d)") | |
| 142 | + | case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".promisor"): | |
| 143 | + | return false, unsupported("partial clone (%s)", n) | |
| 144 | + | case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".idx"): | |
| 145 | + | idxNames = append(idxNames, strings.TrimSuffix(n, ".idx")) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | if hasMidx { | |
| 149 | + | if err := checkMidx(dir+"/multi-pack-index", idxNames); err != nil { | |
| 150 | + | return false, err | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | old := make(map[string]*pack, len(cur.packs)) | |
| 155 | + | for _, p := range cur.packs { | |
| 156 | + | old[p.name] = p | |
| 157 | + | } | |
| 158 | + | var packs []*pack | |
| 159 | + | changed := len(idxNames) != len(cur.packs) | |
| 160 | + | for _, name := range idxNames { | |
| 161 | + | isig, err := statSig(dir + "/" + name + ".idx") | |
| 162 | + | if err != nil { | |
| 163 | + | return false, err | |
| 164 | + | } | |
| 165 | + | if p := old[name]; p != nil && p.sig == isig { | |
| 166 | + | p.retain() | |
| 167 | + | packs = append(packs, p) | |
| 168 | + | continue | |
| 169 | + | } | |
| 170 | + | p, err := openPack(dir, name, r.nextPack.Add(1), isig) | |
| 171 | + | if err != nil { | |
| 172 | + | for _, q := range packs { | |
| 173 | + | q.release() | |
| 174 | + | } | |
| 175 | + | return false, err | |
| 176 | + | } | |
| 177 | + | if p == nil { // replaced while we looked | |
| 178 | + | changed = true | |
| 179 | + | continue | |
| 180 | + | } | |
| 181 | + | changed = true | |
| 182 | + | packs = append(packs, p) | |
| 183 | + | } | |
| 184 | + | if len(packs) != len(cur.packs) { | |
| 185 | + | changed = true | |
| 186 | + | } | |
| 187 | + | slices.SortStableFunc(packs, func(a, b *pack) int { | |
| 188 | + | switch { | |
| 189 | + | case a.sig.mtime > b.sig.mtime: | |
| 190 | + | return -1 | |
| 191 | + | case a.sig.mtime < b.sig.mtime: | |
| 192 | + | return 1 | |
| 193 | + | } | |
| 194 | + | return strings.Compare(a.name, b.name) | |
| 195 | + | }) | |
| 196 | + | if !changed { | |
| 197 | + | // Same packs: keep the current snapshot (and its readers) unless the | |
| 198 | + | // directory stat data moved on. | |
| 199 | + | for _, p := range packs { | |
| 200 | + | p.release() | |
| 201 | + | } | |
| 202 | + | if cur.dirSig == dsig { | |
| 203 | + | return false, nil | |
| 204 | + | } | |
| 205 | + | packs = cur.packs | |
| 206 | + | for _, p := range packs { | |
| 207 | + | p.retain() | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | ns := newSnapshot(packs) | |
| 211 | + | ns.dirSig = dsig | |
| 212 | + | r.snap.Store(ns) | |
| 213 | + | cur.release() | |
| 214 | + | return changed, nil | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | const ( | |
| 218 | + | midxHeaderLen = 12 | |
| 219 | + | midxChunkPNAM = 0x504e414d | |
| 220 | + | maxMidxNames = 4 << 20 | |
| 221 | + | ) | |
| 222 | + | ||
| 223 | + | // checkMidx accepts a multi-pack-index only when every pack it lists has | |
| 224 | + | // its own .idx file: the midx is then redundant and ignored. | |
| 225 | + | func checkMidx(path string, idxNames []string) error { | |
| 226 | + | f, err := os.Open(path) | |
| 227 | + | if err != nil { | |
| 228 | + | return ignoreMissing(err) | |
| 229 | + | } | |
| 230 | + | defer func() { _ = f.Close() }() | |
| 231 | + | var hdr [midxHeaderLen]byte | |
| 232 | + | if _, err := f.ReadAt(hdr[:], 0); err != nil || string(hdr[:4]) != "MIDX" { | |
| 233 | + | return unsupported("unreadable multi-pack-index") | |
| 234 | + | } | |
| 235 | + | if hdr[4] != 1 || hdr[5] != 1 { | |
| 236 | + | return unsupported("multi-pack-index version %d, object format %d", hdr[4], hdr[5]) | |
| 237 | + | } | |
| 238 | + | chunks := int(hdr[6]) | |
| 239 | + | if hdr[7] != 0 { | |
| 240 | + | return unsupported("incremental multi-pack-index") | |
| 241 | + | } | |
| 242 | + | nPacks := binary.BigEndian.Uint32(hdr[8:]) | |
| 243 | + | table := make([]byte, (chunks+1)*12) | |
| 244 | + | if _, err := f.ReadAt(table, midxHeaderLen); err != nil { | |
| 245 | + | return unsupported("multi-pack-index chunk table: %v", err) | |
| 246 | + | } | |
| 247 | + | var start, end uint64 | |
| 248 | + | for i := 0; i < chunks; i++ { | |
| 249 | + | if binary.BigEndian.Uint32(table[i*12:]) == midxChunkPNAM { | |
| 250 | + | start = binary.BigEndian.Uint64(table[i*12+4:]) | |
| 251 | + | end = binary.BigEndian.Uint64(table[(i+1)*12+4:]) | |
| 252 | + | break | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | if end <= start || end-start > maxMidxNames { | |
| 256 | + | return unsupported("multi-pack-index without a usable pack name chunk") | |
| 257 | + | } | |
| 258 | + | names := make([]byte, end-start) | |
| 259 | + | if _, err := f.ReadAt(names, int64(start)); err != nil { | |
| 260 | + | return unsupported("multi-pack-index pack names: %v", err) | |
| 261 | + | } | |
| 262 | + | have := make(map[string]bool, len(idxNames)) | |
| 263 | + | for _, n := range idxNames { | |
| 264 | + | have[n] = true | |
| 265 | + | } | |
| 266 | + | listed := uint32(0) | |
| 267 | + | for len(names) > 0 && listed < nPacks { | |
| 268 | + | i := 0 | |
| 269 | + | for i < len(names) && names[i] != 0 { | |
| 270 | + | i++ | |
| 271 | + | } | |
| 272 | + | if i == len(names) { | |
| 273 | + | break | |
| 274 | + | } | |
| 275 | + | name := string(names[:i]) | |
| 276 | + | names = names[i+1:] | |
| 277 | + | listed++ | |
| 278 | + | if name == "" { // padding | |
| 279 | + | continue | |
| 280 | + | } | |
| 281 | + | name = strings.TrimSuffix(strings.TrimSuffix(name, ".idx"), ".pack") | |
| 282 | + | if !have[name] { | |
| 283 | + | return unsupported("multi-pack-index lists %s, which has no .idx file", truncate(name)) | |
| 284 | + | } | |
| 285 | + | } | |
| 286 | + | return nil | |
| 287 | + | } | |
| @@ -0,0 +1,287 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "errors" | |||
| 8 | + | "io/fs" | |||
| 9 | + | "os" | |||
| 10 | + | "slices" | |||
| 11 | + | "strings" | |||
| 12 | + | "sync/atomic" | |||
| 13 | + | "time" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // fileSig identifies a version of a file or directory by stat data. | |||
| 17 | + | type fileSig struct { | |||
| 18 | + | exists bool | |||
| 19 | + | size int64 | |||
| 20 | + | mtime int64 // unix nanoseconds | |||
| 21 | + | ino uint64 | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func sigOf(fi os.FileInfo) fileSig { | |||
| 25 | + | return fileSig{exists: true, size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func statSig(path string) (fileSig, error) { | |||
| 29 | + | fi, err := os.Stat(path) | |||
| 30 | + | if errors.Is(err, fs.ErrNotExist) { | |||
| 31 | + | return fileSig{}, nil | |||
| 32 | + | } | |||
| 33 | + | if err != nil { | |||
| 34 | + | return fileSig{}, err | |||
| 35 | + | } | |||
| 36 | + | return sigOf(fi), nil | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // racyWindow is how long after its modification time a file may still change | |||
| 40 | + | // without its stat data changing, because of coarse file system clocks. | |||
| 41 | + | const racyWindow = 2 * time.Second | |||
| 42 | + | ||||
| 43 | + | // same reports whether b is the same version as a and can be trusted to be | |||
| 44 | + | // unchanged. | |||
| 45 | + | func (a fileSig) same(b fileSig) bool { | |||
| 46 | + | if a != b { | |||
| 47 | + | return false | |||
| 48 | + | } | |||
| 49 | + | return !a.exists || time.Since(time.Unix(0, a.mtime)) > racyWindow | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func ignoreMissing(err error) error { | |||
| 53 | + | if errors.Is(err, fs.ErrNotExist) { | |||
| 54 | + | return nil | |||
| 55 | + | } | |||
| 56 | + | return err | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // snapshot is an immutable list of packs, shared by readers. It is released | |||
| 60 | + | // by each reader and by the Repo; the last release drops its packs. | |||
| 61 | + | type snapshot struct { | |||
| 62 | + | refs atomic.Int32 | |||
| 63 | + | packs []*pack // newest first | |||
| 64 | + | dirSig fileSig // of objects/pack when the list was built | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func newSnapshot(packs []*pack) *snapshot { | |||
| 68 | + | s := &snapshot{packs: packs} | |||
| 69 | + | s.refs.Store(1) | |||
| 70 | + | return s | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (s *snapshot) release() { | |||
| 74 | + | if s.refs.Add(-1) == 0 { | |||
| 75 | + | for _, p := range s.packs { | |||
| 76 | + | p.release() | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // find looks id up in the packs. | |||
| 82 | + | func (s *snapshot) find(id *ObjectID) (*pack, uint64, error) { | |||
| 83 | + | for _, p := range s.packs { | |||
| 84 | + | if pos, ok := p.idx.find(id); ok { | |||
| 85 | + | off, err := p.idx.offset(pos) | |||
| 86 | + | if err != nil { | |||
| 87 | + | return nil, 0, wrapPack(p.name, err) | |||
| 88 | + | } | |||
| 89 | + | return p, off, nil | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | return nil, 0, nil | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // acquire returns the current snapshot, which the caller must release. | |||
| 96 | + | func (r *Repo) acquire() (*snapshot, error) { | |||
| 97 | + | for { | |||
| 98 | + | s := r.snap.Load() | |||
| 99 | + | if s == nil { | |||
| 100 | + | return nil, ErrClosed | |||
| 101 | + | } | |||
| 102 | + | n := s.refs.Load() | |||
| 103 | + | if n > 0 && s.refs.CompareAndSwap(n, n+1) { | |||
| 104 | + | return s, nil | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func (r *Repo) packDir() string { return r.dir + "/objects/pack" } | |||
| 110 | + | ||||
| 111 | + | // reloadPacks rescans objects/pack and installs a new snapshot when the | |||
| 112 | + | // directory changed, or always with force. It reports whether the list of | |||
| 113 | + | // packs changed. | |||
| 114 | + | func (r *Repo) reloadPacks(force bool) (bool, error) { | |||
| 115 | + | r.mu.Lock() | |||
| 116 | + | defer r.mu.Unlock() | |||
| 117 | + | if r.closed { | |||
| 118 | + | return false, ErrClosed | |||
| 119 | + | } | |||
| 120 | + | dir := r.packDir() | |||
| 121 | + | cur := r.snap.Load() | |||
| 122 | + | dsig, err := statSig(dir) | |||
| 123 | + | if err != nil { | |||
| 124 | + | return false, err | |||
| 125 | + | } | |||
| 126 | + | if !force && cur.dirSig.same(dsig) { | |||
| 127 | + | return false, nil | |||
| 128 | + | } | |||
| 129 | + | entries, err := os.ReadDir(dir) | |||
| 130 | + | if err != nil && !errors.Is(err, fs.ErrNotExist) { | |||
| 131 | + | return false, err | |||
| 132 | + | } | |||
| 133 | + | var idxNames []string | |||
| 134 | + | hasMidx := false | |||
| 135 | + | for _, e := range entries { | |||
| 136 | + | n := e.Name() | |||
| 137 | + | switch { | |||
| 138 | + | case n == "multi-pack-index": | |||
| 139 | + | hasMidx = true | |||
| 140 | + | case n == "multi-pack-index.d": | |||
| 141 | + | return false, unsupported("incremental multi-pack-index (objects/pack/multi-pack-index.d)") | |||
| 142 | + | case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".promisor"): | |||
| 143 | + | return false, unsupported("partial clone (%s)", n) | |||
| 144 | + | case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".idx"): | |||
| 145 | + | idxNames = append(idxNames, strings.TrimSuffix(n, ".idx")) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | if hasMidx { | |||
| 149 | + | if err := checkMidx(dir+"/multi-pack-index", idxNames); err != nil { | |||
| 150 | + | return false, err | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | old := make(map[string]*pack, len(cur.packs)) | |||
| 155 | + | for _, p := range cur.packs { | |||
| 156 | + | old[p.name] = p | |||
| 157 | + | } | |||
| 158 | + | var packs []*pack | |||
| 159 | + | changed := len(idxNames) != len(cur.packs) | |||
| 160 | + | for _, name := range idxNames { | |||
| 161 | + | isig, err := statSig(dir + "/" + name + ".idx") | |||
| 162 | + | if err != nil { | |||
| 163 | + | return false, err | |||
| 164 | + | } | |||
| 165 | + | if p := old[name]; p != nil && p.sig == isig { | |||
| 166 | + | p.retain() | |||
| 167 | + | packs = append(packs, p) | |||
| 168 | + | continue | |||
| 169 | + | } | |||
| 170 | + | p, err := openPack(dir, name, r.nextPack.Add(1), isig) | |||
| 171 | + | if err != nil { | |||
| 172 | + | for _, q := range packs { | |||
| 173 | + | q.release() | |||
| 174 | + | } | |||
| 175 | + | return false, err | |||
| 176 | + | } | |||
| 177 | + | if p == nil { // replaced while we looked | |||
| 178 | + | changed = true | |||
| 179 | + | continue | |||
| 180 | + | } | |||
| 181 | + | changed = true | |||
| 182 | + | packs = append(packs, p) | |||
| 183 | + | } | |||
| 184 | + | if len(packs) != len(cur.packs) { | |||
| 185 | + | changed = true | |||
| 186 | + | } | |||
| 187 | + | slices.SortStableFunc(packs, func(a, b *pack) int { | |||
| 188 | + | switch { | |||
| 189 | + | case a.sig.mtime > b.sig.mtime: | |||
| 190 | + | return -1 | |||
| 191 | + | case a.sig.mtime < b.sig.mtime: | |||
| 192 | + | return 1 | |||
| 193 | + | } | |||
| 194 | + | return strings.Compare(a.name, b.name) | |||
| 195 | + | }) | |||
| 196 | + | if !changed { | |||
| 197 | + | // Same packs: keep the current snapshot (and its readers) unless the | |||
| 198 | + | // directory stat data moved on. | |||
| 199 | + | for _, p := range packs { | |||
| 200 | + | p.release() | |||
| 201 | + | } | |||
| 202 | + | if cur.dirSig == dsig { | |||
| 203 | + | return false, nil | |||
| 204 | + | } | |||
| 205 | + | packs = cur.packs | |||
| 206 | + | for _, p := range packs { | |||
| 207 | + | p.retain() | |||
| 208 | + | } | |||
| 209 | + | } | |||
| 210 | + | ns := newSnapshot(packs) | |||
| 211 | + | ns.dirSig = dsig | |||
| 212 | + | r.snap.Store(ns) | |||
| 213 | + | cur.release() | |||
| 214 | + | return changed, nil | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | const ( | |||
| 218 | + | midxHeaderLen = 12 | |||
| 219 | + | midxChunkPNAM = 0x504e414d | |||
| 220 | + | maxMidxNames = 4 << 20 | |||
| 221 | + | ) | |||
| 222 | + | ||||
| 223 | + | // checkMidx accepts a multi-pack-index only when every pack it lists has | |||
| 224 | + | // its own .idx file: the midx is then redundant and ignored. | |||
| 225 | + | func checkMidx(path string, idxNames []string) error { | |||
| 226 | + | f, err := os.Open(path) | |||
| 227 | + | if err != nil { | |||
| 228 | + | return ignoreMissing(err) | |||
| 229 | + | } | |||
| 230 | + | defer func() { _ = f.Close() }() | |||
| 231 | + | var hdr [midxHeaderLen]byte | |||
| 232 | + | if _, err := f.ReadAt(hdr[:], 0); err != nil || string(hdr[:4]) != "MIDX" { | |||
| 233 | + | return unsupported("unreadable multi-pack-index") | |||
| 234 | + | } | |||
| 235 | + | if hdr[4] != 1 || hdr[5] != 1 { | |||
| 236 | + | return unsupported("multi-pack-index version %d, object format %d", hdr[4], hdr[5]) | |||
| 237 | + | } | |||
| 238 | + | chunks := int(hdr[6]) | |||
| 239 | + | if hdr[7] != 0 { | |||
| 240 | + | return unsupported("incremental multi-pack-index") | |||
| 241 | + | } | |||
| 242 | + | nPacks := binary.BigEndian.Uint32(hdr[8:]) | |||
| 243 | + | table := make([]byte, (chunks+1)*12) | |||
| 244 | + | if _, err := f.ReadAt(table, midxHeaderLen); err != nil { | |||
| 245 | + | return unsupported("multi-pack-index chunk table: %v", err) | |||
| 246 | + | } | |||
| 247 | + | var start, end uint64 | |||
| 248 | + | for i := 0; i < chunks; i++ { | |||
| 249 | + | if binary.BigEndian.Uint32(table[i*12:]) == midxChunkPNAM { | |||
| 250 | + | start = binary.BigEndian.Uint64(table[i*12+4:]) | |||
| 251 | + | end = binary.BigEndian.Uint64(table[(i+1)*12+4:]) | |||
| 252 | + | break | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | if end <= start || end-start > maxMidxNames { | |||
| 256 | + | return unsupported("multi-pack-index without a usable pack name chunk") | |||
| 257 | + | } | |||
| 258 | + | names := make([]byte, end-start) | |||
| 259 | + | if _, err := f.ReadAt(names, int64(start)); err != nil { | |||
| 260 | + | return unsupported("multi-pack-index pack names: %v", err) | |||
| 261 | + | } | |||
| 262 | + | have := make(map[string]bool, len(idxNames)) | |||
| 263 | + | for _, n := range idxNames { | |||
| 264 | + | have[n] = true | |||
| 265 | + | } | |||
| 266 | + | listed := uint32(0) | |||
| 267 | + | for len(names) > 0 && listed < nPacks { | |||
| 268 | + | i := 0 | |||
| 269 | + | for i < len(names) && names[i] != 0 { | |||
| 270 | + | i++ | |||
| 271 | + | } | |||
| 272 | + | if i == len(names) { | |||
| 273 | + | break | |||
| 274 | + | } | |||
| 275 | + | name := string(names[:i]) | |||
| 276 | + | names = names[i+1:] | |||
| 277 | + | listed++ | |||
| 278 | + | if name == "" { // padding | |||
| 279 | + | continue | |||
| 280 | + | } | |||
| 281 | + | name = strings.TrimSuffix(strings.TrimSuffix(name, ".idx"), ".pack") | |||
| 282 | + | if !have[name] { | |||
| 283 | + | return unsupported("multi-pack-index lists %s, which has no .idx file", truncate(name)) | |||
| 284 | + | } | |||
| 285 | + | } | |||
| 286 | + | return nil | |||
| 287 | + | } | |||
A tree.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "fmt" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Entry is an entry of a tree. | |
| 11 | + | type Entry struct { | |
| 12 | + | // Name is the file name. It points into the tree buffer and is only | |
| 13 | + | // valid during the callback that receives the entry; copy it to keep it. | |
| 14 | + | Name []byte | |
| 15 | + | Mode Mode | |
| 16 | + | ID ObjectID | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // ParseTree calls fn for each entry of a tree object in order, until fn | |
| 20 | + | // returns false. The entries are not copied, so iterating allocates nothing. | |
| 21 | + | // It returns ErrCorrupt on a malformed tree. | |
| 22 | + | func ParseTree(data []byte, fn func(Entry) bool) error { | |
| 23 | + | var e Entry | |
| 24 | + | for len(data) > 0 { | |
| 25 | + | sp := bytes.IndexByte(data, ' ') | |
| 26 | + | if sp <= 0 || sp > 7 { | |
| 27 | + | return corrupt("tree: bad mode") | |
| 28 | + | } | |
| 29 | + | var mode uint32 | |
| 30 | + | for _, c := range data[:sp] { | |
| 31 | + | if c < '0' || c > '7' { | |
| 32 | + | return corrupt("tree: bad mode") | |
| 33 | + | } | |
| 34 | + | mode = mode<<3 | uint32(c-'0') | |
| 35 | + | } | |
| 36 | + | data = data[sp+1:] | |
| 37 | + | nul := bytes.IndexByte(data, 0) | |
| 38 | + | if nul <= 0 || len(data)-nul-1 < 20 { | |
| 39 | + | return corrupt("tree: truncated entry") | |
| 40 | + | } | |
| 41 | + | e.Name = data[:nul] | |
| 42 | + | e.Mode = Mode(mode) | |
| 43 | + | copy(e.ID[:], data[nul+1:nul+21]) | |
| 44 | + | data = data[nul+21:] | |
| 45 | + | if !fn(e) { | |
| 46 | + | return nil | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | return nil | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // ReadTree reads the tree id and calls fn for each entry, in tree order, | |
| 53 | + | // until fn returns false. Entry.Name is only valid during the call. The tree | |
| 54 | + | // is read into a pooled buffer, so listing allocates nothing per entry. | |
| 55 | + | func (r *Repo) ReadTree(id ObjectID, fn func(Entry) bool) error { | |
| 56 | + | b, err := r.readTyped(id, TypeTree) | |
| 57 | + | if err != nil { | |
| 58 | + | return err | |
| 59 | + | } | |
| 60 | + | defer putBuf(b) | |
| 61 | + | if err := ParseTree(b.b, fn); err != nil { | |
| 62 | + | return wrapObj(id, err) | |
| 63 | + | } | |
| 64 | + | return nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // LookupPath finds the entry at path (slash separated, no leading slash) | |
| 68 | + | // below the tree root. It returns ErrNotFound when a component does not | |
| 69 | + | // exist or is not a directory. An empty path designates root itself, as a | |
| 70 | + | // directory. | |
| 71 | + | func (r *Repo) LookupPath(root ObjectID, path string) (Entry, error) { | |
| 72 | + | cur := Entry{Mode: ModeTree, ID: root} | |
| 73 | + | for path != "" { | |
| 74 | + | var name string | |
| 75 | + | name, path, _ = cutByte(path, '/') | |
| 76 | + | if name == "" { | |
| 77 | + | return Entry{}, notFound("empty path component") | |
| 78 | + | } | |
| 79 | + | if !cur.Mode.IsTree() { | |
| 80 | + | return Entry{}, notFound("%q is not a directory", name) | |
| 81 | + | } | |
| 82 | + | found := false | |
| 83 | + | var next Entry | |
| 84 | + | err := r.ReadTree(cur.ID, func(e Entry) bool { | |
| 85 | + | if string(e.Name) == name { | |
| 86 | + | next = Entry{Mode: e.Mode, ID: e.ID} | |
| 87 | + | found = true | |
| 88 | + | return false | |
| 89 | + | } | |
| 90 | + | return true | |
| 91 | + | }) | |
| 92 | + | if err != nil { | |
| 93 | + | return Entry{}, err | |
| 94 | + | } | |
| 95 | + | if !found { | |
| 96 | + | return Entry{}, notFound("path component %q", name) | |
| 97 | + | } | |
| 98 | + | cur = next | |
| 99 | + | } | |
| 100 | + | return cur, nil | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func cutByte(s string, c byte) (before, after string, found bool) { | |
| 104 | + | for i := 0; i < len(s); i++ { | |
| 105 | + | if s[i] == c { | |
| 106 | + | return s[:i], s[i+1:], true | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | return s, "", false | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func wrongType(id ObjectID, got, want Type) error { | |
| 113 | + | return fmt.Errorf("%w: %s is a %s, not a %s", ErrWrongType, id.String(), got, want) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func wrapObj(id ObjectID, err error) error { | |
| 117 | + | return fmt.Errorf("object %s: %w", id.String(), err) | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "fmt" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Entry is an entry of a tree. | |||
| 11 | + | type Entry struct { | |||
| 12 | + | // Name is the file name. It points into the tree buffer and is only | |||
| 13 | + | // valid during the callback that receives the entry; copy it to keep it. | |||
| 14 | + | Name []byte | |||
| 15 | + | Mode Mode | |||
| 16 | + | ID ObjectID | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // ParseTree calls fn for each entry of a tree object in order, until fn | |||
| 20 | + | // returns false. The entries are not copied, so iterating allocates nothing. | |||
| 21 | + | // It returns ErrCorrupt on a malformed tree. | |||
| 22 | + | func ParseTree(data []byte, fn func(Entry) bool) error { | |||
| 23 | + | var e Entry | |||
| 24 | + | for len(data) > 0 { | |||
| 25 | + | sp := bytes.IndexByte(data, ' ') | |||
| 26 | + | if sp <= 0 || sp > 7 { | |||
| 27 | + | return corrupt("tree: bad mode") | |||
| 28 | + | } | |||
| 29 | + | var mode uint32 | |||
| 30 | + | for _, c := range data[:sp] { | |||
| 31 | + | if c < '0' || c > '7' { | |||
| 32 | + | return corrupt("tree: bad mode") | |||
| 33 | + | } | |||
| 34 | + | mode = mode<<3 | uint32(c-'0') | |||
| 35 | + | } | |||
| 36 | + | data = data[sp+1:] | |||
| 37 | + | nul := bytes.IndexByte(data, 0) | |||
| 38 | + | if nul <= 0 || len(data)-nul-1 < 20 { | |||
| 39 | + | return corrupt("tree: truncated entry") | |||
| 40 | + | } | |||
| 41 | + | e.Name = data[:nul] | |||
| 42 | + | e.Mode = Mode(mode) | |||
| 43 | + | copy(e.ID[:], data[nul+1:nul+21]) | |||
| 44 | + | data = data[nul+21:] | |||
| 45 | + | if !fn(e) { | |||
| 46 | + | return nil | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | return nil | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // ReadTree reads the tree id and calls fn for each entry, in tree order, | |||
| 53 | + | // until fn returns false. Entry.Name is only valid during the call. The tree | |||
| 54 | + | // is read into a pooled buffer, so listing allocates nothing per entry. | |||
| 55 | + | func (r *Repo) ReadTree(id ObjectID, fn func(Entry) bool) error { | |||
| 56 | + | b, err := r.readTyped(id, TypeTree) | |||
| 57 | + | if err != nil { | |||
| 58 | + | return err | |||
| 59 | + | } | |||
| 60 | + | defer putBuf(b) | |||
| 61 | + | if err := ParseTree(b.b, fn); err != nil { | |||
| 62 | + | return wrapObj(id, err) | |||
| 63 | + | } | |||
| 64 | + | return nil | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // LookupPath finds the entry at path (slash separated, no leading slash) | |||
| 68 | + | // below the tree root. It returns ErrNotFound when a component does not | |||
| 69 | + | // exist or is not a directory. An empty path designates root itself, as a | |||
| 70 | + | // directory. | |||
| 71 | + | func (r *Repo) LookupPath(root ObjectID, path string) (Entry, error) { | |||
| 72 | + | cur := Entry{Mode: ModeTree, ID: root} | |||
| 73 | + | for path != "" { | |||
| 74 | + | var name string | |||
| 75 | + | name, path, _ = cutByte(path, '/') | |||
| 76 | + | if name == "" { | |||
| 77 | + | return Entry{}, notFound("empty path component") | |||
| 78 | + | } | |||
| 79 | + | if !cur.Mode.IsTree() { | |||
| 80 | + | return Entry{}, notFound("%q is not a directory", name) | |||
| 81 | + | } | |||
| 82 | + | found := false | |||
| 83 | + | var next Entry | |||
| 84 | + | err := r.ReadTree(cur.ID, func(e Entry) bool { | |||
| 85 | + | if string(e.Name) == name { | |||
| 86 | + | next = Entry{Mode: e.Mode, ID: e.ID} | |||
| 87 | + | found = true | |||
| 88 | + | return false | |||
| 89 | + | } | |||
| 90 | + | return true | |||
| 91 | + | }) | |||
| 92 | + | if err != nil { | |||
| 93 | + | return Entry{}, err | |||
| 94 | + | } | |||
| 95 | + | if !found { | |||
| 96 | + | return Entry{}, notFound("path component %q", name) | |||
| 97 | + | } | |||
| 98 | + | cur = next | |||
| 99 | + | } | |||
| 100 | + | return cur, nil | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func cutByte(s string, c byte) (before, after string, found bool) { | |||
| 104 | + | for i := 0; i < len(s); i++ { | |||
| 105 | + | if s[i] == c { | |||
| 106 | + | return s[:i], s[i+1:], true | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | return s, "", false | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func wrongType(id ObjectID, got, want Type) error { | |||
| 113 | + | return fmt.Errorf("%w: %s is a %s, not a %s", ErrWrongType, id.String(), got, want) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func wrapObj(id ObjectID, err error) error { | |||
| 117 | + | return fmt.Errorf("object %s: %w", id.String(), err) | |||
| 118 | + | } | |||
A types.go +94 -0
| @@ -0,0 +1,94 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import "strconv" | |
| 6 | + | ||
| 7 | + | // Type is the type of a Git object. The values are those of pack entries. | |
| 8 | + | type Type uint8 | |
| 9 | + | ||
| 10 | + | // Object types. | |
| 11 | + | const ( | |
| 12 | + | TypeCommit Type = 1 | |
| 13 | + | TypeTree Type = 2 | |
| 14 | + | TypeBlob Type = 3 | |
| 15 | + | TypeTag Type = 4 | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // String returns the name Git uses for the type ("commit", "tree", "blob", | |
| 19 | + | // "tag"). | |
| 20 | + | func (t Type) String() string { | |
| 21 | + | switch t { | |
| 22 | + | case TypeCommit: | |
| 23 | + | return "commit" | |
| 24 | + | case TypeTree: | |
| 25 | + | return "tree" | |
| 26 | + | case TypeBlob: | |
| 27 | + | return "blob" | |
| 28 | + | case TypeTag: | |
| 29 | + | return "tag" | |
| 30 | + | } | |
| 31 | + | return "type(" + strconv.Itoa(int(t)) + ")" | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func parseTypeName(b []byte) (Type, bool) { | |
| 35 | + | switch string(b) { | |
| 36 | + | case "commit": | |
| 37 | + | return TypeCommit, true | |
| 38 | + | case "tree": | |
| 39 | + | return TypeTree, true | |
| 40 | + | case "blob": | |
| 41 | + | return TypeBlob, true | |
| 42 | + | case "tag": | |
| 43 | + | return TypeTag, true | |
| 44 | + | } | |
| 45 | + | return 0, false | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Mode is the file mode of a tree entry. | |
| 49 | + | type Mode uint32 | |
| 50 | + | ||
| 51 | + | // Tree entry modes. | |
| 52 | + | const ( | |
| 53 | + | ModeTree Mode = 0o040000 | |
| 54 | + | ModeFile Mode = 0o100644 | |
| 55 | + | ModeExec Mode = 0o100755 | |
| 56 | + | ModeSymlink Mode = 0o120000 | |
| 57 | + | ModeGitlink Mode = 0o160000 // a submodule | |
| 58 | + | ) | |
| 59 | + | ||
| 60 | + | const modeTypeMask Mode = 0o170000 | |
| 61 | + | ||
| 62 | + | // IsTree reports whether the entry is a directory. | |
| 63 | + | func (m Mode) IsTree() bool { return m&modeTypeMask == ModeTree } | |
| 64 | + | ||
| 65 | + | // IsGitlink reports whether the entry is a submodule commit. | |
| 66 | + | func (m Mode) IsGitlink() bool { return m&modeTypeMask == ModeGitlink } | |
| 67 | + | ||
| 68 | + | // IsSymlink reports whether the entry is a symbolic link. | |
| 69 | + | func (m Mode) IsSymlink() bool { return m&modeTypeMask == ModeSymlink } | |
| 70 | + | ||
| 71 | + | // IsRegular reports whether the entry is a regular file, executable or not. | |
| 72 | + | func (m Mode) IsRegular() bool { return m&modeTypeMask == 0o100000 } | |
| 73 | + | ||
| 74 | + | // Type returns the type of the object the entry designates: a tree for a | |
| 75 | + | // directory, a commit for a submodule, a blob otherwise. | |
| 76 | + | func (m Mode) Type() Type { | |
| 77 | + | switch m & modeTypeMask { | |
| 78 | + | case ModeTree: | |
| 79 | + | return TypeTree | |
| 80 | + | case ModeGitlink: | |
| 81 | + | return TypeCommit | |
| 82 | + | } | |
| 83 | + | return TypeBlob | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // String returns the mode as Git prints it: six octal digits (five for a | |
| 87 | + | // directory is printed as "040000"). | |
| 88 | + | func (m Mode) String() string { | |
| 89 | + | s := strconv.FormatUint(uint64(m), 8) | |
| 90 | + | for len(s) < 6 { | |
| 91 | + | s = "0" + s | |
| 92 | + | } | |
| 93 | + | return s | |
| 94 | + | } | |
| @@ -0,0 +1,94 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import "strconv" | |||
| 6 | + | ||||
| 7 | + | // Type is the type of a Git object. The values are those of pack entries. | |||
| 8 | + | type Type uint8 | |||
| 9 | + | ||||
| 10 | + | // Object types. | |||
| 11 | + | const ( | |||
| 12 | + | TypeCommit Type = 1 | |||
| 13 | + | TypeTree Type = 2 | |||
| 14 | + | TypeBlob Type = 3 | |||
| 15 | + | TypeTag Type = 4 | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // String returns the name Git uses for the type ("commit", "tree", "blob", | |||
| 19 | + | // "tag"). | |||
| 20 | + | func (t Type) String() string { | |||
| 21 | + | switch t { | |||
| 22 | + | case TypeCommit: | |||
| 23 | + | return "commit" | |||
| 24 | + | case TypeTree: | |||
| 25 | + | return "tree" | |||
| 26 | + | case TypeBlob: | |||
| 27 | + | return "blob" | |||
| 28 | + | case TypeTag: | |||
| 29 | + | return "tag" | |||
| 30 | + | } | |||
| 31 | + | return "type(" + strconv.Itoa(int(t)) + ")" | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func parseTypeName(b []byte) (Type, bool) { | |||
| 35 | + | switch string(b) { | |||
| 36 | + | case "commit": | |||
| 37 | + | return TypeCommit, true | |||
| 38 | + | case "tree": | |||
| 39 | + | return TypeTree, true | |||
| 40 | + | case "blob": | |||
| 41 | + | return TypeBlob, true | |||
| 42 | + | case "tag": | |||
| 43 | + | return TypeTag, true | |||
| 44 | + | } | |||
| 45 | + | return 0, false | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Mode is the file mode of a tree entry. | |||
| 49 | + | type Mode uint32 | |||
| 50 | + | ||||
| 51 | + | // Tree entry modes. | |||
| 52 | + | const ( | |||
| 53 | + | ModeTree Mode = 0o040000 | |||
| 54 | + | ModeFile Mode = 0o100644 | |||
| 55 | + | ModeExec Mode = 0o100755 | |||
| 56 | + | ModeSymlink Mode = 0o120000 | |||
| 57 | + | ModeGitlink Mode = 0o160000 // a submodule | |||
| 58 | + | ) | |||
| 59 | + | ||||
| 60 | + | const modeTypeMask Mode = 0o170000 | |||
| 61 | + | ||||
| 62 | + | // IsTree reports whether the entry is a directory. | |||
| 63 | + | func (m Mode) IsTree() bool { return m&modeTypeMask == ModeTree } | |||
| 64 | + | ||||
| 65 | + | // IsGitlink reports whether the entry is a submodule commit. | |||
| 66 | + | func (m Mode) IsGitlink() bool { return m&modeTypeMask == ModeGitlink } | |||
| 67 | + | ||||
| 68 | + | // IsSymlink reports whether the entry is a symbolic link. | |||
| 69 | + | func (m Mode) IsSymlink() bool { return m&modeTypeMask == ModeSymlink } | |||
| 70 | + | ||||
| 71 | + | // IsRegular reports whether the entry is a regular file, executable or not. | |||
| 72 | + | func (m Mode) IsRegular() bool { return m&modeTypeMask == 0o100000 } | |||
| 73 | + | ||||
| 74 | + | // Type returns the type of the object the entry designates: a tree for a | |||
| 75 | + | // directory, a commit for a submodule, a blob otherwise. | |||
| 76 | + | func (m Mode) Type() Type { | |||
| 77 | + | switch m & modeTypeMask { | |||
| 78 | + | case ModeTree: | |||
| 79 | + | return TypeTree | |||
| 80 | + | case ModeGitlink: | |||
| 81 | + | return TypeCommit | |||
| 82 | + | } | |||
| 83 | + | return TypeBlob | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // String returns the mode as Git prints it: six octal digits (five for a | |||
| 87 | + | // directory is printed as "040000"). | |||
| 88 | + | func (m Mode) String() string { | |||
| 89 | + | s := strconv.FormatUint(uint64(m), 8) | |||
| 90 | + | for len(s) < 6 { | |||
| 91 | + | s = "0" + s | |||
| 92 | + | } | |||
| 93 | + | return s | |||
| 94 | + | } | |||