diff --git a/BUILD b/BUILD index 6cf90f7..d2e351c 100644 --- a/BUILD +++ b/BUILD @@ -6,7 +6,7 @@ go_library( importpath = "github.com/motiejus/code/undocker", visibility = ["//visibility:private"], deps = [ - "//src/undocker/rootfs:rootfs_lib", + "//src/undocker/rootfs:go_default_library", "@com_github_jessevdk_go_flags//:go_default_library", ], ) diff --git a/rootfs/BUILD b/rootfs/BUILD index 27a24ff..385fdaa 100644 --- a/rootfs/BUILD +++ b/rootfs/BUILD @@ -1,10 +1,11 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( - name = "rootfs_lib", + name = "go_default_library", srcs = ["rootfs.go"], importpath = "github.com/motiejus/code/undocker/rootfs", visibility = ["//visibility:public"], + deps = ["@org_uber_go_multierr//:go_default_library"], ) go_test( diff --git a/rootfs/rootfs.go b/rootfs/rootfs.go index 038e944..607a619 100644 --- a/rootfs/rootfs.go +++ b/rootfs/rootfs.go @@ -3,16 +3,24 @@ package rootfs import ( "archive/tar" "encoding/json" + "errors" "io" + "path/filepath" "strings" + + "go.uber.org/multierr" ) const ( _manifestJSON = "manifest.json" ) +var ( + ErrBadManifest = errors.New("bad or missing manifest.json") +) + type dockerManifestJSON []struct { - Config string `json:"Config"` + Config string `json:"Config,omitempty"` Layers []string `json:"Layers"` } @@ -25,9 +33,12 @@ type dockerManifestJSON []struct { // I) layer name // II) offset (0 being the first file in the layer) // 4. go through -func RootFS(in io.ReadSeeker, out io.Writer) error { +func RootFS(in io.ReadSeeker, out io.Writer) (err error) { tr := tar.NewReader(in) tw := tar.NewWriter(out) + defer func() { + err = multierr.Append(err, tw.Close()) + }() // layerOffsets maps a layer name (a9b123c0daa/layer.tar) to it's offset layerOffsets := map[string]int64{} @@ -46,7 +57,7 @@ func RootFS(in io.ReadSeeker, out io.Writer) error { } switch { - case hdr.Name == _manifestJSON: + case filepath.Clean(hdr.Name) == _manifestJSON: dec := json.NewDecoder(tr) if err := dec.Decode(&manifest); err != nil { return err @@ -60,6 +71,10 @@ func RootFS(in io.ReadSeeker, out io.Writer) error { } } + if len(manifest) == 0 { + return ErrBadManifest + } + // phase 1.5: enumerate layers layers := make([]int64, len(layerOffsets)) for i, name := range manifest[0].Layers { @@ -136,5 +151,5 @@ func RootFS(in io.ReadSeeker, out io.Writer) error { } } - return tw.Close() + return nil } diff --git a/rootfs/rootfs_test.go b/rootfs/rootfs_test.go index 0c01b50..14490ec 100644 --- a/rootfs/rootfs_test.go +++ b/rootfs/rootfs_test.go @@ -11,19 +11,23 @@ import ( "github.com/stretchr/testify/require" ) -type layers []string - -func (l layers) bytes() []byte { - dockerManifest := dockerManifestJSON{{Layers: l}} - b, err := json.Marshal(dockerManifest) - if err != nil { - panic("panic in a unit test") - } - return b +type tarrable interface { + tar(*testing.T, *tar.Writer) } -type tarrable interface { - tar(*tar.Writer) +type dir struct { + name string + uid int +} + +func (d dir) tar(t *testing.T, tw *tar.Writer) { + t.Helper() + hdr := &tar.Header{ + Typeflag: tar.TypeDir, + Name: d.name, + Uid: d.uid, + } + require.NoError(t, tw.WriteHeader(hdr)) } type file struct { @@ -32,31 +36,39 @@ type file struct { contents []byte } -func (f file) tar(tw *tar.Writer) { - hdr := &tar.Header{Typeflag: tar.TypeReg, Uid: f.uid} - tw.WriteHeader(hdr) - tw.Write(f.contents) +func (f file) tar(t *testing.T, tw *tar.Writer) { + t.Helper() + hdr := &tar.Header{ + Typeflag: tar.TypeReg, + Name: f.name, + Uid: f.uid, + Size: int64(len(f.contents)), + } + require.NoError(t, tw.WriteHeader(hdr)) + _, err := tw.Write(f.contents) + require.NoError(t, err) } -type dir struct { - name string - uid int -} +type manifest []string -func (f dir) tar(tw *tar.Writer) { - hdr := &tar.Header{Typeflag: tar.TypeDir, Uid: f.uid} - tw.WriteHeader(hdr) +func (m manifest) tar(t *testing.T, tw *tar.Writer) { + t.Helper() + b, err := json.Marshal(dockerManifestJSON{{Layers: m}}) + require.NoError(t, err) + file{name: "manifest.json", uid: 0, contents: b}.tar(t, tw) } type tarball []tarrable -func (t tarball) bytes() []byte { +func (tb tarball) bytes(t *testing.T) []byte { + t.Helper() + buf := bytes.Buffer{} tw := tar.NewWriter(&buf) - for _, member := range t { - member.tar(tw) + for _, member := range tb { + member.tar(t, tw) } - tw.Close() + require.NoError(t, tw.Close()) return buf.Bytes() } @@ -81,28 +93,23 @@ func extract(t *testing.T, tarball io.Reader) []file { return ret } -var ( - _layer0 = tarball{ +func TestRootFS(t *testing.T) { + _layer0 := tarball{ dir{name: "/", uid: 0}, file{name: "/file", uid: 1, contents: []byte("from 0")}, } - _layer1 = tarball{ + _layer1 := tarball{ dir{name: "/", uid: 1}, file{name: "/file", uid: 0, contents: []byte("from 1")}, } - _image = tarball{ - file{name: "layer1/layer.tar", contents: _layer1.bytes()}, - file{name: "layer0/layer.tar", contents: _layer0.bytes()}, - file{ - name: "manifest.json", - contents: layers{"layer0/layer.tar", "layer1/layer0.tar"}.bytes(), - }, + _image := tarball{ + file{name: "layer1/layer.tar", contents: _layer1.bytes(t)}, + file{name: "layer0/layer.tar", contents: _layer0.bytes(t)}, + manifest{"layer0/layer0.tar", "layer1/layer.tar"}, } -) -func TestRootFS(t *testing.T) { tests := []struct { name string image tarball @@ -110,7 +117,7 @@ func TestRootFS(t *testing.T) { }{ { name: "empty", - image: tarball{}, + image: tarball{manifest{}}, want: []file{}, }, { @@ -123,11 +130,9 @@ func TestRootFS(t *testing.T) { }, } - assert.Fail(t, "foo") - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - in := bytes.NewReader(tt.image.bytes()) + in := bytes.NewReader(tt.image.bytes(t)) out := bytes.Buffer{} require.NoError(t, RootFS(in, &out))