storage/pkg/chunked/dump/dump.go

241 lines
4.9 KiB
Go

package dump
import (
"bufio"
"fmt"
"io"
"path/filepath"
"strings"
"time"
"unicode"
"github.com/containers/storage/pkg/chunked/internal"
"golang.org/x/sys/unix"
)
const (
ESCAPE_STANDARD = 0
NOESCAPE_SPACE = 1 << iota
ESCAPE_EQUAL
ESCAPE_LONE_DASH
)
func escaped(val string, escape int) string {
noescapeSpace := escape&NOESCAPE_SPACE != 0
escapeEqual := escape&ESCAPE_EQUAL != 0
escapeLoneDash := escape&ESCAPE_LONE_DASH != 0
length := len(val)
if escapeLoneDash && val == "-" {
return fmt.Sprintf("\\x%.2x", val[0])
}
var result string
for i := 0; i < length; i++ {
c := val[i]
hexEscape := false
var special string
switch c {
case '\\':
special = "\\\\"
case '\n':
special = "\\n"
case '\r':
special = "\\r"
case '\t':
special = "\\t"
case '=':
hexEscape = escapeEqual
default:
if noescapeSpace {
hexEscape = !unicode.IsPrint(rune(c))
} else {
hexEscape = !unicode.IsPrint(rune(c)) || unicode.IsSpace(rune(c))
}
}
if special != "" {
result += special
} else if hexEscape {
result += fmt.Sprintf("\\x%.2x", c)
} else {
result += string(c)
}
}
return result
}
func escapedOptional(val string, escape int) string {
if val == "" {
return "-"
}
return escaped(val, escape)
}
func getStMode(mode uint32, typ string) (uint32, error) {
switch typ {
case internal.TypeReg, internal.TypeLink:
mode |= unix.S_IFREG
case internal.TypeChar:
mode |= unix.S_IFCHR
case internal.TypeBlock:
mode |= unix.S_IFBLK
case internal.TypeDir:
mode |= unix.S_IFDIR
case internal.TypeFifo:
mode |= unix.S_IFIFO
case internal.TypeSymlink:
mode |= unix.S_IFLNK
default:
return 0, fmt.Errorf("unknown type %s", typ)
}
return mode, nil
}
func sanitizeName(name string) string {
path := filepath.Clean(name)
if path == "." {
path = "/"
} else if path[0] != '/' {
path = "/" + path
}
return path
}
func dumpNode(out io.Writer, links map[string]int, verityDigests map[string]string, entry *internal.FileMetadata) error {
path := sanitizeName(entry.Name)
if _, err := fmt.Fprint(out, escaped(path, ESCAPE_STANDARD)); err != nil {
return err
}
nlinks := links[entry.Name] + links[entry.Linkname] + 1
link := ""
if entry.Type == internal.TypeLink {
link = "@"
}
rdev := unix.Mkdev(uint32(entry.Devmajor), uint32(entry.Devminor))
entryTime := entry.ModTime
if entryTime == nil {
t := time.Unix(0, 0)
entryTime = &t
}
mode, err := getStMode(uint32(entry.Mode), entry.Type)
if err != nil {
return err
}
if _, err := fmt.Fprintf(out, " %d %s%o %d %d %d %d %d.%d ", entry.Size,
link, mode,
nlinks, entry.UID, entry.GID, rdev,
entryTime.Unix(), entryTime.Nanosecond()); err != nil {
return err
}
var payload string
if entry.Linkname != "" {
if entry.Type == internal.TypeSymlink {
payload = entry.Linkname
} else {
payload = sanitizeName(entry.Linkname)
}
} else {
if len(entry.Digest) > 10 {
d := strings.Replace(entry.Digest, "sha256:", "", 1)
payload = d[:2] + "/" + d[2:]
}
}
if _, err := fmt.Fprintf(out, escapedOptional(payload, ESCAPE_LONE_DASH)); err != nil {
return err
}
/* inline content. */
if _, err := fmt.Fprint(out, " -"); err != nil {
return err
}
/* store the digest. */
if _, err := fmt.Fprint(out, " "); err != nil {
return err
}
digest := verityDigests[payload]
if _, err := fmt.Fprintf(out, escapedOptional(digest, ESCAPE_LONE_DASH)); err != nil {
return err
}
for k, v := range entry.Xattrs {
name := escaped(k, ESCAPE_EQUAL)
value := escaped(v, ESCAPE_EQUAL)
if _, err := fmt.Fprintf(out, " %s=%s", name, value); err != nil {
return err
}
}
if _, err := fmt.Fprint(out, "\n"); err != nil {
return err
}
return nil
}
// GenerateDump generates a dump of the TOC in the same format as `composefs-info dump`
func GenerateDump(tocI interface{}, verityDigests map[string]string) (io.Reader, error) {
toc, ok := tocI.(*internal.TOC)
if !ok {
return nil, fmt.Errorf("invalid TOC type")
}
pipeR, pipeW := io.Pipe()
go func() {
closed := false
w := bufio.NewWriter(pipeW)
defer func() {
if !closed {
w.Flush()
pipeW.Close()
}
}()
links := make(map[string]int)
for _, e := range toc.Entries {
if e.Linkname == "" {
continue
}
if e.Type == internal.TypeSymlink {
continue
}
links[e.Linkname] = links[e.Linkname] + 1
}
if len(toc.Entries) == 0 || (sanitizeName(toc.Entries[0].Name) != "/") {
root := &internal.FileMetadata{
Name: "/",
Type: internal.TypeDir,
Mode: 0o755,
}
if err := dumpNode(w, links, verityDigests, root); err != nil {
pipeW.CloseWithError(err)
closed = true
return
}
}
for _, e := range toc.Entries {
if e.Type == internal.TypeChunk {
continue
}
if err := dumpNode(w, links, verityDigests, &e); err != nil {
pipeW.CloseWithError(err)
closed = true
return
}
}
}()
return pipeR, nil
}