aboutsummaryrefslogtreecommitdiffstats
path: root/formats/objfile/writer.go
blob: 8337a3a2282503b6b3f3288dc0000584e5cac318 (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
package objfile

import (
	"errors"
	"io"

	"gopkg.in/src-d/go-git.v4/core"

	"github.com/klauspost/compress/zlib"
)

var (
	// ErrOverflow is returned when an attempt is made to write more data than
	// was declared in NewWriter.
	ErrOverflow = errors.New("objfile: declared data length exceeded (overflow)")
)

// Writer writes and encodes data in compressed objfile format to a provided
// io.Writer.
//
// Writer implements io.WriteCloser. Close should be called when finished with
// the Writer. Close will not close the underlying io.Writer.
type Writer struct {
	header header
	hash   core.Hash // final computed hash stored after Close

	w          io.Writer      // provided writer wrapped in compressor and tee
	compressor io.WriteCloser // provided writer wrapped in compressor, retained for calling Close
	h          core.Hasher    // streaming SHA1 hash of encoded data
	written    int64          // Number of bytes written
}

// NewWriter returns a new Writer writing to w.
//
// The provided t is the type of object being written. The provided size is the
// number of uncompressed bytes being written.
//
// Calling NewWriter causes it to immediately write header data containing
// size and type information. Any errors encountered in that process will be
// returned in err.
//
// If an invalid t is provided, core.ErrInvalidType is returned. If a negative
// size is provided, ErrNegativeSize is returned.
//
// The returned Writer implements io.WriteCloser. Close should be called when
// finished with the Writer. Close will not close the underlying io.Writer.
func NewWriter(w io.Writer, t core.ObjectType, size int64) (*Writer, error) {
	if !t.Valid() {
		return nil, core.ErrInvalidType
	}
	if size < 0 {
		return nil, ErrNegativeSize
	}
	writer := &Writer{
		header: header{t: t, size: size},
	}
	return writer, writer.init(w)
}

// init prepares the zlib compressor for the given output as well as a hasher
// for computing its hash.
//
// init immediately writes header data to the output. This leaves the writer in
// a state that is ready to write content.
func (w *Writer) init(output io.Writer) (err error) {
	w.compressor = zlib.NewWriter(output)

	err = w.header.Write(w.compressor)
	if err != nil {
		w.compressor.Close()
		return
	}

	w.h = core.NewHasher(w.header.t, w.header.size)
	w.w = io.MultiWriter(w.compressor, w.h) // All writes to the compressor also write to the hash

	return
}

// Write reads len(p) from p to the object data stream. It returns the number of
// bytes written from p (0 <= n <= len(p)) and any error encountered that caused
// the write to stop early. The slice data contained in p will not be modified.
//
// If writing len(p) bytes would exceed the size provided in NewWriter,
// ErrOverflow is returned without writing any data.
func (w *Writer) Write(p []byte) (n int, err error) {
	if w.w == nil {
		return 0, ErrClosed
	}

	if w.written+int64(len(p)) > w.header.size {
		return 0, ErrOverflow
	}

	n, err = w.w.Write(p)
	w.written += int64(n)

	return
}

// Type returns the type of the object.
func (w *Writer) Type() core.ObjectType {
	return w.header.t
}

// Size returns the uncompressed size of the object in bytes.
func (w *Writer) Size() int64 {
	return w.header.size
}

// Hash returns the hash of the object data stream that has been written so far.
// It can be called before or after Close.
func (w *Writer) Hash() core.Hash {
	if w.w != nil {
		return w.h.Sum() // Not yet closed, return hash of data written so far
	}
	return w.hash
}

// Close releases any resources consumed by the Writer.
//
// Calling Close does not close the wrapped io.Writer originally passed to
// NewWriter.
func (w *Writer) Close() (err error) {
	if w.w == nil {
		// TODO: Consider returning ErrClosed here?
		return nil // Already closed
	}

	// Release the compressor's resources
	err = w.compressor.Close()

	// Save the hash because we're about to throw away the hasher
	w.hash = w.h.Sum()

	// Release references
	w.w = nil // Indicates closed state
	w.compressor = nil
	w.h.Hash = nil

	return
}