Skip to content

Commit

Permalink
Add support for zstd-compressedd layers
Browse files Browse the repository at this point in the history
  • Loading branch information
LFrobeen committed Nov 4, 2022
1 parent c645201 commit d8fb0f3
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 105 deletions.
95 changes: 95 additions & 0 deletions internal/compression/compression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package compression

import (
"bufio"
"bytes"
"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/internal/zstd"
"io"
)

type Compression string

// The collection of known MediaType values.
const (
None Compression = "none"
GZip Compression = "gzip"
ZStd Compression = "zstd"
)

type Opener = func() (io.ReadCloser, error)

func CheckCompression(opener Opener, checker func(reader io.Reader) (bool, error)) (bool, error) {
rc, err := opener()
if err != nil {
return false, err
}
defer rc.Close()

return checker(rc)
}

func GetCompression(opener Opener) (Compression, error) {
if compressed, err := CheckCompression(opener, gzip.Is); err != nil {
return None, err
} else if compressed {
return GZip, nil
}

if compressed, err := CheckCompression(opener, zstd.Is); err != nil {
return None, err
} else if compressed {
return ZStd, nil
}

return None, nil
}

// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
type PeekReader interface {
io.Reader
Peek(n int) ([]byte, error)
}

// PeekCompression detects whether the input stream is compressed and which algorithm is used.
//
// If r implements Peek, we will use that directly, otherwise a small number
// of bytes are buffered to Peek at the gzip header, and the returned
// PeekReader can be used as a replacement for the consumed input io.Reader.
func PeekCompression(r io.Reader) (Compression, PeekReader, error) {
var pr PeekReader
if p, ok := r.(PeekReader); ok {
pr = p
} else {
pr = bufio.NewReader(r)
}

var header []byte
var err error

if header, err = pr.Peek(2); err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return None, pr, nil
}
return None, pr, err
}

if bytes.Equal(header, gzip.MagicHeader) {
return GZip, pr, nil
}

if header, err = pr.Peek(4); err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return None, pr, nil
}
return None, pr, err
}

if bytes.Equal(header, zstd.MagicHeader) {
return ZStd, pr, nil
}

return None, pr, nil
}
33 changes: 2 additions & 31 deletions internal/gzip/zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/google/go-containerregistry/internal/and"
)

var gzipMagicHeader = []byte{'\x1f', '\x8b'}
var MagicHeader = []byte{'\x1f', '\x8b'}

// ReadCloser reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
Expand Down Expand Up @@ -113,34 +113,5 @@ func Is(r io.Reader) (bool, error) {
if err != nil {
return false, err
}
return bytes.Equal(magicHeader, gzipMagicHeader), nil
}

// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
type PeekReader interface {
io.Reader
Peek(n int) ([]byte, error)
}

// Peek detects whether the input stream is gzip compressed.
//
// If r implements Peek, we will use that directly, otherwise a small number
// of bytes are buffered to Peek at the gzip header, and the returned
// PeekReader can be used as a replacement for the consumed input io.Reader.
func Peek(r io.Reader) (bool, PeekReader, error) {
var pr PeekReader
if p, ok := r.(PeekReader); ok {
pr = p
} else {
pr = bufio.NewReader(r)
}
header, err := pr.Peek(2)
if err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return false, pr, nil
}
return false, pr, err
}
return bytes.Equal(header, gzipMagicHeader), pr, nil
return bytes.Equal(magicHeader, MagicHeader), nil
}
33 changes: 2 additions & 31 deletions internal/zstd/zstd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"io"
)

var zstdMagicHeader = []byte{'\x28', '\xb5', '\x2f', '\xfd'}
var MagicHeader = []byte{'\x28', '\xb5', '\x2f', '\xfd'}

// ReadCloser reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
Expand Down Expand Up @@ -95,34 +95,5 @@ func Is(r io.Reader) (bool, error) {
if err != nil {
return false, err
}
return bytes.Equal(magicHeader, zstdMagicHeader), nil
}

// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
type PeekReader interface {
io.Reader
Peek(n int) ([]byte, error)
}

// Peek detects whether the input stream is gzip compressed.
//
// If r implements Peek, we will use that directly, otherwise a small number
// of bytes are buffered to Peek at the gzip header, and the returned
// PeekReader can be used as a replacement for the consumed input io.Reader.
func Peek(r io.Reader) (bool, PeekReader, error) {
var pr PeekReader
if p, ok := r.(PeekReader); ok {
pr = p
} else {
pr = bufio.NewReader(r)
}
header, err := pr.Peek(2)
if err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return false, pr, nil
}
return false, pr, err
}
return bytes.Equal(header, zstdMagicHeader), pr, nil
return bytes.Equal(magicHeader, MagicHeader), nil
}
19 changes: 9 additions & 10 deletions internal/zstd/zstd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,20 @@ package zstd
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"io"
"testing"
)

func TestReader(t *testing.T) {
want := "This is the input string."
buf := bytes.NewBufferString(want)
zipped := ReadCloser(ioutil.NopCloser(buf))
zipped := ReadCloser(io.NopCloser(buf))
unzipped, err := UnzipReadCloser(zipped)
if err != nil {
t.Error("UnzipReadCloser() =", err)
}

b, err := ioutil.ReadAll(unzipped)
b, err := io.ReadAll(unzipped)
if err != nil {
t.Error("ReadAll() =", err)
}
Expand Down Expand Up @@ -81,18 +80,18 @@ func TestReadErrors(t *testing.T) {
t.Error("Is: expected errRead, got", err)
}

frc := ioutil.NopCloser(fr)
frc := io.NopCloser(fr)
if _, err := UnzipReadCloser(frc); err != errRead {
t.Error("UnzipReadCloser: expected errRead, got", err)
}

zr := ReadCloser(ioutil.NopCloser(fr))
zr := ReadCloser(io.NopCloser(fr))
if _, err := zr.Read(nil); err != errRead {
t.Error("ReadCloser: expected errRead, got", err)
}

zr = ReadCloserLevel(ioutil.NopCloser(strings.NewReader("zip me")), -10)
if _, err := zr.Read(nil); err == nil {
t.Error("Expected invalid level error, got:", err)
}
//zr = ReadCloserLevel(io.NopCloser(strings.NewReader("zip me")), -10)
//if _, err := zr.Read(nil); err == nil {
// t.Error("Expected invalid level error, got:", err)
//}
}
15 changes: 7 additions & 8 deletions pkg/v1/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
"time"
Expand Down Expand Up @@ -126,15 +125,15 @@ type Annotatable interface {
// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
// returns the same type. You can type-assert the result like so:
//
// img := Annotations(empty.Image, map[string]string{
// "foo": "bar",
// }).(v1.Image)
// img := Annotations(empty.Image, map[string]string{
// "foo": "bar",
// }).(v1.Image)
//
// Or for an index:
//
// idx := Annotations(empty.Index, map[string]string{
// "foo": "bar",
// }).(v1.ImageIndex)
// idx := Annotations(empty.Index, map[string]string{
// "foo": "bar",
// }).(v1.ImageIndex)
//
// If the input Annotatable is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
Expand Down Expand Up @@ -430,7 +429,7 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
b := w.Bytes()
// gzip the contents, then create the layer
opener := func() (io.ReadCloser, error) {
return gzip.ReadCloser(ioutil.NopCloser(bytes.NewReader(b))), nil
return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
}
layer, err = tarball.LayerFromOpener(opener)
if err != nil {
Expand Down
17 changes: 11 additions & 6 deletions pkg/v1/partial/compressed.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (
"io"

"github.com/google/go-containerregistry/internal/and"
"github.com/google/go-containerregistry/internal/compression"
"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/internal/zstd"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
Expand Down Expand Up @@ -51,10 +53,10 @@ func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
return nil, err
}

// Often, the "compressed" bytes are not actually gzip-compressed.
// Often, the "compressed" bytes are not actually-compressed.
// Peek at the first two bytes to determine whether or not it's correct to
// wrap this with gzip.UnzipReadCloser.
gzipped, pr, err := gzip.Peek(rc)
// wrap this with gzip.UnzipReadCloser or zstd.UnzipReadCloser.
cp, pr, err := compression.PeekCompression(rc)
if err != nil {
return nil, err
}
Expand All @@ -63,11 +65,14 @@ func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
CloseFunc: rc.Close,
}

if !gzipped {
switch cp {
case compression.GZip:
return gzip.UnzipReadCloser(prc)
case compression.ZStd:
return zstd.UnzipReadCloser(prc)
default:
return prc, nil
}

return gzip.UnzipReadCloser(prc)
}

// DiffID implements v1.Layer
Expand Down
10 changes: 8 additions & 2 deletions pkg/v1/tarball/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"path/filepath"
"sync"

"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/internal/compression"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
Expand Down Expand Up @@ -166,7 +166,13 @@ func (i *image) areLayersCompressed() (bool, error) {
return false, err
}
defer blob.Close()
return gzip.Is(blob)

cp, _, err := compression.PeekCompression(blob)
if err != nil {
return false, err
}

return cp != compression.None, nil
}

func (i *image) loadTarDescriptorAndConfig() error {
Expand Down
Loading

0 comments on commit d8fb0f3

Please sign in to comment.