dive-zfs/image/image.go
William Murphy bbe7d436ce Improve bounds checking and EOF handling (#9)
Some fields that were never empty in our original test image
can apparently be empty in images that one finds in the wild. This
adds appropriate checking so that blank file lists and missing
image IDs no longer cause crashes.
2018-07-04 11:32:11 -04:00

237 lines
4.9 KiB
Go

package image
import (
"archive/tar"
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
humanize "github.com/dustin/go-humanize"
"github.com/wagoodman/docker-image-explorer/filetree"
"golang.org/x/net/context"
)
const (
LayerFormat = "%-25s %7s %s"
)
func check(e error) {
if e != nil {
panic(e)
}
}
type ImageManifest struct {
ConfigPath string `json:"Config"`
RepoTags []string `json:"RepoTags"`
LayerTarPaths []string `json:"Layers"`
}
func NewManifest(reader *tar.Reader, header *tar.Header) ImageManifest {
size := header.Size
manifestBytes := make([]byte, size)
_, err := reader.Read(manifestBytes)
if err != nil && err != io.EOF {
panic(err)
}
var m []ImageManifest
err = json.Unmarshal(manifestBytes, &m)
if err != nil {
panic(err)
}
return m[0]
}
type Layer struct {
TarPath string
History types.ImageHistory
}
func (layer *Layer) Id() string {
rangeBound := 25
if length := len(layer.History.ID); length < 25 {
rangeBound = length
}
id := layer.History.ID[0:rangeBound]
if len(layer.History.Tags) > 0 {
id = "[" + strings.Join(layer.History.Tags, ",") + "]"
}
return id
}
func (layer *Layer) String() string {
return fmt.Sprintf(LayerFormat, layer.Id(), humanize.Bytes(uint64(layer.History.Size)), strings.TrimPrefix(layer.History.CreatedBy, "/bin/sh -c "))
}
func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) {
var manifest ImageManifest
var layerMap = make(map[string]*filetree.FileTree)
var trees []*filetree.FileTree = make([]*filetree.FileTree, 0)
// save this image to disk temporarily to get the content info
imageTarPath, tmpDir := saveImage(imageID)
defer os.RemoveAll(tmpDir)
// read through the image contents and build a tree
tarFile, err := os.Open(imageTarPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer tarFile.Close()
tarReader := tar.NewReader(tarFile)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
if name == "manifest.json" {
manifest = NewManifest(tarReader, header)
}
switch header.Typeflag {
case tar.TypeDir:
continue
case tar.TypeReg:
if strings.HasSuffix(name, "layer.tar") {
tree := filetree.NewFileTree()
tree.Name = name
fileInfos := getFileList(tarReader, header)
for _, element := range fileInfos {
tree.AddPath(element.Path, element)
}
layerMap[tree.Name] = tree
}
default:
fmt.Printf("ERRG: unknown tar entry: %v: %s\n", header.Typeflag, name)
}
}
// build the content tree
for _, treeName := range manifest.LayerTarPaths {
trees = append(trees, layerMap[treeName])
}
// get the history of this image
ctx := context.Background()
dockerClient, err := client.NewEnvClient()
if err != nil {
panic(err)
}
history, err := dockerClient.ImageHistory(ctx, imageID)
// build the layers array
layers := make([]*Layer, len(history)-1)
for idx := 0; idx < len(layers); idx++ {
layers[idx] = new(Layer)
layers[idx].History = history[idx]
if len(manifest.LayerTarPaths) > idx {
layers[idx].TarPath = manifest.LayerTarPaths[idx]
}
}
return layers, trees
}
func saveImage(imageID string) (string, string) {
ctx := context.Background()
dockerClient, err := client.NewEnvClient()
if err != nil {
panic(err)
}
readCloser, err := dockerClient.ImageSave(ctx, []string{imageID})
check(err)
defer readCloser.Close()
tmpDir, err := ioutil.TempDir("", "docker-image-explorer")
check(err)
imageTarPath := filepath.Join(tmpDir, "image.tar")
imageFile, err := os.Create(imageTarPath)
check(err)
defer func() {
if err := imageFile.Close(); err != nil {
panic(err)
}
}()
imageWriter := bufio.NewWriter(imageFile)
buf := make([]byte, 1024)
for {
n, err := readCloser.Read(buf)
if err != nil && err != io.EOF {
panic(err)
}
if n == 0 {
break
}
if _, err := imageWriter.Write(buf[:n]); err != nil {
panic(err)
}
}
if err = imageWriter.Flush(); err != nil {
panic(err)
}
return imageTarPath, tmpDir
}
func getFileList(parentReader *tar.Reader, header *tar.Header) []filetree.FileInfo {
var files []filetree.FileInfo
var tarredBytes = make([]byte, header.Size)
_, err := parentReader.Read(tarredBytes)
if err != nil && err != io.EOF {
panic(err)
}
reader := bytes.NewReader(tarredBytes)
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
name := header.Name
switch header.Typeflag {
case tar.TypeXGlobalHeader:
fmt.Printf("ERRG: XGlobalHeader: %v: %s\n", header.Typeflag, name)
case tar.TypeXHeader:
fmt.Printf("ERRG: XHeader: %v: %s\n", header.Typeflag, name)
default:
files = append(files, filetree.NewFileInfo(tarReader, header, name))
}
}
return files
}