main de06cbd

blob.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
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. The checksum of the stream is verified when the last byte is
18// read: that Read returns (0, ErrCorrupt) instead of the final chunk, so
19// io.ReadFull and io.CopyN of exactly size bytes report a corrupt trailer too.
20// Close returns ErrCorrupt if all size bytes were consumed (or the blob is
21// empty) but the end of the stream was not checked. A reader closed before
22// size bytes were read cannot verify the checksum.
23func (r *Repo) OpenBlob(id ObjectID) (int64, io.ReadCloser, error) {
24	f, err := r.finder()
25	if err != nil {
26		return 0, nil, err
27	}
28	defer f.close()
29	for {
30		p, off, err := f.pack(&id)
31		if err != nil {
32			return 0, nil, err
33		}
34		if p != nil {
35			size, rc, err := r.openPackedBlob(p, off, id)
36			if err != nil {
37				return 0, nil, wrapPack(p.name, err)
38			}
39			return size, rc, nil
40		}
41		size, rc, err := r.openLooseBlob(&id)
42		if err == nil {
43			return size, rc, nil
44		}
45		if !errors.Is(err, ErrNotFound) {
46			return 0, nil, err
47		}
48		if again, err := f.again(); err != nil {
49			return 0, nil, err
50		} else if !again {
51			return 0, nil, notFound("object %s", id)
52		}
53	}
54}
55
56func (r *Repo) openPackedBlob(p *pack, off uint64, id ObjectID) (int64, io.ReadCloser, error) {
57	h, err := parseEntryHeader(p.data, off)
58	if err != nil {
59		return 0, nil, err
60	}
61	if h.isDelta() {
62		b := getBuf()
63		typ, data, err := r.readPacked(p, off, b.b)
64		if err != nil {
65			putBuf(b)
66			return 0, nil, err
67		}
68		b.b = data
69		if typ != TypeBlob {
70			putBuf(b)
71			return 0, nil, wrongType(id, typ, TypeBlob)
72		}
73		return int64(len(data)), &memBlob{buf: b, r: bytes.NewReader(data)}, nil
74	}
75	if Type(h.typ) != TypeBlob {
76		return 0, nil, wrongType(id, Type(h.typ), TypeBlob)
77	}
78	if h.size > 1<<62 || h.size/1032 > uint64(len(p.stream(&h))) {
79		return 0, nil, corrupt("blob of %d bytes at %d", h.size, off)
80	}
81	m, err := getMem(p.stream(&h))
82	if err != nil {
83		return 0, nil, err
84	}
85	p.retain()
86	return int64(h.size), &streamBlob{src: m, pack: p, left: int64(h.size), zr: &m.zr}, nil
87}
88
89func (r *Repo) openLooseBlob(id *ObjectID) (int64, io.ReadCloser, error) {
90	l, err := r.openLoose(id)
91	if err != nil {
92		return 0, nil, err
93	}
94	if l.typ != TypeBlob {
95		l.close()
96		return 0, nil, wrongType(*id, l.typ, TypeBlob)
97	}
98	if l.size > 1<<62 || uint64(l.nlea) > l.size {
99		l.close()
100		return 0, nil, corrupt("loose object %s: size %d", id.String(), l.size)
101	}
102	return int64(l.size), &streamBlob{loose: l, left: int64(l.size), zr: &l.fi.zr, lead: l.lead[:l.nlea]}, nil
103}
104
105// memBlob serves a blob rebuilt in memory.
106type memBlob struct {
107	buf *buffer
108	r   *bytes.Reader
109}
110
111func (m *memBlob) Read(p []byte) (int, error) {
112	if m.r == nil {
113		return 0, io.ErrClosedPipe
114	}
115	return m.r.Read(p)
116}
117
118func (m *memBlob) WriteTo(w io.Writer) (int64, error) {
119	if m.r == nil {
120		return 0, io.ErrClosedPipe
121	}
122	return m.r.WriteTo(w)
123}
124
125func (m *memBlob) Close() error {
126	if m.r != nil {
127		m.r = nil
128		putBuf(m.buf)
129		m.buf = nil
130	}
131	return nil
132}
133
134// streamBlob inflates a blob while it is read.
135type streamBlob struct {
136	src   *memInflater // packed
137	pack  *pack
138	loose *looseObject // loose
139	zr    io.Reader
140	lead  []byte // loose: content already read with the header
141	left  int64  // bytes still to return
142	done  bool   // end of stream verified
143	err   error
144	probe [1]byte
145}
146
147func (s *streamBlob) Read(p []byte) (int, error) {
148	if s.zr == nil {
149		return 0, io.ErrClosedPipe
150	}
151	if s.err != nil {
152		return 0, s.err
153	}
154	if len(p) == 0 {
155		return 0, nil
156	}
157	if s.left == 0 {
158		return 0, s.finish()
159	}
160	if int64(len(p)) > s.left {
161		p = p[:s.left]
162	}
163	if len(s.lead) > 0 {
164		n := copy(p, s.lead)
165		s.lead = s.lead[n:]
166		s.left -= int64(n)
167		return s.served(n)
168	}
169	n, err := s.zr.Read(p)
170	s.left -= int64(n)
171	if err != nil && !errors.Is(err, io.EOF) {
172		s.err = corrupt("blob: %v", err)
173		return 0, s.err
174	}
175	if errors.Is(err, io.EOF) && s.left > 0 {
176		s.err = corrupt("blob shorter than its declared size")
177		return 0, s.err
178	}
179	if n > 0 {
180		return s.served(n)
181	}
182	if s.left == 0 {
183		return 0, s.finish()
184	}
185	return 0, nil
186}
187
188// served returns the n > 0 bytes just read. When they are the last ones, it
189// verifies the end of the stream and its checksum before returning them, so a
190// caller that stops at the declared size (io.ReadFull, io.CopyN) still sees a
191// corrupt trailer. On failure the chunk is withheld, (0, err), because
192// io.ReadFull drops an error that comes with a full buffer.
193func (s *streamBlob) served(n int) (int, error) {
194	if s.left == 0 {
195		if err := s.finish(); !errors.Is(err, io.EOF) {
196			return 0, err
197		}
198	}
199	return n, nil
200}
201
202// finish checks that the stream ends exactly here and that its checksum is
203// right, then reports EOF.
204func (s *streamBlob) finish() error {
205	if !s.done {
206		n, err := s.zr.Read(s.probe[:])
207		switch {
208		case n != 0:
209			s.err = corrupt("blob longer than its declared size")
210		case !errors.Is(err, io.EOF):
211			s.err = corrupt("blob: %v", err)
212		}
213		s.done = true
214	}
215	if s.err != nil {
216		return s.err
217	}
218	return io.EOF
219}
220
221func (s *streamBlob) Close() error {
222	if s.zr == nil {
223		return nil
224	}
225	var err error
226	if s.left == 0 && !s.done {
227		// Nothing was read yet (an empty blob): verify the stream now.
228		if err = s.finish(); errors.Is(err, io.EOF) {
229			err = nil
230		}
231	}
232	s.zr = nil
233	if s.src != nil {
234		putMem(s.src)
235		s.pack.release()
236	}
237	if s.loose != nil {
238		s.loose.close()
239	}
240	return err
241}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close