aboutsummaryrefslogtreecommitdiff
path: root/object/store/loose/reader.go
blob: 940ce3303bcc48df432f99b5993d2eac86466d46 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package loose

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"

	"lindenii.org/go/furgit/internal/compress/zlib"
	"lindenii.org/go/furgit/internal/iolimit"
	"lindenii.org/go/furgit/object/id"
	"lindenii.org/go/furgit/object/store"
	"lindenii.org/go/furgit/object/typ"
)

// ReadBytesFull reads a full serialized object as "type size\x00content".
//
// It inflates and parses the full loose object,
// including verifying the zlib Adler-32 trailer.
func (loose *Loose) ReadBytesFull(objectID id.ObjectID) ([]byte, error) {
	raw, _, _, err := loose.readBytesParsed(objectID)
	if err != nil {
		return nil, err
	}

	return raw, nil
}

// ReadBytesContent reads an object's type and content bytes.
//
// Like ReadBytesFull,
// it inflates and parses the full loose object,
// including verifying the zlib Adler-32 trailer.
func (loose *Loose) ReadBytesContent(objectID id.ObjectID) (typ.Type, []byte, error) {
	_, ty, content, err := loose.readBytesParsed(objectID)
	if err != nil {
		return typ.TypeUnknown, nil, err
	}

	return ty, content, nil
}

// ReadHeader reads an object's type and declared content length.
//
// It parses only enough of the zlib-decoded object to recover the object header.
// It does not verify that the remaining object content is readable
// and does not verify the zlib Adler-32 trailer.
func (loose *Loose) ReadHeader(objectID id.ObjectID) (typ.Type, uint64, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return typ.TypeUnknown, 0, err
	}

	defer func() { _ = file.Close() }()

	zr, err := zlib.NewReader(file)
	if err != nil {
		return typ.TypeUnknown, 0, fmt.Errorf("object/store/loose: %w", err)
	}

	defer func() { _ = zr.Close() }()

	_, ty, size, err := readHeader(bufio.NewReader(zr))
	if err != nil {
		return typ.TypeUnknown, 0, err
	}

	return ty, size, nil
}

// ReadSize reads an object's declared content length.
//
// Like ReadHeader,
// it parses only enough of the zlib-decoded object to recover the header
// and does not verify the zlib Adler-32 trailer.
func (loose *Loose) ReadSize(objectID id.ObjectID) (uint64, error) {
	_, size, err := loose.ReadHeader(objectID)

	return size, err
}

// ReadReaderFull reads a full serialized object stream as "type size\x00content".
//
// Close releases resources only.
// It does not drain unread data for additional validation.
// In particular,
// malformed trailing compressed data,
// trailing bytes past the declared object size,
// and the zlib Adler-32 trailer
// may go unverified unless the caller reads to io.EOF.
func (loose *Loose) ReadReaderFull(objectID id.ObjectID) (io.ReadCloser, error) {
	file, zr, err := loose.openInflated(objectID)
	if err != nil {
		return nil, err
	}

	br := bufio.NewReader(zr)

	headerBytes, _, size, err := readHeader(br)
	if err != nil {
		_ = zr.Close()
		_ = file.Close()

		return nil, err
	}

	return &objectReader{
		reader: io.MultiReader(
			bytes.NewReader(headerBytes),
			iolimit.ExpectLengthReader(br, size),
		),
		file: file,
		zr:   zr,
	}, nil
}

// ReadReaderContent reads an object's type, declared content length,
// and content stream.
//
// Close releases resources only.
// It does not drain unread data for additional validation.
// In particular,
// malformed trailing compressed data,
// trailing bytes past the declared object size,
// and the zlib Adler-32 trailer
// may go unverified unless the caller reads to io.EOF.
func (loose *Loose) ReadReaderContent(objectID id.ObjectID) (typ.Type, uint64, io.ReadCloser, error) {
	file, zr, err := loose.openInflated(objectID)
	if err != nil {
		return typ.TypeUnknown, 0, nil, err
	}

	br := bufio.NewReader(zr)

	_, ty, size, err := readHeader(br)
	if err != nil {
		_ = zr.Close()
		_ = file.Close()

		return typ.TypeUnknown, 0, nil, err
	}

	return ty, size, &objectReader{
		reader: iolimit.ExpectLengthReader(br, size),
		file:   file,
		zr:     zr,
	}, nil
}

// Refresh is a no-op for loose object stores.
func (loose *Loose) Refresh() error {
	return nil
}

// openObject opens the loose object file for objectID.
// Missing files cause store.ErrObjectNotFound.
func (loose *Loose) openObject(objectID id.ObjectID) (*os.File, error) {
	relPath, err := loose.objectPath(objectID)
	if err != nil {
		return nil, err
	}

	file, err := loose.root.Open(relPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, store.ErrObjectNotFound
		}

		return nil, fmt.Errorf("object/store/loose: %w", err)
	}

	return file, nil
}

// readBytesParsed reads, inflates, and parses a loose object in one pass.
// It returns the full raw payload and its parsed type and content.
func (loose *Loose) readBytesParsed(objectID id.ObjectID) ([]byte, typ.Type, []byte, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

	defer func() { _ = file.Close() }()

	raw, err := decodeAll(file)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

	ty, content, err := parseRaw(raw)
	if err != nil {
		return nil, typ.TypeUnknown, nil, err
	}

	return raw, ty, content, nil
}

// openInflated opens and zlib-decodes a loose object file.
// The caller owns both returned closers and must close them.
func (loose *Loose) openInflated(objectID id.ObjectID) (*os.File, io.ReadCloser, error) {
	file, err := loose.openObject(objectID)
	if err != nil {
		return nil, nil, err
	}

	zr, err := zlib.NewReader(file)
	if err != nil {
		_ = file.Close()

		return nil, nil, fmt.Errorf("object/store/loose: %w", err)
	}

	return file, zr, nil
}

// objectReader streams one inflated loose object
// and owns the underlying file and zlib decoder.
type objectReader struct {
	// reader is the stream exposed by Read.
	reader io.Reader
	// file is the underlying loose object file and is closed by Close.
	file *os.File
	// zr is the zlib decoder and is closed by Close.
	zr io.ReadCloser
}

func (reader *objectReader) Read(dst []byte) (int, error) {
	return reader.reader.Read(dst) //nolint:wrapcheck
}

func (reader *objectReader) Close() error {
	errZlib := reader.zr.Close()
	errFile := reader.file.Close()

	return errors.Join(errZlib, errFile)
}