443 lines
11 KiB
Go
443 lines
11 KiB
Go
package squashfs
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
|
|
"github.com/CalebQ42/squashfs/internal/routinemanager"
|
|
squashfslow "github.com/CalebQ42/squashfs/low"
|
|
"github.com/CalebQ42/squashfs/low/data"
|
|
"github.com/CalebQ42/squashfs/low/inode"
|
|
)
|
|
|
|
// File represents a file inside a squashfs archive.
|
|
type File struct {
|
|
full data.FullReader
|
|
rdr data.Reader
|
|
rdrInit bool
|
|
parent FS
|
|
r *Reader
|
|
b squashfslow.FileBase
|
|
dirsRead int
|
|
}
|
|
|
|
// Creates a new *File from the given *squashfs.Base
|
|
func (r *Reader) FileFromBase(b squashfslow.FileBase, parent FS) File {
|
|
return File{
|
|
b: b,
|
|
parent: parent,
|
|
r: r,
|
|
}
|
|
}
|
|
|
|
func (f File) FS() (FS, error) {
|
|
if !f.IsDir() {
|
|
return FS{}, errors.New("not a directory")
|
|
}
|
|
d, err := f.b.ToDir(f.r.Low)
|
|
if err != nil {
|
|
return FS{}, err
|
|
}
|
|
return FS{d: d, parent: &f.parent, r: f.r}, nil
|
|
}
|
|
|
|
// Closes the underlying readers.
|
|
// Further calls to Read and WriteTo will re-create the readers.
|
|
// Never returns an error.
|
|
func (f *File) Close() error {
|
|
f.rdr.Close()
|
|
f.full.Close()
|
|
return nil
|
|
}
|
|
|
|
// Returns the file the symlink points to.
|
|
// If the file isn't a symlink, or points to a file outside the archive, returns nil.
|
|
func (f File) GetSymlinkFile() fs.File {
|
|
if !f.IsSymlink() {
|
|
return nil
|
|
}
|
|
if filepath.IsAbs(f.SymlinkPath()) {
|
|
return nil
|
|
}
|
|
fil, err := f.parent.Open(f.SymlinkPath())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return fil
|
|
}
|
|
|
|
// Returns whether the file is a directory.
|
|
func (f File) IsDir() bool {
|
|
return f.b.IsDir()
|
|
}
|
|
|
|
// Returns whether the file is a regular file.
|
|
func (f File) IsRegular() bool {
|
|
return f.b.IsRegular()
|
|
}
|
|
|
|
// Returns whether the file is a symlink.
|
|
func (f File) IsSymlink() bool {
|
|
return f.b.Inode.Type == inode.Sym || f.b.Inode.Type == inode.ESym
|
|
}
|
|
|
|
func (f File) Mode() fs.FileMode {
|
|
return f.b.Inode.Mode()
|
|
}
|
|
|
|
// Read reads the data from the file. Only works if file is a normal file.
|
|
func (f *File) Read(b []byte) (int, error) {
|
|
if !f.IsRegular() {
|
|
return 0, errors.New("file is not a regular file")
|
|
}
|
|
if !f.rdrInit {
|
|
err := f.initializeReaders()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return f.rdr.Read(b)
|
|
}
|
|
|
|
// ReadDir returns n fs.DirEntry's that's contained in the File (if it's a directory).
|
|
// If n <= 0 all fs.DirEntry's are returned.
|
|
func (f *File) ReadDir(n int) ([]fs.DirEntry, error) {
|
|
if !f.IsDir() {
|
|
return nil, errors.New("file is not a directory")
|
|
}
|
|
d, err := f.b.ToDir(f.r.Low)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start, end := 0, len(d.Entries)
|
|
if n > 0 {
|
|
start, end = f.dirsRead, f.dirsRead+n
|
|
if end > len(d.Entries) {
|
|
end = len(d.Entries)
|
|
err = io.EOF
|
|
}
|
|
}
|
|
var out []fs.DirEntry
|
|
var fi FileInfo
|
|
for _, e := range d.Entries[start:end] {
|
|
fi, err = f.r.newFileInfo(e)
|
|
if err != nil {
|
|
f.dirsRead += len(out)
|
|
return out, err
|
|
}
|
|
out = append(out, fs.FileInfoToDirEntry(fi))
|
|
}
|
|
f.dirsRead += len(out)
|
|
return out, err
|
|
}
|
|
|
|
// Returns the file's fs.FileInfo
|
|
func (f File) Stat() (fs.FileInfo, error) {
|
|
uid, err := f.b.Uid(&f.r.Low)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gid, err := f.b.Gid(&f.r.Low)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newFileInfo(f.b.Name, uid, gid, &f.b.Inode), nil
|
|
}
|
|
|
|
// SymlinkPath returns the symlink's target path. Is the File isn't a symlink, returns an empty string.
|
|
func (f File) SymlinkPath() string {
|
|
switch f.b.Inode.Type {
|
|
case inode.Sym:
|
|
return string(f.b.Inode.Data.(inode.Symlink).Target)
|
|
case inode.ESym:
|
|
return string(f.b.Inode.Data.(inode.ESymlink).Target)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Writes all data from the file to the given writer in a multi-threaded manner.
|
|
// The underlying reader is separate
|
|
func (f *File) WriteTo(w io.Writer) (int64, error) {
|
|
if !f.IsRegular() {
|
|
return 0, errors.New("file is not a regular file")
|
|
}
|
|
if !f.rdrInit {
|
|
err := f.initializeReaders()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return f.full.WriteTo(w)
|
|
}
|
|
|
|
func (f *File) initializeReaders() error {
|
|
var err error
|
|
f.rdr, f.full, err = f.b.GetRegFileReaders(f.r.Low)
|
|
if err == nil {
|
|
f.rdrInit = true
|
|
} else {
|
|
f.rdr.Close()
|
|
f.full.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (f File) deviceDevices() (maj uint32, min uint32) {
|
|
var dev uint32
|
|
switch f.b.Inode.Type {
|
|
case inode.Char, inode.Block:
|
|
dev = f.b.Inode.Data.(inode.Device).Dev
|
|
case inode.EChar, inode.EBlock:
|
|
dev = f.b.Inode.Data.(inode.EDevice).Dev
|
|
}
|
|
return dev >> 8, dev & 0x000FF
|
|
}
|
|
|
|
func (f File) path() string {
|
|
if f.parent.d.Name == "" {
|
|
return f.b.Name
|
|
}
|
|
return filepath.Join(f.parent.path(), f.b.Name)
|
|
}
|
|
|
|
// Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
|
|
// Uses default extraction options.
|
|
func (f File) Extract(folder string) error {
|
|
return f.ExtractWithOptions(folder, DefaultOptions())
|
|
}
|
|
|
|
// Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
|
|
// Allows setting various extraction options via ExtractionOptions.
|
|
func (f File) ExtractWithOptions(path string, op *ExtractionOptions) error {
|
|
if op.manager == nil {
|
|
op.manager = routinemanager.NewManager(op.SimultaneousFiles)
|
|
if op.LogOutput != nil {
|
|
log.SetOutput(op.LogOutput)
|
|
}
|
|
err := os.MkdirAll(path, 0777)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create initial directory", path)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
switch f.b.Inode.Type {
|
|
case inode.Dir, inode.EDir:
|
|
d, err := f.b.ToDir(f.r.Low)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create squashfs.Directory for", path)
|
|
}
|
|
return errors.Join(errors.New("failed to create squashfs.Directory: "+path), err)
|
|
}
|
|
errChan := make(chan error, len(d.Entries))
|
|
for i := range d.Entries {
|
|
b, err := f.r.Low.BaseFromEntry(d.Entries[i])
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to get squashfs.Base from entry for", path)
|
|
}
|
|
return errors.Join(errors.New("failed to get base from entry: "+path), err)
|
|
}
|
|
go func(b squashfslow.FileBase, path string) {
|
|
i := op.manager.Lock()
|
|
if b.IsDir() {
|
|
extDir := filepath.Join(path, b.Name)
|
|
err = os.Mkdir(extDir, 0777)
|
|
op.manager.Unlock(i)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create directory", path)
|
|
}
|
|
errChan <- errors.Join(errors.New("failed to create directory: "+path), err)
|
|
return
|
|
}
|
|
err = f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)).ExtractWithOptions(extDir, op)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to extract directory", path)
|
|
}
|
|
errChan <- errors.Join(errors.New("failed to extract directory: "+path), err)
|
|
return
|
|
}
|
|
errChan <- nil
|
|
} else {
|
|
fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent))
|
|
err = fil.ExtractWithOptions(path, op)
|
|
op.manager.Unlock(i)
|
|
fil.Close()
|
|
errChan <- err
|
|
}
|
|
}(b, path)
|
|
}
|
|
var errCache []error
|
|
for range d.Entries {
|
|
err := <-errChan
|
|
if err != nil {
|
|
errCache = append(errCache, err)
|
|
}
|
|
}
|
|
if len(errCache) > 0 {
|
|
return errors.Join(errors.New("failed to extract folder: "+path), errors.Join(errCache...))
|
|
}
|
|
case inode.Fil, inode.EFil:
|
|
path = filepath.Join(path, f.b.Name)
|
|
outFil, err := os.Create(path)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create file", path)
|
|
}
|
|
return errors.Join(errors.New("failed to create file: "+path), err)
|
|
}
|
|
defer outFil.Close()
|
|
full, err := f.b.GetFullReader(&f.r.Low)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create full reader for", path)
|
|
}
|
|
return errors.Join(errors.New("failed to create full reader: "+path), err)
|
|
}
|
|
full.SetGoroutineLimit(op.ExtractionRoutines)
|
|
_, err = full.WriteTo(outFil)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to write file", path)
|
|
}
|
|
return errors.Join(errors.New("failed to write file: "+path), err)
|
|
}
|
|
case inode.Sym, inode.ESym:
|
|
symPath := f.SymlinkPath()
|
|
if op.DereferenceSymlink {
|
|
filTmp := f.GetSymlinkFile()
|
|
if filTmp == nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to get symlink's file:", f.path())
|
|
}
|
|
return errors.New("failed to get symlink's file")
|
|
}
|
|
fil := filTmp.(*File)
|
|
fil.b.Name = f.b.Name
|
|
err := fil.ExtractWithOptions(path, op)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to extract symlink's file:", filepath.Join(path, f.b.Name))
|
|
}
|
|
return errors.Join(errors.New("failed to extract symlink's file: "+path), err)
|
|
}
|
|
} else {
|
|
if op.UnbreakSymlink {
|
|
filTmp := f.GetSymlinkFile()
|
|
if filTmp == nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to get symlink's file:", f.path())
|
|
}
|
|
return errors.New("failed to get symlink's file")
|
|
}
|
|
extractLoc := filepath.Join(path, filepath.Dir(symPath))
|
|
fil := filTmp.(*File)
|
|
err := fil.ExtractWithOptions(extractLoc, op)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Error while extracting", fil.path(), "to make sure symlink at", f.path(), "is unbroken")
|
|
}
|
|
return errors.Join(errors.New("failed to extract symlink's file: "+extractLoc), err)
|
|
}
|
|
}
|
|
path = filepath.Join(path, f.b.Name)
|
|
err := os.Symlink(f.SymlinkPath(), path)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to create symlink:", path)
|
|
}
|
|
return errors.Join(errors.New("failed to create symlink: "+path), err)
|
|
}
|
|
}
|
|
case inode.Char, inode.EChar, inode.Block, inode.EBlock, inode.Fifo, inode.EFifo:
|
|
if runtime.GOOS == "windows" {
|
|
if op.Verbose {
|
|
log.Println(f.path(), "ignored. A device link and can't be created on Windows.")
|
|
}
|
|
return nil
|
|
}
|
|
_, err := exec.LookPath("mknod")
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("mknot command not found, cannot create device link for", f.path())
|
|
}
|
|
return errors.Join(errors.New("mknot command not found"), err)
|
|
}
|
|
path = filepath.Join(path, f.b.Name)
|
|
var typ string
|
|
switch f.b.Inode.Type {
|
|
case inode.Char, inode.EChar:
|
|
typ = "c"
|
|
case inode.Block, inode.EBlock:
|
|
typ = "b"
|
|
default: //Fifo IPC
|
|
if runtime.GOOS == "darwin" {
|
|
if op.Verbose {
|
|
log.Println(f.path(), "ignored. A Fifo file and can't be created on Darwin.")
|
|
}
|
|
return nil
|
|
}
|
|
typ = "p"
|
|
}
|
|
cmd := exec.Command("mknod", path, typ)
|
|
if typ != "p" {
|
|
maj, min := f.deviceDevices()
|
|
cmd.Args = append(cmd.Args, strconv.Itoa(int(maj)), strconv.Itoa(int(min)))
|
|
}
|
|
if op.Verbose {
|
|
cmd.Stdout = op.LogOutput
|
|
cmd.Stderr = op.LogOutput
|
|
}
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Error while running mknod for", path)
|
|
}
|
|
return errors.Join(errors.New("error while running mknod for "+path), err)
|
|
}
|
|
case inode.Sock, inode.ESock:
|
|
if op.Verbose {
|
|
log.Println(f.path(), "ignored since it's a socket file.")
|
|
}
|
|
return nil
|
|
default:
|
|
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.b.Inode.Type)))
|
|
}
|
|
if op.Verbose {
|
|
log.Println(f.path(), "extracted to", path)
|
|
}
|
|
if op.IgnorePerm {
|
|
return nil
|
|
}
|
|
uid, err := f.b.Uid(&f.r.Low)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to get uid for", path)
|
|
log.Println(err)
|
|
}
|
|
return nil
|
|
}
|
|
gid, err := f.b.Gid(&f.r.Low)
|
|
if err != nil {
|
|
if op.Verbose {
|
|
log.Println("Failed to get gid for", path)
|
|
log.Println(err)
|
|
}
|
|
return nil
|
|
}
|
|
os.Chmod(path, f.Mode())
|
|
os.Chown(path, int(uid), int(gid))
|
|
return nil
|
|
}
|