...

Source file src/github.com/containerd/stargz-snapshotter/estargz/gzip.go

Documentation: github.com/containerd/stargz-snapshotter/estargz

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  /*
    18     Copyright 2019 The Go Authors. All rights reserved.
    19     Use of this source code is governed by a BSD-style
    20     license that can be found in the LICENSE file.
    21  */
    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  // gzipFooterBytes returns the 51 bytes footer.
   102  func gzipFooterBytes(tocOff int64) []byte {
   103  	buf := bytes.NewBuffer(make([]byte, 0, FooterSize))
   104  	gz, _ := gzip.NewWriterLevel(buf, gzip.NoCompression) // MUST be NoCompression to keep 51 bytes
   105  
   106  	// Extra header indicating the offset of TOCJSON
   107  	// https://tools.ietf.org/html/rfc1952#section-2.3.1.1
   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))) // little-endian per RFC1952
   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