1
16
17
22
23 package estargz
24
25 import (
26 "archive/tar"
27 "bytes"
28 "compress/gzip"
29 "encoding/binary"
30 "encoding/json"
31 "fmt"
32 "hash"
33 "io"
34 "strconv"
35
36 digest "github.com/opencontainers/go-digest"
37 )
38
39 type gzipCompression struct {
40 *GzipCompressor
41 *GzipDecompressor
42 }
43
44 func newGzipCompressionWithLevel(level int) Compression {
45 return &gzipCompression{
46 &GzipCompressor{level},
47 &GzipDecompressor{},
48 }
49 }
50
51 func NewGzipCompressor() *GzipCompressor {
52 return &GzipCompressor{gzip.BestCompression}
53 }
54
55 func NewGzipCompressorWithLevel(level int) *GzipCompressor {
56 return &GzipCompressor{level}
57 }
58
59 type GzipCompressor struct {
60 compressionLevel int
61 }
62
63 func (gc *GzipCompressor) Writer(w io.Writer) (WriteFlushCloser, error) {
64 return gzip.NewWriterLevel(w, gc.compressionLevel)
65 }
66
67 func (gc *GzipCompressor) WriteTOCAndFooter(w io.Writer, off int64, toc *JTOC, diffHash hash.Hash) (digest.Digest, error) {
68 tocJSON, err := json.MarshalIndent(toc, "", "\t")
69 if err != nil {
70 return "", err
71 }
72 gz, _ := gzip.NewWriterLevel(w, gc.compressionLevel)
73 gw := io.Writer(gz)
74 if diffHash != nil {
75 gw = io.MultiWriter(gz, diffHash)
76 }
77 tw := tar.NewWriter(gw)
78 if err := tw.WriteHeader(&tar.Header{
79 Typeflag: tar.TypeReg,
80 Name: TOCTarName,
81 Size: int64(len(tocJSON)),
82 }); err != nil {
83 return "", err
84 }
85 if _, err := tw.Write(tocJSON); err != nil {
86 return "", err
87 }
88
89 if err := tw.Close(); err != nil {
90 return "", err
91 }
92 if err := gz.Close(); err != nil {
93 return "", err
94 }
95 if _, err := w.Write(gzipFooterBytes(off)); err != nil {
96 return "", err
97 }
98 return digest.FromBytes(tocJSON), nil
99 }
100
101
102 func gzipFooterBytes(tocOff int64) []byte {
103 buf := bytes.NewBuffer(make([]byte, 0, FooterSize))
104 gz, _ := gzip.NewWriterLevel(buf, gzip.NoCompression)
105
106
107
108 header := make([]byte, 4)
109 header[0], header[1] = 'S', 'G'
110 subfield := fmt.Sprintf("%016xSTARGZ", tocOff)
111 binary.LittleEndian.PutUint16(header[2:4], uint16(len(subfield)))
112 gz.Header.Extra = append(header, []byte(subfield)...)
113 gz.Close()
114 if buf.Len() != FooterSize {
115 panic(fmt.Sprintf("footer buffer = %d, not %d", buf.Len(), FooterSize))
116 }
117 return buf.Bytes()
118 }
119
120 type GzipDecompressor struct{}
121
122 func (gz *GzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) {
123 return gzip.NewReader(r)
124 }
125
126 func (gz *GzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
127 return parseTOCEStargz(r)
128 }
129
130 func (gz *GzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) {
131 if len(p) != FooterSize {
132 return 0, 0, 0, fmt.Errorf("invalid length %d cannot be parsed", len(p))
133 }
134 zr, err := gzip.NewReader(bytes.NewReader(p))
135 if err != nil {
136 return 0, 0, 0, err
137 }
138 defer zr.Close()
139 extra := zr.Header.Extra
140 si1, si2, subfieldlen, subfield := extra[0], extra[1], extra[2:4], extra[4:]
141 if si1 != 'S' || si2 != 'G' {
142 return 0, 0, 0, fmt.Errorf("invalid subfield IDs: %q, %q; want E, S", si1, si2)
143 }
144 if slen := binary.LittleEndian.Uint16(subfieldlen); slen != uint16(16+len("STARGZ")) {
145 return 0, 0, 0, fmt.Errorf("invalid length of subfield %d; want %d", slen, 16+len("STARGZ"))
146 }
147 if string(subfield[16:]) != "STARGZ" {
148 return 0, 0, 0, fmt.Errorf("STARGZ magic string must be included in the footer subfield")
149 }
150 tocOffset, err = strconv.ParseInt(string(subfield[:16]), 16, 64)
151 if err != nil {
152 return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err)
153 }
154 return tocOffset, tocOffset, 0, nil
155 }
156
157 func (gz *GzipDecompressor) FooterSize() int64 {
158 return FooterSize
159 }
160
161 func (gz *GzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) {
162 return decompressTOCEStargz(r)
163 }
164
165 type LegacyGzipDecompressor struct{}
166
167 func (gz *LegacyGzipDecompressor) Reader(r io.Reader) (io.ReadCloser, error) {
168 return gzip.NewReader(r)
169 }
170
171 func (gz *LegacyGzipDecompressor) ParseTOC(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
172 return parseTOCEStargz(r)
173 }
174
175 func (gz *LegacyGzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, tocSize int64, err error) {
176 if len(p) != legacyFooterSize {
177 return 0, 0, 0, fmt.Errorf("legacy: invalid length %d cannot be parsed", len(p))
178 }
179 zr, err := gzip.NewReader(bytes.NewReader(p))
180 if err != nil {
181 return 0, 0, 0, fmt.Errorf("legacy: failed to get footer gzip reader: %w", err)
182 }
183 defer zr.Close()
184 extra := zr.Header.Extra
185 if len(extra) != 16+len("STARGZ") {
186 return 0, 0, 0, fmt.Errorf("legacy: invalid stargz's extra field size")
187 }
188 if string(extra[16:]) != "STARGZ" {
189 return 0, 0, 0, fmt.Errorf("legacy: magic string STARGZ not found")
190 }
191 tocOffset, err = strconv.ParseInt(string(extra[:16]), 16, 64)
192 if err != nil {
193 return 0, 0, 0, fmt.Errorf("legacy: failed to parse toc offset: %w", err)
194 }
195 return tocOffset, tocOffset, 0, nil
196 }
197
198 func (gz *LegacyGzipDecompressor) FooterSize() int64 {
199 return legacyFooterSize
200 }
201
202 func (gz *LegacyGzipDecompressor) DecompressTOC(r io.Reader) (tocJSON io.ReadCloser, err error) {
203 return decompressTOCEStargz(r)
204 }
205
206 func parseTOCEStargz(r io.Reader) (toc *JTOC, tocDgst digest.Digest, err error) {
207 tr, err := decompressTOCEStargz(r)
208 if err != nil {
209 return nil, "", err
210 }
211 dgstr := digest.Canonical.Digester()
212 toc = new(JTOC)
213 if err := json.NewDecoder(io.TeeReader(tr, dgstr.Hash())).Decode(&toc); err != nil {
214 return nil, "", fmt.Errorf("error decoding TOC JSON: %v", err)
215 }
216 if err := tr.Close(); err != nil {
217 return nil, "", err
218 }
219 return toc, dgstr.Digest(), nil
220 }
221
222 func decompressTOCEStargz(r io.Reader) (tocJSON io.ReadCloser, err error) {
223 zr, err := gzip.NewReader(r)
224 if err != nil {
225 return nil, fmt.Errorf("malformed TOC gzip header: %v", err)
226 }
227 zr.Multistream(false)
228 tr := tar.NewReader(zr)
229 h, err := tr.Next()
230 if err != nil {
231 return nil, fmt.Errorf("failed to find tar header in TOC gzip stream: %v", err)
232 }
233 if h.Name != TOCTarName {
234 return nil, fmt.Errorf("TOC tar entry had name %q; expected %q", h.Name, TOCTarName)
235 }
236 return readCloser{tr, zr.Close}, nil
237 }
238
View as plain text