doc
This commit is contained in:
parent
ce8ce9f59e
commit
4c7afb8afa
8
main.go
8
main.go
@ -50,9 +50,7 @@ func (r *cmdRootFS) Execute(args []string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() { err = multierr.Append(err, in.Close()) }()
|
||||||
err = multierr.Append(err, in.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
var out *os.File
|
var out *os.File
|
||||||
outf := string(r.PositionalArgs.Outfile)
|
outf := string(r.PositionalArgs.Outfile)
|
||||||
@ -64,9 +62,7 @@ func (r *cmdRootFS) Execute(args []string) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() { err = multierr.Append(err, out.Close()) }()
|
||||||
err = multierr.Append(err, out.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
return rootfs.RootFS(in, out)
|
return rootfs.RootFS(in, out)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
|||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = ["rootfs.go"],
|
srcs = [
|
||||||
|
"doc.go",
|
||||||
|
"rootfs.go",
|
||||||
|
],
|
||||||
importpath = "github.com/motiejus/code/undocker/rootfs",
|
importpath = "github.com/motiejus/code/undocker/rootfs",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = ["@org_uber_go_multierr//:go_default_library"],
|
deps = ["@org_uber_go_multierr//:go_default_library"],
|
||||||
|
70
rootfs/doc.go
Normal file
70
rootfs/doc.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Package rootfs extracts all layers of a Docker container image to a single
|
||||||
|
// tarball. It will go trough all layers in order and copy every file to the
|
||||||
|
// destination archive.
|
||||||
|
//
|
||||||
|
// Except it will also reasonably process those files.
|
||||||
|
//
|
||||||
|
// == Non-directory will be copied only once ==
|
||||||
|
// A non-directory will be copied only once, only from within it's past
|
||||||
|
// occurrence. I.e. if file /a/b was found in layers 0 and 2, only the file
|
||||||
|
// from layer 2 will be used.
|
||||||
|
// Directories will always be copied, even if there are duplicates. This is
|
||||||
|
// to avoid a situation like this:
|
||||||
|
// layer0:
|
||||||
|
// - ./dir/
|
||||||
|
// - ./dir/file
|
||||||
|
// layer1:
|
||||||
|
// - ./dir/
|
||||||
|
// - ./dir/file
|
||||||
|
// In theory, the directory from layer 1 takes precedence, so a tarball like
|
||||||
|
// this could be created:
|
||||||
|
// - ./dir/ (from layer1)
|
||||||
|
// - ./dir/file1 (from layer1)
|
||||||
|
// However, imagine the following:
|
||||||
|
// layer0:
|
||||||
|
// - ./dir/
|
||||||
|
// - ./dir/file1
|
||||||
|
// layer1:
|
||||||
|
// - ./dir/
|
||||||
|
// Then the resulting tarball would have:
|
||||||
|
// - ./dir/file1 (from layer1)
|
||||||
|
// - ./dir/ (from layer0)
|
||||||
|
// Which would mean `untar` would try to untar a file to a directory which
|
||||||
|
// was not yet created. Therefore directories will be copied to the resulting
|
||||||
|
// tar in the order they appear in the layers.
|
||||||
|
//
|
||||||
|
// == Special files: .dockerenv ==
|
||||||
|
//
|
||||||
|
// .dockernv is present in all docker containers, and is likely to remain
|
||||||
|
// such. So if you do `docker export <container>`, the resulting tarball will
|
||||||
|
// have this file. rootfs will not add it. You are welcome to append one
|
||||||
|
// yourself.
|
||||||
|
//
|
||||||
|
// == Special files: opaque files and dirs (.wh.*) ==
|
||||||
|
//
|
||||||
|
// From mount.aufs(8)[1]:
|
||||||
|
//
|
||||||
|
// The whiteout is for hiding files on lower branches. Also it is applied to
|
||||||
|
// stop readdir going lower branches. The latter case is called ‘opaque
|
||||||
|
// directory.’ Any whiteout is an empty file, it means whiteout is just an
|
||||||
|
// mark. In the case of hiding lower files, the name of whiteout is
|
||||||
|
// ‘.wh.<filename>.’ And in the case of stopping readdir, the name is
|
||||||
|
// ‘.wh..wh..opq’. All whiteouts are hardlinked, including ‘<writable branch
|
||||||
|
// top dir>/.wh..wh.aufs`.
|
||||||
|
//
|
||||||
|
// My interpretation:
|
||||||
|
// - a hardlink called `.wh..wh..opq` means that directory contents from the
|
||||||
|
// layers below the mentioned file should be ignored. Higher layers may add
|
||||||
|
// files on top.
|
||||||
|
// - if hardlink `.wh.([^/]+)` is found, $1 should be deleted from the current
|
||||||
|
// and lower layers.
|
||||||
|
//
|
||||||
|
// == Tar format ==
|
||||||
|
//
|
||||||
|
// Since we do care about long filenames and large file sizes (>8GB), we are
|
||||||
|
// using "classic" GNU Tar. However, at least NetBSD pax is known to have
|
||||||
|
// problems reading it[2].
|
||||||
|
//
|
||||||
|
// [1]: https://manpages.debian.org/unstable/aufs-tools/mount.aufs.8.en.html
|
||||||
|
// [2]: https://mgorny.pl/articles/portability-of-tar-features.html
|
||||||
|
package rootfs
|
@ -4,6 +4,7 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -13,10 +14,11 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
_manifestJSON = "manifest.json"
|
_manifestJSON = "manifest.json"
|
||||||
|
_layerSuffix = "/layer.tar"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrBadManifest = errors.New("bad or missing manifest.json")
|
errBadManifest = errors.New("bad or missing manifest.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
type dockerManifestJSON []struct {
|
type dockerManifestJSON []struct {
|
||||||
@ -24,7 +26,7 @@ type dockerManifestJSON []struct {
|
|||||||
Layers []string `json:"Layers"`
|
Layers []string `json:"Layers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rootfs accepts a docker layer tarball and writes it to outfile.
|
// RootFS accepts a docker layer tarball and writes it to outfile.
|
||||||
// 1. create map[string]io.ReadSeeker for each layer.
|
// 1. create map[string]io.ReadSeeker for each layer.
|
||||||
// 2. parse manifest.json and get the layer order.
|
// 2. parse manifest.json and get the layer order.
|
||||||
// 3. go through each layer in order and write:
|
// 3. go through each layer in order and write:
|
||||||
@ -36,9 +38,7 @@ type dockerManifestJSON []struct {
|
|||||||
func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
||||||
tr := tar.NewReader(in)
|
tr := tar.NewReader(in)
|
||||||
tw := tar.NewWriter(out)
|
tw := tar.NewWriter(out)
|
||||||
defer func() {
|
defer func() { err = multierr.Append(err, tw.Close()) }()
|
||||||
err = multierr.Append(err, tw.Close())
|
|
||||||
}()
|
|
||||||
// layerOffsets maps a layer name (a9b123c0daa/layer.tar) to it's offset
|
// layerOffsets maps a layer name (a9b123c0daa/layer.tar) to it's offset
|
||||||
layerOffsets := map[string]int64{}
|
layerOffsets := map[string]int64{}
|
||||||
|
|
||||||
@ -60,9 +60,9 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
case filepath.Clean(hdr.Name) == _manifestJSON:
|
case filepath.Clean(hdr.Name) == _manifestJSON:
|
||||||
dec := json.NewDecoder(tr)
|
dec := json.NewDecoder(tr)
|
||||||
if err := dec.Decode(&manifest); err != nil {
|
if err := dec.Decode(&manifest); err != nil {
|
||||||
return err
|
return fmt.Errorf("parse %s: %w", _manifestJSON, err)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(hdr.Name, "/layer.tar"):
|
case strings.HasSuffix(hdr.Name, _layerSuffix):
|
||||||
here, err := in.Seek(0, io.SeekCurrent)
|
here, err := in.Seek(0, io.SeekCurrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -71,15 +71,11 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(manifest) == 0 {
|
if len(manifest) == 0 || len(layerOffsets) != len(manifest[0].Layers) {
|
||||||
return ErrBadManifest
|
return errBadManifest
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(layerOffsets) != len(manifest[0].Layers) {
|
// enumerate layers the way they would be laid down in the image
|
||||||
return ErrBadManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
// phase 1.5: enumerate layers
|
|
||||||
layers := make([]int64, len(layerOffsets))
|
layers := make([]int64, len(layerOffsets))
|
||||||
for i, name := range manifest[0].Layers {
|
for i, name := range manifest[0].Layers {
|
||||||
layers[i] = layerOffsets[name]
|
layers[i] = layerOffsets[name]
|
||||||
@ -88,8 +84,7 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
// file2layer maps a filename to layer number (index in "layers")
|
// file2layer maps a filename to layer number (index in "layers")
|
||||||
file2layer := map[string]int{}
|
file2layer := map[string]int{}
|
||||||
|
|
||||||
// phase 2: iterate through all layers and save filenames
|
// iterate through all layers and save filenames for all kinds of files.
|
||||||
// for all kinds of files.
|
|
||||||
for i, offset := range layers {
|
for i, offset := range layers {
|
||||||
if _, err := in.Seek(offset, io.SeekStart); err != nil {
|
if _, err := in.Seek(offset, io.SeekStart); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -124,12 +119,21 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// only directories can have multiple entries with the same name.
|
// Only directories can have multiple entries with the same name.
|
||||||
// all other file types cannot.
|
// all other file types cannot.
|
||||||
if hdr.Typeflag != tar.TypeDir && file2layer[hdr.Name] != i {
|
if hdr.Typeflag != tar.TypeDir && file2layer[hdr.Name] != i {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := writeFile(tr, tw, hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(tr *tar.Reader, tw *tar.Writer, hdr *tar.Header) error {
|
||||||
hdrOut := &tar.Header{
|
hdrOut := &tar.Header{
|
||||||
Typeflag: hdr.Typeflag,
|
Typeflag: hdr.Typeflag,
|
||||||
Name: hdr.Name,
|
Name: hdr.Name,
|
||||||
@ -143,7 +147,7 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
ModTime: hdr.ModTime,
|
ModTime: hdr.ModTime,
|
||||||
Devmajor: hdr.Devmajor,
|
Devmajor: hdr.Devmajor,
|
||||||
Devminor: hdr.Devminor,
|
Devminor: hdr.Devminor,
|
||||||
Format: tar.FormatPAX,
|
Format: tar.FormatGNU,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tw.WriteHeader(hdrOut); err != nil {
|
if err := tw.WriteHeader(hdrOut); err != nil {
|
||||||
@ -155,8 +159,6 @@ func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,11 @@ import (
|
|||||||
func TestRootFS(t *testing.T) {
|
func TestRootFS(t *testing.T) {
|
||||||
layer0 := tarball{
|
layer0 := tarball{
|
||||||
dir{name: "/", uid: 0},
|
dir{name: "/", uid: 0},
|
||||||
file{name: "/file", uid: 0, contents: []byte("from 0")},
|
file{name: "/file", uid: 0, contents: bytes.NewBufferString("from 0")},
|
||||||
}
|
}
|
||||||
|
|
||||||
layer1 := tarball{
|
layer1 := tarball{
|
||||||
file{name: "/file", uid: 1, contents: []byte("from 1")},
|
file{name: "/file", uid: 1, contents: bytes.NewBufferString("from 1")},
|
||||||
}
|
}
|
||||||
|
|
||||||
layer2 := tarball{
|
layer2 := tarball{
|
||||||
@ -44,34 +44,95 @@ func TestRootFS(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "basic file overwrite, layer order mixed",
|
name: "basic file overwrite, layer order mixed",
|
||||||
image: tarball{
|
image: tarball{
|
||||||
file{name: "layer1/layer.tar", contents: layer1.bytes(t)},
|
file{name: "layer1/layer.tar", contents: layer1},
|
||||||
file{name: "layer0/layer.tar", contents: layer0.bytes(t)},
|
file{name: "layer0/layer.tar", contents: layer0},
|
||||||
manifest{"layer0/layer.tar", "layer1/layer.tar"},
|
manifest{"layer0/layer.tar", "layer1/layer.tar"},
|
||||||
},
|
},
|
||||||
want: []extractable{
|
want: []extractable{
|
||||||
dir{name: "/", uid: 0},
|
dir{name: "/", uid: 0},
|
||||||
file{name: "/file", uid: 1, contents: []byte("from 1")},
|
file{name: "/file", uid: 1, contents: bytes.NewBufferString("from 1")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "directory overwrite retains original dir",
|
name: "directory overwrite retains original dir",
|
||||||
image: tarball{
|
image: tarball{
|
||||||
file{name: "layer2/layer.tar", contents: layer2.bytes(t)},
|
file{name: "layer2/layer.tar", contents: layer2},
|
||||||
file{name: "layer0/layer.tar", contents: layer0.bytes(t)},
|
file{name: "layer0/layer.tar", contents: layer0},
|
||||||
file{name: "layer1/layer.tar", contents: layer1.bytes(t)},
|
file{name: "layer1/layer.tar", contents: layer1},
|
||||||
manifest{"layer0/layer.tar", "layer1/layer.tar", "layer2/layer.tar"},
|
manifest{"layer0/layer.tar", "layer1/layer.tar", "layer2/layer.tar"},
|
||||||
},
|
},
|
||||||
want: []extractable{
|
want: []extractable{
|
||||||
dir{name: "/", uid: 0},
|
dir{name: "/", uid: 0},
|
||||||
file{name: "/file", uid: 1, contents: []byte("from 1")},
|
file{name: "/file", uid: 1, contents: bytes.NewBufferString("from 1")},
|
||||||
dir{name: "/", uid: 2},
|
dir{name: "/", uid: 2},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "simple whiteout",
|
||||||
|
image: tarball{
|
||||||
|
file{name: "layer0/layer.tar", contents: tarball{
|
||||||
|
file{name: "filea"},
|
||||||
|
file{name: "fileb"},
|
||||||
|
dir{name: "dira"},
|
||||||
|
dir{name: "dirb"},
|
||||||
|
}},
|
||||||
|
file{name: "layer1/layer.tar", contents: tarball{
|
||||||
|
hardlink{name: ".wh.filea"},
|
||||||
|
hardlink{name: ".wh.dira"},
|
||||||
|
}},
|
||||||
|
manifest{"layer0/layer.tar", "layer1/layer.tar"},
|
||||||
|
},
|
||||||
|
want: []extractable{
|
||||||
|
file{name: "fileb"},
|
||||||
|
dir{name: "dirb"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whiteout with override",
|
||||||
|
image: tarball{
|
||||||
|
file{name: "layer0/layer.tar", contents: tarball{
|
||||||
|
file{name: "filea", contents: bytes.NewBufferString("from 0")},
|
||||||
|
}},
|
||||||
|
file{name: "layer1/layer.tar", contents: tarball{
|
||||||
|
hardlink{name: ".wh.filea"},
|
||||||
|
}},
|
||||||
|
file{name: "layer2/layer.tar", contents: tarball{
|
||||||
|
file{name: "filea", contents: bytes.NewBufferString("from 3")},
|
||||||
|
}},
|
||||||
|
manifest{
|
||||||
|
"layer0/layer.tar",
|
||||||
|
"layer1/layer.tar",
|
||||||
|
"layer2/layer.tar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []extractable{
|
||||||
|
file{name: "filea", contents: bytes.NewBufferString("from 3")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "files and directories do not whiteout",
|
||||||
|
image: tarball{
|
||||||
|
file{name: "layer0/layer.tar", contents: tarball{
|
||||||
|
dir{name: "dir"},
|
||||||
|
file{name: "file"},
|
||||||
|
}},
|
||||||
|
file{name: "layer1/layer.tar", contents: tarball{
|
||||||
|
dir{name: ".wh.dir"},
|
||||||
|
file{name: ".wh.file"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
want: []extractable{
|
||||||
|
dir{name: "dir"},
|
||||||
|
dir{name: ".wh.dir"},
|
||||||
|
file{name: "file"},
|
||||||
|
file{name: ".wh.file"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
in := bytes.NewReader(tt.image.bytes(t))
|
in := bytes.NewReader(tt.image.Bytes())
|
||||||
out := bytes.Buffer{}
|
out := bytes.Buffer{}
|
||||||
|
|
||||||
err := RootFS(in, &out)
|
err := RootFS(in, &out)
|
||||||
@ -81,15 +142,31 @@ func TestRootFS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
got := extract(t, &out)
|
got := extract(t, &out)
|
||||||
assert.Equal(t, got, tt.want)
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
type tarrable interface {
|
type tarrer interface {
|
||||||
tar(*testing.T, *tar.Writer)
|
tar(*tar.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type byter interface {
|
||||||
|
Bytes() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type tarball []tarrer
|
||||||
|
|
||||||
|
func (tb tarball) Bytes() []byte {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
tw := tar.NewWriter(&buf)
|
||||||
|
for _, member := range tb {
|
||||||
|
member.tar(tw)
|
||||||
|
}
|
||||||
|
tw.Close()
|
||||||
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractable is an empty interface for comparing extracted outputs in tests.
|
// extractable is an empty interface for comparing extracted outputs in tests.
|
||||||
@ -101,63 +178,70 @@ type dir struct {
|
|||||||
uid int
|
uid int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d dir) tar(t *testing.T, tw *tar.Writer) {
|
func (d dir) tar(tw *tar.Writer) {
|
||||||
t.Helper()
|
|
||||||
hdr := &tar.Header{
|
hdr := &tar.Header{
|
||||||
Typeflag: tar.TypeDir,
|
Typeflag: tar.TypeDir,
|
||||||
Name: d.name,
|
Name: d.name,
|
||||||
Mode: 0644,
|
Mode: 0644,
|
||||||
Uid: d.uid,
|
Uid: d.uid,
|
||||||
}
|
}
|
||||||
require.NoError(t, tw.WriteHeader(hdr))
|
tw.WriteHeader(hdr)
|
||||||
}
|
}
|
||||||
|
|
||||||
type file struct {
|
type file struct {
|
||||||
name string
|
name string
|
||||||
uid int
|
uid int
|
||||||
contents []byte
|
contents byter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f file) tar(t *testing.T, tw *tar.Writer) {
|
func (f file) tar(tw *tar.Writer) {
|
||||||
t.Helper()
|
var contentbytes []byte
|
||||||
|
if f.contents != nil {
|
||||||
|
contentbytes = f.contents.Bytes()
|
||||||
|
}
|
||||||
hdr := &tar.Header{
|
hdr := &tar.Header{
|
||||||
Typeflag: tar.TypeReg,
|
Typeflag: tar.TypeReg,
|
||||||
Name: f.name,
|
Name: f.name,
|
||||||
Mode: 0644,
|
Mode: 0644,
|
||||||
Uid: f.uid,
|
Uid: f.uid,
|
||||||
Size: int64(len(f.contents)),
|
Size: int64(len(contentbytes)),
|
||||||
}
|
}
|
||||||
require.NoError(t, tw.WriteHeader(hdr))
|
tw.WriteHeader(hdr)
|
||||||
_, err := tw.Write(f.contents)
|
tw.Write(contentbytes)
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type manifest []string
|
type manifest []string
|
||||||
|
|
||||||
func (m manifest) tar(t *testing.T, tw *tar.Writer) {
|
func (m manifest) tar(tw *tar.Writer) {
|
||||||
t.Helper()
|
|
||||||
b, err := json.Marshal(dockerManifestJSON{{Layers: m}})
|
b, err := json.Marshal(dockerManifestJSON{{Layers: m}})
|
||||||
require.NoError(t, err)
|
if err != nil {
|
||||||
file{name: "manifest.json", uid: 0, contents: b}.tar(t, tw)
|
panic("testerr")
|
||||||
}
|
|
||||||
|
|
||||||
type tarball []tarrable
|
|
||||||
|
|
||||||
func (tb tarball) bytes(t *testing.T) []byte {
|
|
||||||
t.Helper()
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
tw := tar.NewWriter(&buf)
|
|
||||||
for _, member := range tb {
|
|
||||||
member.tar(t, tw)
|
|
||||||
}
|
}
|
||||||
require.NoError(t, tw.Close())
|
file{
|
||||||
return buf.Bytes()
|
name: "manifest.json",
|
||||||
|
uid: 0,
|
||||||
|
contents: bytes.NewBuffer(b),
|
||||||
|
}.tar(tw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extract(t *testing.T, f io.Reader) []extractable {
|
type hardlink struct {
|
||||||
|
name string
|
||||||
|
uid int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hardlink) tar(tw *tar.Writer) {
|
||||||
|
tw.WriteHeader(&tar.Header{
|
||||||
|
Typeflag: tar.TypeLink,
|
||||||
|
Name: h.name,
|
||||||
|
Mode: 0644,
|
||||||
|
Uid: h.uid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func extract(t *testing.T, r io.Reader) []extractable {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret := []extractable{}
|
ret := []extractable{}
|
||||||
tr := tar.NewReader(f)
|
tr := tar.NewReader(r)
|
||||||
for {
|
for {
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@ -170,9 +254,13 @@ func extract(t *testing.T, f io.Reader) []extractable {
|
|||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
elem = dir{name: hdr.Name, uid: hdr.Uid}
|
elem = dir{name: hdr.Name, uid: hdr.Uid}
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
buf := bytes.Buffer{}
|
f := file{name: hdr.Name, uid: hdr.Uid}
|
||||||
|
if hdr.Size > 0 {
|
||||||
|
var buf bytes.Buffer
|
||||||
io.Copy(&buf, tr)
|
io.Copy(&buf, tr)
|
||||||
elem = file{name: hdr.Name, uid: hdr.Uid, contents: buf.Bytes()}
|
f.contents = &buf
|
||||||
|
}
|
||||||
|
elem = f
|
||||||
}
|
}
|
||||||
ret = append(ret, elem)
|
ret = append(ret, elem)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user