aboutsummaryrefslogtreecommitdiff
path: root/internal/format/packidx/write.go
blob: d3f22c83812cb675005b62f2260cb165fb3d1027 (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
package packidx

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"math"

	"lindenii.org/go/furgit/internal/stickyio"
	"lindenii.org/go/furgit/object/id"
)

// ErrInvalidEntries reports that
// entries supplied for an index write
// are unsorted, duplicated, or not representable
// in the pack index format.
var ErrInvalidEntries = errors.New("internal/format/packidx: invalid entries")

// Entry is one object record for an index write.
type Entry struct {
	// OID holds the object ID bytes;
	// only the first hash-size bytes are meaningful.
	OID [id.MaxObjectIDSize]byte
	// Offset is the entry's pack file offset.
	Offset uint64
	// CRC32 is the CRC32 of the entry's packed data.
	CRC32 uint32
}

// Write writes one pack index over entries to w.
//
// entries must be sorted by object ID without duplicates.
// packHash must be the pack's trailer hash;
// Write panics when its length does not match the object format.
func Write(w io.Writer, objectFormat id.ObjectFormat, entries []Entry, packHash []byte) error {
	hashSize := objectFormat.Size()
	if hashSize == 0 {
		return id.ErrInvalidObjectFormat
	}

	if len(packHash) != hashSize {
		panic("internal/format/packidx: invalid pack hash length")
	}

	if len(entries) > math.MaxUint32 {
		return fmt.Errorf("%w: too many entries", ErrInvalidEntries)
	}

	for i := 1; i < len(entries); i++ {
		if bytes.Compare(entries[i-1].OID[:hashSize], entries[i].OID[:hashSize]) >= 0 {
			return fmt.Errorf("%w: not sorted by object ID", ErrInvalidEntries)
		}
	}

	hashImpl, err := objectFormat.New()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	bw := bufio.NewWriter(io.MultiWriter(w, hashImpl))
	sw := stickyio.New(bw)

	sw.PutUint32(signature)
	sw.PutUint32(version)

	var counts [256]uint32
	for i := range entries {
		counts[entries[i].OID[0]]++
	}

	cumulative := uint32(0)
	for _, count := range counts {
		cumulative += count
		sw.PutUint32(cumulative)
	}

	for i := range entries {
		sw.Put(entries[i].OID[:hashSize])
	}

	for i := range entries {
		sw.PutUint32(entries[i].CRC32)
	}

	var largeOffsets []uint64

	for i := range entries {
		offset := entries[i].Offset
		if offset < largeOffsetFlag {
			sw.PutUint32(uint32(offset))

			continue
		}

		slot := len(largeOffsets)
		if slot >= largeOffsetFlag {
			return fmt.Errorf("%w: too many large offsets", ErrInvalidEntries)
		}

		sw.PutUint32(largeOffsetFlag | uint32(slot))

		largeOffsets = append(largeOffsets, offset)
	}

	for _, offset := range largeOffsets {
		sw.PutUint64(offset)
	}

	sw.Put(packHash)

	err = sw.Err()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	err = bw.Flush()
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	_, err = w.Write(hashImpl.Sum(nil))
	if err != nil {
		return fmt.Errorf("internal/format/packidx: %w", err)
	}

	return nil
}