1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
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.
15const maxLooseHeader = 32
16
17func (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.
30type 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.
42func (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.
79const statThreshold = 1 << 16
80
81func 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
88func (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
99func (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>".
129func 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.
159func (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.
183const headerPrefix = 1024
184
185// looseHeader reads the type and size of a loose object, inflating as little
186// as possible.
187func (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.
219func 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.
244func (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}