Found some good squashfs documentation so I can start work

This commit is contained in:
Caleb Gardner
2020-11-10 03:48:03 -06:00
parent c00ec36268
commit 40541575f8
6 changed files with 2247 additions and 0 deletions
+618
View File
@@ -0,0 +1,618 @@
package squashfs
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"golang.org/x/xerrors"
)
type Reader struct {
r io.ReaderAt
super superblock
}
func NewReader(r io.ReaderAt) (*Reader, error) {
var sb superblock
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(sb))), binary.LittleEndian, &sb); err != nil {
return nil, fmt.Errorf("reading superblock: %v", err)
}
if got, want := sb.Magic, uint32(magic); got != want {
return nil, fmt.Errorf("invalid magic (not a SquashFS image?): got %x, want %x", got, want)
}
//log.Printf("superblock: %+v", sb)
return &Reader{
r: r,
super: sb,
}, nil
}
// TODO: maybe mmap instead of seeking?
func (r *Reader) inode(i Inode) (blockoffset int64, offset int64) {
return int64(i >> 16), int64(i & 0xFFFF)
}
type blockReader struct {
r io.ReadSeeker
lenBuf [2]byte
buf []byte
i int64
off int64 // TODO: remove this once using mmap
}
func (br *blockReader) Read(p []byte) (n int, err error) {
if br.i >= int64(len(br.buf)) {
br.i = 0
if _, err := io.ReadFull(br.r, br.lenBuf[:]); err != nil {
return 0, err
}
l := binary.LittleEndian.Uint16(br.lenBuf[:])
//uncompressed := l&0x8000 > 0
l &= 0x7FFF
//log.Printf("block of len %d, uncompressed: %v", l, uncompressed)
if int(l) > cap(br.buf) {
br.buf = make([]byte, int(l))
}
br.buf = br.buf[:l]
if _, err := io.ReadFull(br.r, br.buf); err != nil {
return 0, err
}
//log.Printf("(retry) n = %v, err = %v", n, err)
}
n = copy(p, br.buf[br.i:])
br.i += int64(n)
return n, err
}
func (br *blockReader) Close() error {
blockReaderPool.Put(br)
return nil
}
var blockReaderPool = sync.Pool{
New: func() interface{} {
return &blockReader{
buf: make([]byte, 0, metadataBlockSize),
}
},
}
func (r *Reader) blockReader(blockoffset, offset int64) (io.ReadCloser, error) {
//log.Printf("blockoffset %v (%x), offset %v (%x)", blockoffset, blockoffset, offset, offset)
br := blockReaderPool.Get().(*blockReader)
br.buf = br.buf[:0]
br.r = io.NewSectionReader(r.r, blockoffset, 5500*1024*1024) // TODO: correct limit? can we use IntMax
br.off = blockoffset
br.i = 0
//log.Printf("discarding %d bytes", offset)
for n := int64(0); n < offset; {
remaining := offset - n
if remaining > metadataBlockSize {
remaining = metadataBlockSize
}
nn, err := br.Read(br.buf[:remaining])
if err != nil {
return nil, err
}
n += int64(nn)
}
return br, nil
}
// TODO: define an inode type to use instead of interface{}?
func (r *Reader) readInode(i Inode) (interface{}, error) {
blockoffset, offset := r.inode(i)
br, err := r.blockReader(r.super.InodeTableStart+blockoffset, offset)
if err != nil {
fmt.Println("oops! ", err)
return nil, err
}
defer br.Close()
fmt.Println("Hello There")
// We need the inode type before we know which type to pass to binary.Read,
// so we need to read it twice:
var inodeType uint16
typeBuf := bytes.NewBuffer(make([]byte, 0, binary.Size(inodeType)))
if err := binary.Read(io.TeeReader(br, typeBuf), binary.LittleEndian, &inodeType); err != nil {
return nil, err
}
br = ioutil.NopCloser(io.MultiReader(typeBuf, br))
// var ih inodeHeader
// if err := binary.Read(br, binary.LittleEndian, &ih); err != nil {
// return err
// }
// //log.Printf("ih: %+v", ih)
//log.Printf("inode type: %v", inodeType)
switch inodeType {
case dirType:
var di dirInodeHeader
if err := binary.Read(br, binary.LittleEndian, &di); err != nil {
return nil, err
}
return di, nil
case fileType:
var ri regInodeHeader
if err := binary.Read(br, binary.LittleEndian, &ri); err != nil {
return nil, err
}
return ri, nil
case symlinkType:
var si symlinkInodeHeader
if err := binary.Read(br, binary.LittleEndian, &si); err != nil {
return nil, err
}
return si, nil
case ldirType:
var di ldirInodeHeader
if err := binary.Read(br, binary.LittleEndian, &di); err != nil {
return nil, err
}
return di, nil
case lregType:
var di lregInodeHeader
if err := binary.Read(br, binary.LittleEndian, &di); err != nil {
return nil, err
}
return di, nil
// TODO:
// blkdevType
// chrdevType
// fifoType
// socketType
// // The larger types are used for e.g. sparse files, xattrs, etc.
// ldirType
// lsymlinkType
// lblkdevType
// lchrdevType
// lfifoType
// lsocketType
}
return nil, fmt.Errorf("unknown inode type %d", inodeType)
}
func (r *Reader) RootInode() Inode {
return r.super.RootInode
}
func (r *Reader) Stat(name string, i Inode) (os.FileInfo, error) {
inode, err := r.readInode(i)
if err != nil {
return nil, err
}
//log.Printf("i %d, inode: %T, %+v", i, inode, inode)
switch x := inode.(type) {
case dirInodeHeader:
return &FileInfo{
name: name,
size: int64(x.FileSize),
mode: os.ModeDir | os.FileMode(x.Mode),
modTime: time.Unix(int64(x.Mtime), 0),
Inode: i,
}, nil
case ldirInodeHeader:
return &FileInfo{
name: name,
size: int64(x.FileSize),
mode: os.ModeDir | os.FileMode(x.Mode),
modTime: time.Unix(int64(x.Mtime), 0),
Inode: i,
}, nil
case regInodeHeader:
mode := os.FileMode(x.Mode & 0777)
if x.Mode&syscall.S_ISUID != 0 {
mode |= os.ModeSetuid
}
return &FileInfo{
name: name,
size: int64(x.FileSize),
mode: mode,
modTime: time.Unix(int64(x.Mtime), 0),
Inode: i,
}, nil
case lregInodeHeader:
mode := os.FileMode(x.Mode & 0777)
if x.Mode&syscall.S_ISUID != 0 {
mode |= os.ModeSetuid
}
return &FileInfo{
name: name,
size: int64(x.FileSize),
mode: mode,
modTime: time.Unix(int64(x.Mtime), 0),
Inode: i,
}, nil
case symlinkInodeHeader:
return &FileInfo{
name: name,
size: int64(x.SymlinkSize),
mode: os.ModeSymlink | os.FileMode(x.Mode),
modTime: time.Unix(int64(x.Mtime), 0),
Inode: i,
}, nil
}
return nil, fmt.Errorf("unknown inode type %T", inode)
}
func (r *Reader) ReadLink(i Inode) (string, error) {
// TODO: reduce code duplication with readInode
blockoffset, offset := r.inode(i)
br, err := r.blockReader(r.super.InodeTableStart+blockoffset, offset)
if err != nil {
return "", err
}
defer br.Close()
// We need the inode type before we know which type to pass to binary.Read,
// so we need to read it twice:
var inodeType uint16
typeBuf := bytes.NewBuffer(make([]byte, 0, binary.Size(inodeType)))
if err := binary.Read(io.TeeReader(br, typeBuf), binary.LittleEndian, &inodeType); err != nil {
return "", err
}
br = ioutil.NopCloser(io.MultiReader(typeBuf, br))
if inodeType != symlinkType {
return "", fmt.Errorf("invalid inode type: got %d instead of symlink", inodeType)
}
var si symlinkInodeHeader
if err := binary.Read(br, binary.LittleEndian, &si); err != nil {
return "", err
}
// Assumption: r.r is positioned right after the inode
buf := make([]byte, si.SymlinkSize)
if _, err := io.ReadFull(br, buf); err != nil {
return "", err
}
return string(buf), nil
}
func (r *Reader) FileReader(inode Inode) (*io.SectionReader, error) {
//log.Printf("Readfile(%v)", inode)
i, err := r.readInode(inode)
if err != nil {
return nil, err
}
//log.Printf("i: %+v", i)
// TODO(compression): read the blocksizes to read compressed blocks
switch ri := i.(type) {
case regInodeHeader:
off := int64(ri.StartBlock) + int64(ri.Offset)
return io.NewSectionReader(r.r, off, int64(ri.FileSize)), nil
case lregInodeHeader:
off := int64(ri.StartBlock) + int64(ri.Offset)
return io.NewSectionReader(r.r, off, int64(ri.FileSize)), nil
default:
return nil, fmt.Errorf("BUG: non-file inode type")
}
}
type FileNotFoundError struct {
path string
}
func (e *FileNotFoundError) Error() string {
return fmt.Sprintf("%q not found", e.path)
}
func (r *Reader) lookupComponent(parent Inode, component string) (Inode, error) {
rfis, err := r.readdir(parent, false)
if err != nil {
return 0, err
}
for _, rfi := range rfis {
if rfi.Name() == component {
return rfi.Sys().(*FileInfo).Inode, nil
}
}
return 0, &FileNotFoundError{path: component}
}
func (r *Reader) lookupPath(path string, followSymlink bool) (Inode, error) {
inode := r.RootInode()
parts := strings.Split(path, "/")
for idx, part := range parts {
var err error
inode, err = r.lookupComponent(inode, part)
if err != nil {
if _, ok := err.(*FileNotFoundError); ok {
return 0, &FileNotFoundError{path: path}
}
return 0, err
}
if !followSymlink {
continue
}
i, err := r.readInode(inode)
if err != nil {
return 0, xerrors.Errorf("Stat(%d): %v", inode, err)
}
if _, ok := i.(symlinkInodeHeader); ok {
target, err := r.ReadLink(inode)
if err != nil {
return 0, err
}
//log.Printf("component %q (full: %q) resolved to %q", part, parts[:idx+1], target)
target = filepath.Clean(filepath.Join(append(parts[:idx] /* parent */, target)...))
//log.Printf("-> %s", target)
i, err := r.LookupPath(target)
if err != nil {
return 0, err
}
inode = i
}
}
return inode, nil
}
func (r *Reader) LookupPath(path string) (Inode, error) {
return r.lookupPath(path, true)
}
// LlookupPath is like LookupPath, but does not follow symbolic links, i.e. will
// instead return the inode of the link itself.
func (r *Reader) LlookupPath(path string) (Inode, error) {
return r.lookupPath(path, false)
}
func (r *Reader) Readdir(dirInode Inode) ([]os.FileInfo, error) {
return r.readdir(dirInode, true)
}
// Like Readdir, but does not call Stat on each file. The returned FileInfo
// structs will still have a filled in Name, partly filled in Mode, and filled
// in Inode.
func (r *Reader) ReaddirNoStat(dirInode Inode) ([]os.FileInfo, error) {
return r.readdir(dirInode, false)
}
var nameBufPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func (r *Reader) readdir(dirInode Inode, stat bool) ([]os.FileInfo, error) {
//log.Printf("Readdir(%v (%x))", dirInode, dirInode)
i, err := r.readInode(dirInode)
fmt.Println("Yodle")
if err != nil {
return nil, err
}
var (
startBlock int64
fileSize int64
offset int64
)
switch x := i.(type) {
case dirInodeHeader:
startBlock = int64(x.StartBlock)
fileSize = int64(x.FileSize)
offset = int64(x.Offset)
case ldirInodeHeader:
startBlock = int64(x.StartBlock)
fileSize = int64(x.FileSize)
offset = int64(x.Offset)
default:
return nil, fmt.Errorf("unknown directory inode type %T", i)
}
br, err := r.blockReader(r.super.DirectoryTableStart+startBlock, offset)
if err != nil {
return nil, err
}
defer br.Close()
// See also https://elixir.bootlin.com/linux/v4.18.9/source/fs/squashfs/dir.c#L63
limit := fileSize - int64(len(".")) - int64(len(".."))
br = ioutil.NopCloser(io.LimitReader(br, limit))
var fis []os.FileInfo
var dh dirHeader
var de dirEntry
var dhBuf [12]byte
var deBuf [8]byte
nameBuf := nameBufPool.Get().(*bytes.Buffer)
defer nameBufPool.Put(nameBuf)
for {
if _, err := io.ReadFull(br, dhBuf[:]); err != nil {
if err == io.EOF {
return fis, nil
}
return nil, err
}
dh.Unmarshal(dhBuf[:])
dh.Count++ // SquashFS stores count-1
//log.Printf("dh: %+v", dh)
for i := 0; i < int(dh.Count); i++ {
if _, err := io.ReadFull(br, deBuf[:]); err != nil {
return nil, err
}
de.Unmarshal(deBuf[:])
de.Size++ // SquashFS stores size-1
//log.Printf("de: %+v", de)
nameBuf.Reset()
nameBuf.Grow(int(de.Size))
nb := nameBuf.Bytes()[:de.Size]
if _, err := io.ReadFull(br, nb); err != nil {
return nil, err
}
name := string(nb)
//log.Printf("name: %q", string(name))
var fi os.FileInfo
if stat {
var err error
fi, err = r.Stat(name, Inode(int64(dh.StartBlock)<<16|int64(de.Offset)))
if err != nil {
return nil, err
}
} else {
ffi := &FileInfo{
name: name,
Inode: Inode(int64(dh.StartBlock)<<16 | int64(de.Offset)),
}
switch de.EntryType {
case dirType, ldirType:
ffi.mode |= os.ModeDir
case symlinkType, lsymlinkType:
ffi.mode |= os.ModeSymlink
}
fi = ffi
}
fis = append(fis, fi)
}
}
return fis, nil
}
type FileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
Inode Inode
}
func (fi *FileInfo) Name() string { return fi.name }
func (fi *FileInfo) Size() int64 { return fi.size }
func (fi *FileInfo) Mode() os.FileMode { return fi.mode }
func (fi *FileInfo) IsDir() bool { return fi.mode.IsDir() }
func (fi *FileInfo) ModTime() time.Time { return fi.modTime }
func (fi *FileInfo) Sys() interface{} { return fi }
func (r *Reader) readXattr(tableHeader xattrTableHeader, id xattrId) (*Xattr, error) {
blockoffset, offset := r.inode(Inode(id.Xattr))
br, err := r.blockReader(int64(tableHeader.XattrTableStart)+blockoffset, offset)
if err != nil {
return nil, err
}
defer br.Close()
var typ, nameSize uint16
if err := binary.Read(br, binary.LittleEndian, &typ); err != nil {
return nil, err
}
if err := binary.Read(br, binary.LittleEndian, &nameSize); err != nil {
return nil, err
}
log.Printf("type = %v, nameSize = %v", typ, nameSize)
name := make([]byte, nameSize)
if _, err := io.ReadFull(br, name); err != nil {
return nil, err
}
log.Printf("name = %v", string(name))
var valSize uint32
if err := binary.Read(br, binary.LittleEndian, &valSize); err != nil {
return nil, err
}
val := make([]byte, valSize)
if _, err := io.ReadFull(br, val); err != nil {
return nil, err
}
log.Printf("val = %x", val)
return &Xattr{
Type: typ,
FullName: xattrPrefix[int(typ)] + string(name),
Value: val,
}, nil
}
func (r *Reader) ReadXattrs(inode Inode) ([]Xattr, error) {
//log.Printf("Readdir(%v (%x))", dirInode, dirInode)
i, err := r.readInode(inode)
if err != nil {
return nil, err
}
var xid uint32
switch x := i.(type) {
case regInodeHeader,
dirInodeHeader,
ldirInodeHeader,
symlinkInodeHeader:
return nil, nil // no extended attributes
case lregInodeHeader:
if x.Xattr == invalidXattr {
return nil, nil // file has no extended attributes
}
xid = x.Xattr
default:
return nil, fmt.Errorf("unknown inode type %T", i)
}
const idEntriesPerBlock = 512 // = 8192 / 16 /* sizeof(xattrId) */
block := xid / idEntriesPerBlock
offset := (xid % idEntriesPerBlock) * 16
log.Printf("xattr id %d, block %d, offset %d", xid, block, offset)
log.Printf("r.super.XattrIdTableStart = 0x%x, r.super.XattrIdTableStart = %v", r.super.XattrIdTableStart, r.super.XattrIdTableStart)
br := ioutil.NopCloser(io.NewSectionReader(r.r, r.super.XattrIdTableStart, int64(16 /* sizeof(xattrTableHeader) */ +(block+1)*4 /* sizeof(uint32) */)))
var tableHeader xattrTableHeader
if err := binary.Read(br, binary.LittleEndian, &tableHeader); err != nil {
return nil, err
}
// index starts here
if _, err := io.CopyN(ioutil.Discard, br, int64(block*4 /* sizeof(uint32) */)); err != nil {
return nil, err
}
var blockOffset uint32
if err := binary.Read(br, binary.LittleEndian, &blockOffset); err != nil {
return nil, err
}
log.Printf("blockOffset = 0x%x (%d)", blockOffset, blockOffset)
br, err = r.blockReader(int64(blockOffset), int64(offset))
if err != nil {
return nil, err
}
defer br.Close()
var id xattrId
if err := binary.Read(br, binary.LittleEndian, &id); err != nil {
return nil, err
}
log.Printf("id: %+v", id)
log.Printf("tableHeader: %+v (start 0x%x)", tableHeader, tableHeader.XattrTableStart)
var xattrs []Xattr
for i := 0; i < int(id.Count); i++ {
xattr, err := r.readXattr(tableHeader, id)
if err != nil {
return nil, err
}
xattrs = append(xattrs, *xattr)
}
return xattrs, nil
}
+373
View File
@@ -0,0 +1,373 @@
package squashfs
import (
"crypto/md5"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func cmpFileInfo(got os.FileInfo, want FileInfo) error {
if got, want := got.Name(), want.name; got != want {
return fmt.Errorf("unexpected file name: got %q, want %q", got, want)
}
if got, want := got.Size(), want.size; got != want {
return fmt.Errorf("unexpected size: got %d, want %d", got, want)
}
if got, want := got.IsDir(), want.mode.IsDir(); got != want {
return fmt.Errorf("IsDir: got %v, want %v", got, want)
}
// TODO: re-enable when its no longer just a change detector
// if got, want := got.ModTime(), want.modTime; !got.Equal(want) {
// return fmt.Errorf("IsDir: got %v, want %v", got, want)
// }
return nil
}
func TestReaddir(t *testing.T) {
t.Parallel()
// TODO: ship testdata files generated by mksquashfs
pwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
fmt.Println(pwd + "/testdata/testing.squashfs")
f, err := os.Open(pwd + "/testdata/testing.squashfs")
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
fis, err := rd.Readdir(rd.RootInode())
if err != nil {
t.Fatal(err)
}
for _, fi := range fis {
fmt.Println(fi.Name())
}
rdr, err := rd.FileReader(fis[0].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
err = os.Remove(pwd + "/testdata/Magisk.zip")
if !os.IsNotExist(err) && err != nil {
t.Fatal(err)
}
// rdrzlib, err := zlib.NewReader(rdr)
magFil, err := os.Create(pwd + "/testdata/Magisk.zip")
io.Copy(magFil, rdr)
if got, want := len(fis), 3; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[0], FileInfo{
name: "bin",
size: 26,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1581275131, 0), // stat -c %Y /ro/ack-amd64-2.24/bin
}); err != nil {
t.Fatal(err)
}
if err := cmpFileInfo(fis[1], FileInfo{
name: "lib",
size: 3,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1581275131, 0), // stat -c %Y /ro/ack-amd64-2.24/lib
}); err != nil {
t.Fatal(err)
}
if err := cmpFileInfo(fis[2], FileInfo{
name: "out",
size: 48,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1581275130, 0), // stat -c %Y /ro/ack-amd64-2.24/out
}); err != nil {
t.Fatal(err)
}
fis, err = rd.Readdir(fis[0].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 1; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[0], FileInfo{
name: "ack",
size: 38400,
mode: 0755,
modTime: time.Unix(1581275131, 0), // stat -c %Y /ro/ack-amd64-2.24/bin/ack
}); err != nil {
t.Fatal(err)
}
}
// TestReaddirSmoke is a smoke-test, reading the root directories of SquashFS
// images which are known to trigger code paths which were buggy.
func TestReaddirSmoke(t *testing.T) {
t.Parallel()
for _, fn := range []string{
// bash exercises the code path where an inode is split across metadata
// blocks.
"/home/michael/distri/_build/distri/pkg/bash-amd64-5.0-4.squashfs",
// cmake exercises the code path where the root directory entries are
// located outside of the first block.
"/home/michael/distri/_build/distri/pkg/cmake-amd64-3.12.4-8.squashfs",
} {
// TODO: ship testdata files generated by mksquashfs
f, err := os.Open(fn)
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
fis, err := rd.Readdir(rd.RootInode())
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 4; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
}
}
func TestReaddirEmpty(t *testing.T) {
t.Parallel()
// TODO: ship testdata files generated by mksquashfs
f, err := os.Open("/home/michael/distri/_build/distri/pkg/zlib-amd64-1.2.11-4.squashfs")
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
fis, err := rd.Readdir(rd.RootInode())
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 4; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[0], FileInfo{
name: "bin",
size: 3,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1583085224, 0), // stat -c %Y /ro/zlib-amd64-1.2.11/bin
}); err != nil {
t.Fatal(err)
}
fis, err = rd.Readdir(fis[0].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 0; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
}
func TestReaddirSymlink(t *testing.T) {
t.Parallel()
// TODO: ship testdata files generated by mksquashfs
f, err := os.Open("/home/michael/distri/_build/distri/pkg/zlib-amd64-1.2.11-4.squashfs")
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
fis, err := rd.Readdir(rd.RootInode())
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 4; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[3], FileInfo{
name: "out",
size: 54,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1583085224, 0), // stat -c %Y /ro/zlib-amd64-1.2.11/out
}); err != nil {
t.Fatal(err)
}
fis, err = rd.Readdir(fis[3].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 3; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[1], FileInfo{
name: "lib",
size: 83,
mode: 0555 | os.ModeDir,
modTime: time.Unix(1583085224, 0), // stat -c %Y /ro/zlib-amd64-1.2.11/out/lib
}); err != nil {
t.Fatal(err)
}
fis, err = rd.Readdir(fis[1].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 4; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
if err := cmpFileInfo(fis[1], FileInfo{
name: "libz.so",
size: 9,
mode: 0555 | os.ModeSymlink,
modTime: time.Unix(1583085223, 0), // stat -c %Y /ro/zlib-amd64-1.2.11/out/lib/libz.so
}); err != nil {
t.Fatal(err)
}
// TODO: readlink
target, err := rd.ReadLink(fis[1].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := target, "libz.so.1"; got != want {
t.Fatalf("ReadLink(libz.so): got %q, want %q", got, want)
}
}
func TestReadfile(t *testing.T) {
t.Parallel()
// TODO: ship testdata files generated by mksquashfs
f, err := os.Open("/home/michael/distri/_build/distri/pkg/ack-amd64-3.3.1-7.squashfs")
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
fis, err := rd.Readdir(rd.RootInode())
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 3; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
fis, err = rd.Readdir(fis[0].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(fis), 1; got != want {
t.Fatalf("unexpected number of directory entries: got %d, want %d", got, want)
}
r, err := rd.FileReader(fis[0].Sys().(*FileInfo).Inode)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if _, err := r.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
h := md5.New()
if _, err := io.Copy(h, r); err != nil {
t.Fatal(err)
}
sum := fmt.Sprintf("%x", h.Sum(nil))
if got, want := sum, "c6c9b5d4d2a49f1b8b5e501f0f827a5c"; got != want {
t.Fatalf("md5(bin/ack): got %s, want %s", got, want)
}
}
}
// TODO: add test exercising ldirInodeHeader, e.g. '/mnt/loop/ca-certificates-3.39/buildoutput/etc/ssl'
func TestReadXattr(t *testing.T) {
t.Parallel()
// TODO: generate a smaller version of this file
f, err := os.Open("testdata/xattr.squashfs")
if err != nil {
t.Fatal(err)
}
defer f.Close()
rd, err := NewReader(f)
if err != nil {
t.Fatal(err)
}
for _, tt := range []struct {
Path string
Want []Xattr
}{
{
Path: "mtr-packet",
Want: []Xattr{
{
Type: XattrTypeSecurity,
FullName: "security.capability",
Value: []byte{1, 0, 0, 2, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
},
},
{
Path: "gnome-keyring-daemon",
Want: []Xattr{
{
Type: XattrTypeSecurity,
FullName: "security.capability",
Value: []byte{1, 0, 0, 2, 0, 0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
},
},
} {
inode, err := rd.LookupPath(tt.Path)
if err != nil {
t.Fatal(err)
}
xattrs, err := rd.ReadXattrs(inode)
if err != nil {
t.Fatalf("ReadXattrs(%v): %v", inode, err)
}
if diff := cmp.Diff(tt.Want, xattrs); diff != "" {
t.Fatalf("unexpected ReadXattrs result: diff (-want +got):\n%s", diff)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
package squashfs
//Descriptions provided by https://dr-emann.github.io/squashfs/
type superblock struct {
Magic uint32 //Magic will be 0x73717368 if it's a legit Squashfs filesystem
Inodes uint32 //Inodes is the number of inodes in the inodes table
MkfsTime int32 //MkfsTime is when archive was created
BlockSize uint32 //BlockSize is the size of data blocks in bytes
Fragments uint32 //Fragments is the number of entries in fragment table
Compression uint16 //Compression is what type of compression is used
BlockLog uint16 //BlockLog should be log base 2 of BlockSize. If not then the squash might be corrupt
Flags uint16 //Flags are the superblock's flags
IDCount uint16 //IDCount is the number of IDs in the id lookup table
Major uint16
Minor uint16
RootInode Inode
BytesUsed int64
IDTableStart int64
XattrIDTableStart int64
InodeTableStart int64
DirectoryTableStart int64
FragmentTableStart int64
LookupTableStart int64
}
+9
View File
@@ -0,0 +1,9 @@
package squashfs
import "io"
//Squashfs is a squashfs backed by a reader.
type Squashfs struct {
rdr *io.Reader //underlyting reader
super Superblock
}
+913
View File
@@ -0,0 +1,913 @@
// Package squashfs implements writing SquashFS file system images using zlib
// compression for data blocks (inodes and directory entries are written
// uncompressed for simplicity).
//
// Note that SquashFS requires directory entries to be sorted, i.e. files and
// directories need to be added in the correct order.
//
// This package intentionally only implements a subset of SquashFS. Notably,
// block devices, character devices, FIFOs, sockets and xattrs are not
// supported.
package squashfs
import (
"bytes"
"compress/zlib"
"encoding/binary"
"io"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/sys/unix"
)
// inode contains a block number + offset within that block.
type Inode int64
const (
zlibCompression = 1 + iota
lzmaCompression
lzoCompression
xzCompression
lz4Compression
zstdCompression
)
const (
invalidFragment = 0xFFFFFFFF
invalidXattr = 0xFFFFFFFF
)
const (
dirType = 1 + iota
fileType
symlinkType
blkdevType
chrdevType
fifoType
socketType
// The larger types are used for e.g. sparse files, xattrs, etc.
ldirType
lregType
lsymlinkType
lblkdevType
lchrdevType
lfifoType
lsocketType
)
type inodeHeader struct {
InodeType uint16
Mode uint16
Uid uint16
Gid uint16
Mtime int32
InodeNumber uint32
}
// fileType
type regInodeHeader struct {
inodeHeader
// full byte offset from the start of the file system, e.g. 96 for first
// file contents. Not using fragments limits us to 2^32-1-96 (≈ 4GiB) bytes
// of file contents.
StartBlock uint32
Fragment uint32
Offset uint32
FileSize uint32
// Followed by a uint32 array of compressed block sizes.
}
// lregType
type lregInodeHeader struct {
inodeHeader
// full byte offset from the start of the file system, e.g. 96 for first
// file contents. Not using fragments limits us to 2^32-1-96 (≈ 4GiB) bytes
// of file contents.
StartBlock uint64
FileSize uint64
Sparse uint64
Nlink uint32
Fragment uint32
Offset uint32
Xattr uint32
// Followed by a uint32 array of compressed block sizes.
}
// symlinkType
type symlinkInodeHeader struct {
inodeHeader
Nlink uint32
SymlinkSize uint32
// Followed by a byte array of SymlinkSize bytes.
}
// chrdevType and blkdevType
type devInodeHeader struct {
inodeHeader
Nlink uint32
Rdev uint32
}
// fifoType and socketType
type ipcInodeHeader struct {
inodeHeader
Nlink uint32
}
// dirType
type dirInodeHeader struct {
inodeHeader
StartBlock uint32
Nlink uint32
FileSize uint16
Offset uint16
ParentInode uint32
}
// ldirType
type ldirInodeHeader struct {
inodeHeader
Nlink uint32
FileSize uint32
StartBlock uint32
ParentInode uint32
Icount uint16
Offset uint16
Xattr uint32
}
type dirHeader struct {
Count uint32
StartBlock uint32
InodeOffset uint32
}
func (d *dirHeader) Unmarshal(b []byte) {
_ = b[11]
e := binary.LittleEndian
d.Count = e.Uint32(b)
d.StartBlock = e.Uint32(b[4:])
d.InodeOffset = e.Uint32(b[8:])
}
type dirEntry struct {
Offset uint16
InodeNumber int16
EntryType uint16
Size uint16
// Followed by a byte array of Size bytes.
}
func (d *dirEntry) Unmarshal(b []byte) {
_ = b[7]
e := binary.LittleEndian
d.Offset = e.Uint16(b)
d.InodeNumber = int16(e.Uint16(b[2:]))
d.EntryType = e.Uint16(b[4:])
d.Size = e.Uint16(b[6:])
}
// xattr types
const (
XattrTypeUser = iota
XattrTypeTrusted
XattrTypeSecurity
)
var xattrPrefix = map[int]string{
XattrTypeUser: "user.",
XattrTypeTrusted: "trusted.",
XattrTypeSecurity: "security.",
}
type Xattr struct {
Type uint16
FullName string
Value []byte
}
func XattrFromAttr(attr string, val []byte) Xattr {
for typ, prefix := range xattrPrefix {
if !strings.HasPrefix(attr, prefix) {
continue
}
return Xattr{
Type: uint16(typ),
FullName: strings.TrimPrefix(attr, prefix),
Value: val,
}
}
return Xattr{}
}
type xattrId struct {
Xattr uint64
Count uint32
Size uint32
}
func writeIdTable(w io.WriteSeeker, ids []uint32) (start int64, err error) {
metaOff, err := w.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, ids); err != nil {
return 0, err
}
if err := binary.Write(w, binary.LittleEndian, uint16(buf.Len())|0x8000); err != nil {
return 0, err
}
if _, err := io.Copy(w, &buf); err != nil {
return 0, err
}
off, err := w.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
return off, binary.Write(w, binary.LittleEndian, metaOff)
}
type fullDirEntry struct {
startBlock uint32
offset uint16
inodeNumber uint32
entryType uint16
name string
}
const (
magic = 0x73717368
dataBlockSize = 131072
metadataBlockSize = 8192
majorVersion = 4
minorVersion = 0
)
type Writer struct {
// Root represents the file system root. Like all directories, Flush must be
// called precisely once.
Root *Directory
xattrs []Xattr
xattrIds []xattrId
w io.WriteSeeker
sb superblock
inodeBuf bytes.Buffer
dirBuf bytes.Buffer
writeInodeNumTo map[string][]int64
}
// TODO: document what this is doing and what it is used for
func slog(block uint32) uint16 {
for i := uint16(12); i <= 20; i++ {
if block == (1 << i) {
return i
}
}
return 0
}
// filesystemFlags returns flags for a SquashFS file system created by this
// package (disabling most features for now).
func filesystemFlags() uint16 {
const (
noI = 1 << iota // uncompressed metadata
noD // uncompressed data
_
noF // uncompressed fragments
noFrag // never use fragments
alwaysFrag // always use fragments
duplicateChecking // de-duplication
exportable // exportable via NFS
noX // uncompressed xattrs
noXattr // no xattrs
compopt // compressor-specific options present?
)
return noI | noF | noFrag | noX | noXattr
}
// NewWriter returns a Writer which will write a SquashFS file system image to w
// once Flush is called.
//
// Create new files and directories with the corresponding methods on the Root
// directory of the Writer.
//
// File data is written to w even before Flush is called.
func NewWriter(w io.WriteSeeker, mkfsTime time.Time) (*Writer, error) {
// Skip over superblock to the data area, we come back to the superblock
// when flushing.
if _, err := w.Seek(96, io.SeekStart); err != nil {
return nil, err
}
wr := &Writer{
w: w,
sb: superblock{
Magic: magic,
MkfsTime: int32(mkfsTime.Unix()),
BlockSize: dataBlockSize,
Fragments: 0,
Compression: zlibCompression,
BlockLog: slog(dataBlockSize),
Flags: filesystemFlags(),
NoIds: 1, // just one uid/gid mapping (for root)
Major: majorVersion,
Minor: minorVersion,
XattrIdTableStart: -1, // not present
LookupTableStart: -1, // not present
},
writeInodeNumTo: make(map[string][]int64),
}
wr.Root = &Directory{
w: wr,
name: "", // root
modTime: mkfsTime,
}
return wr, nil
}
// Directory represents a SquashFS directory.
type Directory struct {
w *Writer
name string
modTime time.Time
dirEntries []fullDirEntry
parent *Directory
}
func (d *Directory) path() string {
if d.parent == nil {
return d.name
}
return filepath.Join(d.parent.path(), d.name)
}
type file struct {
w *Writer
d *Directory
off int64
size uint32
name string
modTime time.Time
mode uint16
// buf accumulates at least dataBlockSize bytes, at which point a new block
// is being written.
buf bytes.Buffer
// blocksizes stores, for each block of dataBlockSize bytes (uncompressed),
// the number of bytes the block compressed down to.
blocksizes []uint32
// compBuf is used for holding a block during compression to avoid memory
// allocations.
compBuf *bytes.Buffer
// zlibWriter is re-used for each compressed block
zlibWriter *zlib.Writer
xattrRef uint32
}
// Directory creates a new directory with the specified name and modTime.
func (d *Directory) Directory(name string, modTime time.Time) *Directory {
return &Directory{
w: d.w,
name: name,
modTime: modTime,
parent: d,
}
}
// File creates a file with the specified name, modTime and mode. The returned
// io.WriterCloser must be closed after writing the file.
func (d *Directory) File(name string, modTime time.Time, mode uint16, xattrs []Xattr) (io.WriteCloser, error) {
off, err := d.w.w.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}
// zlib.BestSpeed results in only a 2x slow-down over no compression
// (compared to >4x slow-down with DefaultCompression), but generates
// results which are in the same ball park (10% larger).
zw, err := zlib.NewWriterLevel(nil, zlib.BestSpeed)
if err != nil {
return nil, err
}
xattrRef := uint32(invalidXattr)
if len(xattrs) > 0 {
xattrRef = uint32(len(d.w.xattrs))
d.w.xattrs = append(d.w.xattrs, xattrs[0]) // TODO: support multiple
size := len(xattrs[0].FullName) + len(xattrs[0].Value)
d.w.xattrIds = append(d.w.xattrIds, xattrId{
// Xattr is populated in writeXattrTables
Count: 1, // TODO: support multiple
Size: uint32(size),
})
}
return &file{
w: d.w,
d: d,
off: off,
name: name,
modTime: modTime,
mode: mode,
compBuf: bytes.NewBuffer(make([]byte, dataBlockSize)),
zlibWriter: zw,
xattrRef: xattrRef,
}, nil
}
// Symlink creates a symbolic link from newname to oldname with the specified
// modTime and mode.
func (d *Directory) Symlink(oldname, newname string, modTime time.Time, mode os.FileMode) error {
startBlock := d.w.inodeBuf.Len() / metadataBlockSize
offset := d.w.inodeBuf.Len() - startBlock*metadataBlockSize
if err := binary.Write(&d.w.inodeBuf, binary.LittleEndian, symlinkInodeHeader{
inodeHeader: inodeHeader{
InodeType: symlinkType,
Mode: uint16(mode),
Uid: 0,
Gid: 0,
Mtime: int32(modTime.Unix()),
InodeNumber: d.w.sb.Inodes + 1,
},
Nlink: 1, // TODO(later): when is this not 1?
SymlinkSize: uint32(len(oldname)),
}); err != nil {
return err
}
if _, err := d.w.inodeBuf.Write([]byte(oldname)); err != nil {
return err
}
d.dirEntries = append(d.dirEntries, fullDirEntry{
startBlock: uint32(startBlock),
offset: uint16(offset),
inodeNumber: d.w.sb.Inodes + 1,
entryType: symlinkType,
name: newname,
})
d.w.sb.Inodes++
return nil
}
// Flush writes directory entries and creates inodes for the directory.
func (d *Directory) Flush() error {
countByStartBlock := make(map[uint32]uint32)
for _, de := range d.dirEntries {
countByStartBlock[de.startBlock]++
}
dirBufStartBlock := d.w.dirBuf.Len() / metadataBlockSize
dirBufOffset := d.w.dirBuf.Len()
currentBlock := int64(-1)
currentInodeOffset := int64(-1)
var subdirs int
for _, de := range d.dirEntries {
if de.entryType == dirType {
subdirs++
}
if int64(de.startBlock) != currentBlock {
dh := dirHeader{
Count: countByStartBlock[de.startBlock] - 1,
StartBlock: de.startBlock * (metadataBlockSize + 2),
InodeOffset: de.inodeNumber,
}
if err := binary.Write(&d.w.dirBuf, binary.LittleEndian, &dh); err != nil {
return err
}
currentBlock = int64(de.startBlock)
currentInodeOffset = int64(de.inodeNumber)
}
if err := binary.Write(&d.w.dirBuf, binary.LittleEndian, &dirEntry{
Offset: de.offset,
InodeNumber: int16(de.inodeNumber - uint32(currentInodeOffset)),
EntryType: de.entryType,
Size: uint16(len(de.name) - 1),
}); err != nil {
return err
}
if _, err := d.w.dirBuf.Write([]byte(de.name)); err != nil {
return err
}
}
startBlock := d.w.inodeBuf.Len() / metadataBlockSize
offset := d.w.inodeBuf.Len() - startBlock*metadataBlockSize
inodeBufOffset := d.w.inodeBuf.Len()
// parentInodeOffset is the offset (in bytes) of the ParentInode field
// within a dirInodeHeader or ldirInodeHeader
var parentInodeOffset int64
if len(d.dirEntries) > 256 ||
d.w.dirBuf.Len()-dirBufOffset > metadataBlockSize {
parentInodeOffset = (2 + 2 + 2 + 2 + 4 + 4) + 4 + 4 + 4
if err := binary.Write(&d.w.inodeBuf, binary.LittleEndian, ldirInodeHeader{
inodeHeader: inodeHeader{
InodeType: ldirType,
Mode: unix.S_IRUSR | unix.S_IWUSR | unix.S_IXUSR |
unix.S_IRGRP | unix.S_IXGRP |
unix.S_IROTH | unix.S_IXOTH,
Uid: 0,
Gid: 0,
Mtime: int32(d.modTime.Unix()),
InodeNumber: d.w.sb.Inodes + 1,
},
Nlink: uint32(subdirs + 2 - 1), // + 2 for . and ..
FileSize: uint32(d.w.dirBuf.Len()-dirBufOffset) + 3,
StartBlock: uint32(dirBufStartBlock * (metadataBlockSize + 2)),
ParentInode: d.w.sb.Inodes + 2, // invalid
Icount: 0, // no directory index
Offset: uint16(dirBufOffset - dirBufStartBlock*metadataBlockSize),
Xattr: invalidXattr,
}); err != nil {
return err
}
} else {
parentInodeOffset = (2 + 2 + 2 + 2 + 4 + 4) + 4 + 4 + 2 + 2
if err := binary.Write(&d.w.inodeBuf, binary.LittleEndian, dirInodeHeader{
inodeHeader: inodeHeader{
InodeType: dirType,
Mode: unix.S_IRUSR | unix.S_IWUSR | unix.S_IXUSR |
unix.S_IRGRP | unix.S_IXGRP |
unix.S_IROTH | unix.S_IXOTH,
Uid: 0,
Gid: 0,
Mtime: int32(d.modTime.Unix()),
InodeNumber: d.w.sb.Inodes + 1,
},
StartBlock: uint32(dirBufStartBlock * (metadataBlockSize + 2)),
Nlink: uint32(subdirs + 2 - 1), // + 2 for . and ..
FileSize: uint16(d.w.dirBuf.Len()-dirBufOffset) + 3,
Offset: uint16(dirBufOffset - dirBufStartBlock*metadataBlockSize),
ParentInode: d.w.sb.Inodes + 2, // invalid
}); err != nil {
return err
}
}
path := d.path()
for _, offset := range d.w.writeInodeNumTo[path] {
// Directly manipulating unread data in bytes.Buffer via Bytes(), as per
// https://groups.google.com/d/msg/golang-nuts/1ON9XVQ1jXE/8j9RaeSYxuEJ
b := d.w.inodeBuf.Bytes()
binary.LittleEndian.PutUint32(b[offset:offset+4], d.w.sb.Inodes+1)
}
if d.parent != nil {
parentPath := filepath.Dir(d.path())
if parentPath == "." {
parentPath = ""
}
d.w.writeInodeNumTo[parentPath] = append(d.w.writeInodeNumTo[parentPath], int64(inodeBufOffset)+parentInodeOffset)
d.parent.dirEntries = append(d.parent.dirEntries, fullDirEntry{
startBlock: uint32(startBlock),
offset: uint16(offset),
inodeNumber: d.w.sb.Inodes + 1,
entryType: dirType,
name: d.name,
})
} else { // root
d.w.sb.RootInode = Inode((startBlock*(metadataBlockSize+2))<<16 | offset)
}
d.w.sb.Inodes++
return nil
}
// Write implements io.Writer
func (f *file) Write(p []byte) (n int, err error) {
n, err = f.buf.Write(p)
if n > 0 {
// Keep track of the uncompressed file size.
f.size += uint32(n)
for f.buf.Len() >= dataBlockSize {
if err := f.writeBlock(); err != nil {
return 0, err
}
}
}
return n, err
}
func (f *file) writeBlock() error {
n := f.buf.Len()
if n > dataBlockSize {
n = dataBlockSize
}
// Feed dataBlockSize bytes to the compressor
b := f.buf.Bytes()
block := b[:n]
rest := b[n:]
/*
f.compBuf.Reset()
f.zlibWriter.Reset(f.compBuf)
if _, err := f.zlibWriter.Write(block); err != nil {
return err
}
if err := f.zlibWriter.Close(); err != nil {
return err
}
size := f.compBuf.Len()
if size > len(block) {
// Copy uncompressed data: Linux returns i/o errors when it encounters a
// compressed block which is larger than the uncompressed data:
// https://github.com/torvalds/linux/blob/3ca24ce9ff764bc27bceb9b2fd8ece74846c3fd3/fs/squashfs/block.c#L150
size = len(block) | (1 << 24) // SQUASHFS_COMPRESSED_BIT_BLOCK
if _, err := f.w.w.Write(block); err != nil {
return err
}
} else {
if _, err := io.Copy(f.w.w, f.compBuf); err != nil {
return err
}
}
*/
// Copy uncompressed data: Linux returns i/o errors when it encounters a
// compressed block which is larger than the uncompressed data:
// https://github.com/torvalds/linux/blob/3ca24ce9ff764bc27bceb9b2fd8ece74846c3fd3/fs/squashfs/block.c#L150
size := len(block) | (1 << 24) // SQUASHFS_COMPRESSED_BIT_BLOCK
if _, err := f.w.w.Write(block); err != nil {
return err
}
f.blocksizes = append(f.blocksizes, uint32(size))
// Keep the rest in f.buf for the next write
copy(b, rest)
f.buf.Truncate(len(rest))
return nil
}
// Close implements io.Closer
func (f *file) Close() error {
for f.buf.Len() > 0 {
if err := f.writeBlock(); err != nil {
return err
}
}
startBlock := f.w.inodeBuf.Len() / metadataBlockSize
offset := f.w.inodeBuf.Len() - startBlock*metadataBlockSize
if err := binary.Write(&f.w.inodeBuf, binary.LittleEndian, lregInodeHeader{
inodeHeader: inodeHeader{
InodeType: lregType,
Mode: f.mode,
Uid: 0,
Gid: 0,
Mtime: int32(f.modTime.Unix()),
InodeNumber: f.w.sb.Inodes + 1,
},
StartBlock: uint64(f.off),
FileSize: uint64(f.size),
Nlink: 1,
Fragment: invalidFragment,
Offset: 0,
Xattr: f.xattrRef,
}); err != nil {
return err
}
if err := binary.Write(&f.w.inodeBuf, binary.LittleEndian, f.blocksizes); err != nil {
return err
}
f.d.dirEntries = append(f.d.dirEntries, fullDirEntry{
startBlock: uint32(startBlock),
offset: uint16(offset),
inodeNumber: f.w.sb.Inodes + 1,
entryType: fileType,
name: f.name,
})
f.w.sb.Inodes++
return nil
}
func writeXattr(w io.Writer, xattrs []Xattr) error {
for _, attr := range xattrs {
if err := binary.Write(w, binary.LittleEndian, struct {
Type uint16
NameSize uint16
}{
Type: attr.Type,
NameSize: uint16(len(attr.FullName)),
}); err != nil {
return err
}
if _, err := w.Write([]byte(attr.FullName)); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, struct {
ValSize uint32
}{
ValSize: uint32(len(attr.Value)),
}); err != nil {
return err
}
if _, err := w.Write(attr.Value); err != nil {
return err
}
}
return nil
}
type xattrTableHeader struct {
XattrTableStart uint64
XattrIds uint32
Unused uint32
}
func (w *Writer) writeXattrTables() (int64, error) {
if len(w.xattrs) == 0 {
return -1, nil
}
off, err := w.w.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
xattrTableStart := uint64(off)
var xattrBuf bytes.Buffer
if err := writeXattr(&xattrBuf, w.xattrs); err != nil {
return 0, err
}
xattrBlocks := (xattrBuf.Len() + (metadataBlockSize - 1)) / metadataBlockSize
if err := w.writeMetadataChunks(&xattrBuf); err != nil {
return 0, err
}
// write xattr id table
off, err = w.w.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
idTableOff := uint64(off)
var xattrIdBuf bytes.Buffer
size := uint64(0)
for _, id := range w.xattrIds {
id.Xattr = uint64(size)
size += uint64(id.Size) + 8 /* sizeof(Type+NameSize+ValSize) */
if err := binary.Write(&xattrIdBuf, binary.LittleEndian, id); err != nil {
return 0, err
}
}
if err := w.writeMetadataChunks(&xattrIdBuf); err != nil {
return 0, err
}
// xattr table header
off, err = w.w.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
if err := binary.Write(w.w, binary.LittleEndian, xattrTableHeader{
XattrTableStart: xattrTableStart,
XattrIds: uint32(len(w.xattrs)),
}); err != nil {
return 0, err
}
// write block index
for i := 0; i < xattrBlocks; i++ {
if err := binary.Write(w.w, binary.LittleEndian, struct {
BlockOffset uint64
}{
BlockOffset: idTableOff + (uint64(i) * (8192 + 2 /* sizeof(uint16) */)),
}); err != nil {
return 0, err
}
}
return off, nil
}
// writeMetadataChunks copies from r to w in blocks of metadataBlockSize bytes
// each, prefixing each block with a uint16 length header, setting the
// uncompressed bit.
func (w *Writer) writeMetadataChunks(r io.Reader) error {
buf := make([]byte, metadataBlockSize)
for {
buf = buf[:metadataBlockSize]
n, err := r.Read(buf)
if err != nil {
if err == io.EOF { // done
return nil
}
return err
}
buf = buf[:n]
if err := binary.Write(w.w, binary.LittleEndian, uint16(len(buf))|0x8000); err != nil {
return err
}
if _, err := w.w.Write(buf); err != nil {
return err
}
}
}
// Flush writes the SquashFS file system. The Writer must not be used after
// calling Flush.
func (w *Writer) Flush() error {
// (1) superblock will be written later
// (2) compressor-specific options omitted
// (3) data has already been written
// (4) write inode table
off, err := w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
w.sb.InodeTableStart = off
if err := w.writeMetadataChunks(&w.inodeBuf); err != nil {
return err
}
// (5) write directory table
off, err = w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
w.sb.DirectoryTableStart = off
if err := w.writeMetadataChunks(&w.dirBuf); err != nil {
return err
}
// (6) fragment table omitted
off, err = w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
w.sb.FragmentTableStart = off
// (7) export table omitted
// (8) write uid/gid lookup table
idTableStart, err := writeIdTable(w.w, []uint32{0})
if err != nil {
return err
}
w.sb.IdTableStart = idTableStart
// (9) xattr table
off, err = w.writeXattrTables()
if err != nil {
return err
}
w.sb.XattrIdTableStart = off
off, err = w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
w.sb.BytesUsed = off
// Pad to 4096, required for the kernel to be able to access all pages
if pad := off % 4096; pad > 0 {
padding := make([]byte, 4096-pad)
if _, err := w.w.Write(padding); err != nil {
return err
}
}
// (1) Write superblock
if _, err := w.w.Seek(0, io.SeekStart); err != nil {
return err
}
return binary.Write(w.w, binary.LittleEndian, &w.sb)
}
+310
View File
@@ -0,0 +1,310 @@
package squashfs
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/distr1/distri"
// "github.com/distr1/distri/internal/distritest"
"github.com/google/go-cmp/cmp"
"github.com/orcaman/writerseeker"
"golang.org/x/sys/unix"
)
var fsImagePath = flag.String("fs_image_path", "", "Store the SquashFS test file system in the specified path for manual inspection")
func writeTestImage(iow io.WriteSeeker, xattr bool) error {
w, err := NewWriter(iow, time.Now())
if err != nil {
return err
}
var xattrs []Xattr
if xattr {
xattrs = append(xattrs, Xattr{
Type: 2,
FullName: "capability",
Value: []byte{1, 0, 0, 2, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
})
}
ff, err := w.Root.File("hellö wörld", time.Now(), unix.S_IRUSR|unix.S_IRGRP|unix.S_IROTH, xattrs)
if err != nil {
return err
}
if _, err := ff.Write([]byte("hello world!")); err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
ff, err = w.Root.File("leer", time.Now(), unix.S_IRUSR|unix.S_IRGRP|unix.S_IROTH, nil)
if err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
ff, err = w.Root.File("second file", time.Now(), unix.S_IRUSR|unix.S_IXUSR|
unix.S_IRGRP|unix.S_IXGRP|
unix.S_IROTH|unix.S_IXOTH,
nil)
if err != nil {
return err
}
if _, err := ff.Write([]byte("NON.\n")); err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
if err := w.Root.Symlink("second file", "second link", time.Now(), unix.S_IRUSR|unix.S_IRGRP|unix.S_IROTH); err != nil {
return err
}
subdir := w.Root.Directory("subdir", time.Now())
subsubdir := subdir.Directory("deep", time.Now())
ff, err = subsubdir.File("yo", time.Now(), unix.S_IRUSR|unix.S_IRGRP|unix.S_IROTH, nil)
if err != nil {
return err
}
if _, err := ff.Write([]byte("foo\n")); err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
if err := subsubdir.Flush(); err != nil {
return err
}
// TODO: write another file in subdir now, will result in invalid parent inode
ff, err = subdir.File("third file (in subdir)", time.Now(), unix.S_IRUSR|unix.S_IRGRP|unix.S_IROTH, nil)
if err != nil {
return err
}
if _, err := ff.Write([]byte("contents\n")); err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
if err := subdir.Flush(); err != nil {
return err
}
ff, err = w.Root.File("testbin", time.Now(), unix.S_IRUSR|unix.S_IXUSR|
unix.S_IRGRP|unix.S_IXGRP|
unix.S_IROTH|unix.S_IXOTH,
nil)
if err != nil {
return err
}
zf, err := os.Open(os.Args[0])
if err != nil {
return err
}
defer zf.Close()
if _, err := io.Copy(ff, zf); err != nil {
return err
}
if err := ff.Close(); err != nil {
return err
}
if err := w.Root.Flush(); err != nil {
return err
}
if err := w.Flush(); err != nil {
return err
}
return nil
}
func TestUnsquashfs(t *testing.T) {
t.Parallel()
ctx, canc := distri.InterruptibleContext()
defer canc()
if _, err := exec.LookPath("unsquashfs"); err != nil {
t.Skip("unsquashfs not found in $PATH")
}
for _, xattr := range []bool{false, true} {
t.Run(fmt.Sprintf("xattr %v", xattr), func(t *testing.T) {
var (
f *os.File
err error
)
if *fsImagePath != "" {
f, err = os.Create(*fsImagePath + fmt.Sprintf("-xattr-%v", xattr))
} else {
f, err = ioutil.TempFile("", fmt.Sprintf("squashfs-xattr-%v", xattr))
if err == nil {
defer os.Remove(f.Name())
}
}
if err != nil {
t.Fatal(err)
}
if err := writeTestImage(f, xattr); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
// Extract our generated file system using unsquashfs(1)
out, err := ioutil.TempDir("", fmt.Sprintf("unsquashfs-xattr-%v", xattr))
if err != nil {
t.Fatal(err)
}
// defer distritest.RemoveAll(t, out)
cmd := exec.CommandContext(ctx, "unsquashfs", "-no-xattrs", "-d", filepath.Join(out, "x"), f.Name())
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
fbin, err := os.Open(os.Args[0])
if err != nil {
t.Fatal(err)
}
// Verify the extracted files match our expectations.
for _, entry := range []struct {
path string
contents io.Reader
}{
{"leer", strings.NewReader("")},
{"hellö wörld", strings.NewReader("hello world!")},
{"testbin", fbin},
{"subdir/third file (in subdir)", strings.NewReader("contents\n")},
} {
entry := entry // copy
t.Run(entry.path, func(t *testing.T) {
in, err := os.Open(filepath.Join(out, "x", entry.path))
if err != nil {
t.Fatal(err)
}
got, err := ioutil.ReadAll(in)
if err != nil {
t.Fatal(err)
}
want, err := ioutil.ReadAll(entry.contents)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("path %q differs", entry.path)
}
})
}
})
}
}
func TestReader(t *testing.T) {
t.Parallel()
for _, xattr := range []bool{false, true} {
t.Run(fmt.Sprintf("xattr %v", xattr), func(t *testing.T) {
var err error
buf := &writerseeker.WriterSeeker{}
if err := writeTestImage(buf, xattr); err != nil {
t.Fatal(err)
}
if _, err := buf.Seek(0, io.SeekCurrent); err != nil {
t.Fatal(err)
}
rd, err := NewReader(buf.BytesReader())
if err != nil {
t.Fatal(err)
}
fbin, err := os.Open(os.Args[0])
if err != nil {
t.Fatal(err)
}
// Verify the extracted files match our expectations.
for _, entry := range []struct {
path string
contents io.Reader
}{
{"leer", strings.NewReader("")},
{"hellö wörld", strings.NewReader("hello world!")},
{"testbin", fbin},
{"subdir/third file (in subdir)", strings.NewReader("contents\n")},
} {
entry := entry // copy
t.Run(entry.path, func(t *testing.T) {
// TODO: is this t.Parallel()-safe?
inode, err := rd.LookupPath(entry.path)
if err != nil {
t.Fatal(err)
}
in, err := rd.FileReader(inode)
if err != nil {
t.Fatal(err)
}
got, err := ioutil.ReadAll(in)
if err != nil {
t.Fatal(err)
}
want, err := ioutil.ReadAll(entry.contents)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("path %q differs", entry.path)
}
})
}
if xattr {
t.Run("xattrs", func(t *testing.T) {
inode, err := rd.LookupPath("hellö wörld")
if err != nil {
t.Fatal(err)
}
xattrs, err := rd.ReadXattrs(inode)
if err != nil {
t.Fatal(err)
}
if got, want := len(xattrs), 1; got != want {
t.Fatalf("unexpected number of extended attributes: got %d, want %d", got, want)
}
wantXattr := Xattr{
Type: 2,
FullName: "security.capability",
Value: []byte{1, 0, 0, 2, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}
if diff := cmp.Diff(wantXattr, xattrs[0]); diff != "" {
t.Errorf("unexpected extended attribute: diff (-want +got):\n%s", diff)
}
})
}
})
}
}