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
|
package ingest
import (
"bufio"
"bytes"
"errors"
"io"
"os"
"codeberg.org/lindenii/furgit/common/iowrap"
objectid "codeberg.org/lindenii/furgit/object/id"
objectstore "codeberg.org/lindenii/furgit/object/store"
)
// Options controls one pack ingest operation.
type Options struct {
// FixThin appends missing local bases for thin packs.
FixThin bool
// WriteRev writes a .rev alongside the .pack and .idx.
WriteRev bool
// Base supplies existing objects for thin-pack fixup.
Base objectstore.ReadingStore
// Progress receives human-readable progress messages.
//
// When nil, no progress output is emitted.
Progress iowrap.WriteFlusher
// RequireTrailingEOF requires the source to hit EOF after the pack trailer.
//
// This is suitable for exact pack-file readers, but should be disabled for
// full-duplex transport streams like receive-pack where the peer keeps the
// connection open to read the server response.
RequireTrailingEOF bool
}
// Result describes one successful ingest transaction.
type Result struct {
// PackName is the destination-relative filename of the written .pack.
PackName string
// IdxName is the destination-relative filename of the written .idx.
IdxName string
// RevName is the destination-relative filename of the written .rev.
//
// RevName is empty when writeRev is false.
RevName string
// PackHash is the final pack hash (same hash embedded in .idx/.rev trailers).
PackHash objectid.ObjectID
// ObjectCount is the final object count in the resulting pack.
//
// If thin fixup appends objects, this includes appended base objects.
ObjectCount uint32
// ThinFixed reports whether thin fixup appended local bases.
ThinFixed bool
}
// HeaderInfo describes the parsed PACK header.
type HeaderInfo struct {
Version uint32
ObjectCount uint32
}
// DiscardResult describes one successful Discard call.
type DiscardResult struct {
PackHash objectid.ObjectID
ObjectCount uint32
}
// Pending is one started ingest operation awaiting Continue or Discard.
//
// Exactly one of Continue or Discard may be called.
//
// Labels: MT-Unsafe.
type Pending struct {
reader *bufio.Reader
algo objectid.Algorithm
opts Options
header HeaderInfo
headerRaw [packHeaderSize]byte
finalized bool
}
// Ingest reads and validates one PACK header, returning one pending operation.
//
// Labels: Deps-Borrowed, Life-Parent.
func Ingest(
src io.Reader,
algo objectid.Algorithm,
opts Options,
) (*Pending, error) {
if algo.Size() == 0 {
return nil, objectid.ErrInvalidAlgorithm
}
reader := bufio.NewReader(src)
header, headerRaw, err := readAndValidatePackHeader(reader)
if err != nil {
return nil, err
}
return &Pending{
reader: reader,
algo: algo,
opts: opts,
header: header,
headerRaw: headerRaw,
}, nil
}
// Header returns parsed PACK header info.
func (pending *Pending) Header() HeaderInfo {
return pending.header
}
// Continue ingests the pack stream into destination and writes pack artifacts.
//
// Continue invalidates the receiver.
//
// Artifacts are published under content-addressed final names derived from the
// resulting pack hash. If those final names already exist, Continue treats that
// as success and removes its temporary files.
func (pending *Pending) Continue(destination *os.Root) (Result, error) {
pending.finalized = true
if pending.header.ObjectCount == 0 {
return Result{}, ErrZeroObjectContinue
}
state, err := newIngestState(
pending.reader,
destination,
pending.algo,
pending.opts,
pending.header,
pending.headerRaw,
)
if err != nil {
return Result{}, err
}
return ingest(state)
}
// Discard consumes and verifies one zero-object pack stream without writing
// files.
//
// Discard invalidates the receiver.
func (pending *Pending) Discard() (DiscardResult, error) {
pending.finalized = true
if pending.header.ObjectCount != 0 {
return DiscardResult{}, ErrNonZeroDiscard
}
hashImpl, err := pending.algo.New()
if err != nil {
return DiscardResult{}, err
}
_, _ = hashImpl.Write(pending.headerRaw[:])
trailer := make([]byte, pending.algo.Size())
_, err = io.ReadFull(pending.reader, trailer)
if err != nil {
return DiscardResult{}, &PackTrailerMismatchError{}
}
computed := hashImpl.Sum(nil)
if !bytes.Equal(computed, trailer) {
return DiscardResult{}, &PackTrailerMismatchError{}
}
if pending.opts.RequireTrailingEOF {
var probe [1]byte
n, err := pending.reader.Read(probe[:])
if n > 0 || err == nil {
return DiscardResult{}, errors.New("packfile/ingest: pack has trailing garbage")
}
if err != io.EOF {
return DiscardResult{}, err
}
}
packHash, err := objectid.FromBytes(pending.algo, trailer)
if err != nil {
return DiscardResult{}, err
}
return DiscardResult{
PackHash: packHash,
ObjectCount: 0,
}, nil
}
|