Finished. Now for bug fixes

This commit is contained in:
Caleb Gardner
2023-12-24 18:20:05 -06:00
parent 5de59627df
commit d9132ab6a4
6 changed files with 385 additions and 20 deletions
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"flag"
"fmt"
"os"
"time"
"github.com/CalebQ42/squashfs"
)
func main() {
verbose := flag.Bool("v", false, "Verbose")
ignore := flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755")
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Println("Please provide a file name and extraction path")
os.Exit(0)
}
f, err := os.Open(flag.Arg(0))
if err != nil {
panic(err)
}
r, err := squashfs.NewReader(f)
if err != nil {
panic(err)
}
op := squashfs.DefaultOptions()
op.Verbose = *verbose
op.IgnorePerm = *ignore
n := time.Now()
err = r.ExtractWithOptions(flag.Arg(1), op)
if err != nil {
panic(err)
}
fmt.Println("Took:", time.Since(n))
}
+11 -2
View File
@@ -3,19 +3,28 @@ package squashfs
import ( import (
"io" "io"
"io/fs" "io/fs"
"os"
"github.com/CalebQ42/squashfs/internal/routinemanager"
) )
type ExtractionOptions struct { type ExtractionOptions struct {
LogOutput io.Writer //Where error log should write. manager *routinemanager.Manager
LogOutput io.Writer //Where the verbose log should write. Defaults to os.Stdout.
DereferenceSymlink bool //Replace symlinks with the target file. DereferenceSymlink bool //Replace symlinks with the target file.
UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink. UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink.
Verbose bool //Prints extra info to log on an error. Verbose bool //Prints extra info to log on an error.
IgnorePerm bool //Ignore file's permissions and instead use Perm. IgnorePerm bool //Ignore file's permissions and instead use Perm.
Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0777. Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0777.
SimultaneousFiles uint16 //Number of files to process in parallel. Defaults to 10.
ExtractionRoutines uint16 //Number of goroutines to use for each file's extraction. Only applies to regular files. Defaults to 10.
} }
func DefaultOptions() *ExtractionOptions { func DefaultOptions() *ExtractionOptions {
return &ExtractionOptions{ return &ExtractionOptions{
Perm: 0777, LogOutput: os.Stdout,
Perm: 0777,
SimultaneousFiles: 10,
ExtractionRoutines: 10,
} }
} }
+236 -2
View File
@@ -4,8 +4,14 @@ import (
"errors" "errors"
"io" "io"
"io/fs" "io/fs"
"log"
"os"
"os/exec"
"path/filepath" "path/filepath"
"runtime"
"strconv"
"github.com/CalebQ42/squashfs/internal/routinemanager"
"github.com/CalebQ42/squashfs/squashfs" "github.com/CalebQ42/squashfs/squashfs"
"github.com/CalebQ42/squashfs/squashfs/data" "github.com/CalebQ42/squashfs/squashfs/data"
"github.com/CalebQ42/squashfs/squashfs/inode" "github.com/CalebQ42/squashfs/squashfs/inode"
@@ -162,6 +168,20 @@ func (f *File) initializeReaders() error {
return err return err
} }
func (f *File) deviceDevices() (maj uint32, min uint32) {
var dev uint32
if f.b.Inode.Type == inode.Char || f.b.Inode.Type == inode.Block {
dev = f.b.Inode.Data.(inode.Device).Dev
} else if f.b.Inode.Type == inode.EChar || f.b.Inode.Type == inode.EBlock {
dev = f.b.Inode.Data.(inode.EDevice).Dev
}
return dev >> 8, dev & 0x000FF
}
func (f *File) path() string {
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. // 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. // Uses default extraction options.
func (f *File) Extract(folder string) error { func (f *File) Extract(folder string) error {
@@ -170,6 +190,220 @@ func (f *File) Extract(folder string) error {
// Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // 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. // Allows setting various extraction options via ExtractionOptions.
func (f *File) ExtractWithOptions(folder string, op *ExtractionOptions) error { func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
//TODO if op.manager == nil {
op.manager = routinemanager.NewManager(op.SimultaneousFiles)
log.SetOutput(op.LogOutput)
}
switch f.b.Inode.Type {
case inode.Dir, inode.EDir:
d, err := f.b.ToDir(f.r.r)
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))
files := len(d.Entries)
for i := range d.Entries {
b, err := f.r.r.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)
}
if b.IsDir() {
files--
extDir := filepath.Join(path, b.Name)
err = os.Mkdir(extDir, 0777)
if err != nil {
if op.Verbose {
log.Println("Failed to create directory", path)
}
return errors.Join(errors.New("failed to create directory: "+path), err)
}
err = f.ExtractWithOptions(extDir, op)
if err != nil {
if op.Verbose {
log.Println("Failed to extract directory", path)
}
return errors.Join(errors.New("failed to extract directory: "+path), err)
}
} else {
fil := &File{
b: b,
r: f.r,
}
go func(fil *File, folder string) {
i := op.manager.Lock()
defer op.manager.Unlock(i)
errChan <- fil.ExtractWithOptions(folder, op)
}(fil, path)
}
}
var errCache []error
for i := 0; i < files; i++ {
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.r)
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)
}
if op.Verbose {
log.Println(f.path(), "extracted to", path)
}
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
if f.b.Inode.Type == inode.Char || f.b.Inode.Type == inode.EChar {
typ = "c"
} else if f.b.Inode.Type == inode.Block || f.b.Inode.Type == inode.EBlock {
typ = "b"
} else { //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.r)
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.r)
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
} }
+39 -16
View File
@@ -91,13 +91,20 @@ func (f *FS) Open(name string) (fs.File, error) {
} }
} }
if name == "." || name == "" { if name == "." || name == "" {
return &File{ return f.File(), nil
b: &f.d.Base,
r: f.r,
parent: f.parent,
}, nil
} }
split := strings.Split(name, "/") split := strings.Split(name, "/")
if split[0] == ".." {
if f.parent == nil { // root directory
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
} else {
return f.parent.Open(strings.Join(split[1:], "/"))
}
i, found := slices.BinarySearchFunc(f.d.Entries, split[0], func(e directory.Entry, name string) int { i, found := slices.BinarySearchFunc(f.d.Entries, split[0], func(e directory.Entry, name string) int {
return strings.Compare(e.Name, name) return strings.Compare(e.Name, name)
}) })
@@ -116,7 +123,7 @@ func (f *FS) Open(name string) (fs.File, error) {
return &File{ return &File{
b: b, b: b,
r: f.r, r: f.r,
parent: f.parent, parent: f,
}, nil }, nil
} }
if !b.IsDir() { if !b.IsDir() {
@@ -149,11 +156,7 @@ func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
} }
} }
if name == "." || name == "" { if name == "." || name == "" {
return (&File{ return f.File().ReadDir(-1)
b: &f.d.Base,
parent: f.parent,
r: f.r,
}).ReadDir(-1)
} }
fil, err := f.Open(name) fil, err := f.Open(name)
if err != nil { if err != nil {
@@ -196,11 +199,7 @@ func (f *FS) Stat(name string) (fs.FileInfo, error) {
} }
} }
if name == "." || name == "" { if name == "." || name == "" {
return (&File{ return f.File().Stat()
b: &f.d.Base,
parent: f.parent,
r: f.r,
}).Stat()
} }
fil, err := f.Open(name) fil, err := f.Open(name)
if err != nil { if err != nil {
@@ -236,6 +235,30 @@ func (f *FS) Sub(dir string) (fs.FS, error) {
return fil.(*File).FS() return fil.(*File).FS()
} }
// Extract the FS 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 *FS) Extract(folder string) error {
return f.File().Extract(folder)
}
// Extract the FS 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 *FS) ExtractWithOptions(folder string, op *ExtractionOptions) error {
return f.File().ExtractWithOptions(folder, op)
}
// Returns the FS as a *File
func (f *FS) File() *File {
return &File{
b: &f.d.Base,
parent: f.parent,
r: f.r,
}
}
func (f *FS) path() string { func (f *FS) path() string {
if f.parent == nil {
return f.d.Name
}
return filepath.Join(f.parent.path(), f.d.Name) return filepath.Join(f.parent.path(), f.d.Name)
} }
+25
View File
@@ -0,0 +1,25 @@
package routinemanager
type Manager struct {
channel chan uint16
maxRoutines uint16
}
func NewManager(maxRoutines uint16) *Manager {
m := &Manager{
maxRoutines: maxRoutines,
channel: make(chan uint16, maxRoutines),
}
for i := uint16(0); i < maxRoutines; i++ {
m.channel <- i
}
return m
}
func (m *Manager) Lock() uint16 {
return <-m.channel
}
func (m *Manager) Unlock(i uint16) {
m.channel <- i
}
+37
View File
@@ -129,3 +129,40 @@ func (b *Base) GetRegFileReaders(r *Reader) (*data.Reader, *data.FullReader, err
} }
return outRdr, outFull, nil return outRdr, outFull, nil
} }
func (b *Base) GetFullReader(r *Reader) (*data.FullReader, error) {
if !b.IsRegular() {
return nil, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
if b.Inode.Type == inode.Fil {
blockStart = uint64(b.Inode.Data.(inode.File).BlockStart)
fragIndex = b.Inode.Data.(inode.File).FragInd
fragOffset = b.Inode.Data.(inode.File).FragOffset
sizes = b.Inode.Data.(inode.File).BlockSizes
fragSize = uint64(b.Inode.Data.(inode.File).Size % r.Superblock.BlockSize)
} else {
blockStart = b.Inode.Data.(inode.EFile).BlockStart
fragIndex = b.Inode.Data.(inode.EFile).FragInd
fragOffset = b.Inode.Data.(inode.EFile).FragOffset
sizes = b.Inode.Data.(inode.EFile).BlockSizes
fragSize = b.Inode.Data.(inode.EFile).Size % uint64(r.Superblock.BlockSize)
}
outFull := data.NewFullReader(r.r, int64(blockStart), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
outFull.AddFrag(func() (io.Reader, error) {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return nil, err
}
frag := data.NewReader(toreader.NewReader(r.r, int64(ent.Start)), r.d, []uint32{ent.Size}, uint64(r.Superblock.BlockSize), r.Superblock.BlockSize)
frag.Read(make([]byte, fragOffset))
return io.LimitReader(frag, int64(fragSize)), nil
})
}
return outFull, nil
}