Compare commits

...

54 Commits

Author SHA1 Message Date
Caleb Gardner a829f0df9f Accidentally re-added caching_paged_reader 2025-06-07 03:13:33 -05:00
Caleb Gardner ea1b9f20cb Merge pull request #41 from CalebQ42/redo-extract
Redo extract
2025-06-07 03:12:55 -05:00
Caleb Gardner 74d9239c25 Merge branch 'main' into redo-extract 2025-06-07 03:12:42 -05:00
Caleb Gardner 07e9d5f123 Re-added FastOptions
Reset DefaultOptions to half NumCPU
2025-06-07 02:54:30 -05:00
Caleb Gardner 9fd87fe38a More efficient Table reading 2025-06-07 02:43:32 -05:00
Caleb Gardner de1b18fd1c Removed squashfs.test 2025-06-07 02:17:39 -05:00
Caleb Gardner 7d1458b3a4 Add memory & cpu profiles to gitignore 2025-06-07 02:09:43 -05:00
Caleb Gardner b057df9ada Remove memory profile 2025-06-07 02:09:21 -05:00
Caleb Gardner e1449da2f0 Fix memory leaks & inefficiencies 2025-06-07 02:07:45 -05:00
Caleb Gardner ddb81aade0 Rework caching_paged_reader into Table[T]
This *should* fix some issues with extraction due to race conditions
2025-06-06 15:46:25 -05:00
Caleb Gardner b2c8084f41 Minor tweaks 2025-06-06 15:46:25 -05:00
Will Murphy 0905141013 fix: prevent index out of range on long frag tables
Previously, reading fragment 512 would panic with index out of range.
Fix that panic by introducing an abstraction over reading blocks of
items, caching the intermediate result, and returning an item at a
particular index. The primary goal of this abstraction is to make edge
cases like requesting items on page boundaries easy to unit test for.

Additionally, fix unit tests by making t.Fatal calls protected by
nil checks on the error values.
2025-06-06 15:46:22 -05:00
Caleb Gardner 66042eab27 Merge pull request #30 from willmurphyscode/upstream-fix-out-of-range-panic
fix: prevent index out of range on long frag tables
2025-06-06 14:26:20 -05:00
Caleb Gardner 02d98b610c Merge branch 'main' into upstream-fix-out-of-range-panic 2025-06-06 14:26:10 -05:00
Caleb Gardner 81b663b48a Tested and working 2025-06-06 11:20:42 -05:00
Caleb Gardner 97214ca6ca Finished readers (theoretically) 2025-06-06 11:05:43 -05:00
Caleb Gardner 968dff82cb Fixed always showing hardlink info 2025-06-06 11:05:43 -05:00
Caleb Gardner 1f0868fb21 Added -show-hard-links to go-unsquashfs
Exposed the underlying squashfslow values for File and FS
2025-06-06 11:05:43 -05:00
Caleb Gardner f378136299 Added OpenFile to get a *squashfs.File instead of fs.File
Added -e to extract only specific files/folders
Only require the filename for -l, -ll, and -lln
2025-06-06 11:05:43 -05:00
Caleb Gardner 3378651686 Re-write data fullreader & reader
Moved directory and the rest of the inodes to manual decoding
2025-06-06 06:37:39 -05:00
Caleb Gardner ebbbc9e87e Merge pull request #40 from CalebQ42/go-unsquashfs-hardlinks
go-unsquashfs hardlinks & squashfslow access
2025-05-25 13:53:38 -05:00
Caleb Gardner 155999a8e3 Fixed always showing hardlink info 2025-05-25 13:45:51 -05:00
Caleb Gardner 7930f4402b Added -show-hard-links to go-unsquashfs
Exposed the underlying squashfslow values for File and FS
2025-05-25 13:35:40 -05:00
Caleb Gardner ada61a391c Added OpenFile to get a *squashfs.File instead of fs.File
Added -e to extract only specific files/folders
Only require the filename for -l, -ll, and -lln
2025-05-21 01:11:24 -05:00
Caleb Gardner f32cb520dc Zstd re-use 2025-04-16 18:02:19 -05:00
Caleb Gardner f991ddb1d4 Don't ignore permissions in benchmark tests 2025-04-16 16:22:32 -05:00
Caleb Gardner 4c8c9f0b47 Re-use zstd and zlib readers 2025-04-16 05:49:42 -05:00
Caleb Gardner 8a2556ea0d Remove test file 2025-04-16 05:16:58 -05:00
Caleb Gardner 33156751ca Merge pull request #39 from CalebQ42/perfExp
Performance
2025-04-10 11:26:24 -05:00
Caleb Gardner 6224c4be41 Further performance improvements
Further removed multiple pointer instances
Re-use decompression readers (except zstd due to bugs)
2025-04-10 11:20:55 -05:00
Caleb Gardner 6b0e9ef2c6 Reduce use of binary.Read and, by extention, reflection 2025-04-10 06:26:41 -05:00
Caleb Gardner 4490fc3873 Removed all the pointers 2025-04-10 02:15:24 -05:00
Caleb Gardner f242de2710 Better disabling of compression types 2025-03-17 06:53:29 -05:00
Caleb Gardner 88315ee384 Fix build flags 2025-03-17 06:28:31 -05:00
Caleb Gardner 1e2a8f4b75 go mod tidy 2025-03-17 06:22:24 -05:00
Caleb Gardner 863b03fb19 Updated README 2025-03-17 06:21:19 -05:00
Caleb Gardner d3f84344d1 Fix build flags in lzma.go & xz.go 2025-03-17 06:19:45 -05:00
Caleb Gardner ad24995b7b Change no_lzma and no_lzo to no_obsolete and no_gpl
Added build tags section to README
2025-03-17 06:16:25 -05:00
Caleb Gardner 638355ab71 Merge pull request #37 from afbjorklund/comp-none
Allow disabling lzo and lzma
2025-03-17 05:45:26 -05:00
Anders F Björklund 04d914d403 Allow disabling lzo and lzma
By setting the buildtags "no_lzo" and/or "no_lzma",
one can drop the library dependency on lzo and lzma.

The same could be done for xz as well, but there are
still lots of archives using xz compression out there.
2025-03-16 13:56:04 +01:00
Caleb Gardner 7323fe56f6 Merge pull request #35 from afbjorklund/list
Add list option to unsquashfs
2025-03-15 15:48:41 -05:00
Caleb Gardner 6286da31e1 Merge branch 'main' into list 2025-03-15 15:48:06 -05:00
Caleb Gardner 77c87a9653 Merge pull request #34 from afbjorklund/unsquashfs-offset
Allow mounting with an offset
2025-03-15 15:34:04 -05:00
Anders F Björklund e6b0b83dcb Add support for uid/gid 2025-03-15 17:50:02 +01:00
Anders F Björklund cef9090210 Add support for symlinks 2025-03-15 17:46:02 +01:00
Anders F Björklund 24a9457c6b Refactor: export FileInfo 2025-03-15 17:35:40 +01:00
Anders F Björklund e0c1309ed4 Add list option to unsquashfs 2025-03-15 17:32:27 +01:00
Anders F Björklund 8b475b6cc4 Allow mounting with an offset
When mounting squashfs images embedded in apptainer image,
using offset means we don't need to use a temporary copy.
2025-03-15 17:26:59 +01:00
Caleb Gardner 3a48a0bcdc Remove t.Fatal at end of single file test 2025-03-12 00:11:29 -05:00
Caleb Gardner f11416493e Apply FileMode fixes to Inode.Mode() 2025-03-12 00:09:02 -05:00
Caleb Gardner 619bb023b1 Fix missing fileInfo.Mode() types 2025-03-12 00:03:58 -05:00
Caleb Gardner 38e4761d21 Merge pull request #33 from afbjorklund/fileinfo-symlink
Properly show symlinks in Mode
2025-03-11 23:51:01 -05:00
Anders F Björklund 06d2ef3056 Properly show symlinks in Mode
Previously they were extracted OK (as symlinks), but shown
as regular files with length 0 when getting the file info.
2025-03-11 18:46:31 +01:00
Will Murphy 0253a76dbe fix: prevent index out of range on long frag tables
Previously, reading fragment 512 would panic with index out of range.
Fix that panic by introducing an abstraction over reading blocks of
items, caching the intermediate result, and returning an item at a
particular index. The primary goal of this abstraction is to make edge
cases like requesting items on page boundaries easy to unit test for.

Additionally, fix unit tests by making t.Fatal calls protected by
nil checks on the error values.
2024-11-26 07:38:33 -05:00
36 changed files with 1275 additions and 818 deletions
+5
View File
@@ -1,2 +1,7 @@
testing
/go-unsquashfs
squashfs.test
# Memory and CPU pprof profiles
mem.out
cpu.out
+9 -6
View File
@@ -11,6 +11,10 @@ Currently has support for reading squashfs files and extracting files and folder
Special thanks to <https://dr-emann.github.io/squashfs/> for some VERY important information in an easy to understand format.
Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree/master/internal/squashfs) as I referenced it to figure some things out (and double check others).
## Build tags
As of `v1.1.0` this library has two optional build tags: `no_gpl` and `no_obsolete`. `no_gpl` disables the ability to read archives with lzo compression due to the library's gpl license. `no_obsolete` removes "obsolete" compression types for a reduced compilation size; currently this only disable lzma compression since it's superseded by xz.
## FUSE
As of `v1.0`, FUSE capabilities has been moved to [a separate library](https://github.com/CalebQ42/squashfuse).
@@ -24,15 +28,14 @@ As of `v1.0`, FUSE capabilities has been moved to [a separate library](https://g
## Issues
* Significantly slower then `unsquashfs` when nested images
* Noticably slower then `unsquashfs` for extraction, especially on larger images.
* This seems to be related to above along with the general optimization of `unsquashfs` and it's compression libraries.
* Not to mention it's written in C
* Times seem to be largely dependent on file tree size and compression type.
* My main testing image (~100MB) using Zstd takes about 5x longer.
* An Arch Linux airootfs image (~780MB) using XZ compression with LZMA filters takes about 30x longer.
* A Tensorflow docker image (~3.3GB) using Zstd takes about 12x longer.
* My main testing image (~100MB) using Zstd takes ~2x longer.
* An Arch Linux airootfs image (~780MB) using XZ compression with LZMA filters takes ~28x longer.
* A Tensorflow docker image (~3.3GB) using Zstd takes ~3x longer.
Note: These numbers are using `FastOptions()`. `DefaultOptions()` takes about 2x longer.
Note: These numbers are using `FastOptions()`. `DefaultOptions()` takes ~2x longer.
## Recommendations on Usage
+116 -5
View File
@@ -4,16 +4,116 @@ import (
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/CalebQ42/squashfs"
squashfslow "github.com/CalebQ42/squashfs/low"
)
func userName(uid int, numeric bool) string {
us := strconv.Itoa(uid)
if numeric {
return us
}
if u, err := user.LookupId(us); err == nil {
return u.Username
}
return us
}
func groupName(gid int, numeric bool) string {
gs := strconv.Itoa(gid)
if numeric {
return gs
}
if g, err := user.LookupGroupId(gs); err == nil {
return g.Name
}
return gs
}
var hardLinks = make(map[uint32]string)
func printFile(rdr *squashfs.Reader, path string, f *squashfs.File) {
path = filepath.Join(path, f.Low.Name)
fi, _ := f.Stat()
sfi := fi.(squashfs.FileInfo)
owner := fmt.Sprintf("%s/%s",
userName(sfi.Uid(), *numeric),
groupName(sfi.Gid(), *numeric))
var link string
var isHardLink bool
if *showHardLinks {
link, isHardLink = hardLinks[f.Low.Inode.Num]
if !isHardLink {
hardLinks[f.Low.Inode.Num] = path
}
}
var size int64
if isHardLink {
size = 0
} else {
size = fi.Size()
}
if sfi.IsSymlink() {
link = " -> " + sfi.SymlinkPath()
} else if isHardLink {
link = " link to " + link
}
fmt.Printf("%s %s %*d %s %s%s\n",
strings.ToLower(fi.Mode().String()),
owner, 26-len(owner), size,
fi.ModTime().Format("2006-01-02 15:04"),
path, link)
if f.IsDir() {
fs, _ := f.FS()
printDir(rdr, path, fs)
}
}
func printDir(rdr *squashfs.Reader, path string, f squashfs.FS) {
var base squashfslow.FileBase
var fil squashfs.File
var err error
for _, e := range f.LowDir.Entries {
base, err = rdr.Low.BaseFromEntry(e)
if err != nil {
panic(err)
}
fil = rdr.FileFromBase(base, f)
printFile(rdr, path, &fil)
}
}
var (
verbose *bool
list *bool
long *bool
numeric *bool
offset *int64
ignore *bool
file *string
showHardLinks *bool
)
func main() {
verbose := flag.Bool("v", false, "Verbose")
ignore := flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755")
verbose = flag.Bool("v", false, "Verbose")
list = flag.Bool("l", false, "List")
long = flag.Bool("ll", false, "List with attributes")
numeric = flag.Bool("lln", false, "List with attributes and numeric ids")
showHardLinks = flag.Bool("show-hard-links", false, "When used with ll or lln, shows hard links")
offset = flag.Int64("o", 0, "Offset")
ignore = flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755")
file = flag.String("e", "", "File or folder to extract")
flag.Parse()
if len(flag.Args()) < 2 {
if (*list || *long || *numeric) && flag.NArg() < 1 {
fmt.Println("Please provide a file name")
os.Exit(0)
} else if (!*list && !*long && !*numeric) && flag.NArg() < 2 {
fmt.Println("Please provide a file name and extraction path")
os.Exit(0)
}
@@ -21,15 +121,26 @@ func main() {
if err != nil {
panic(err)
}
r, err := squashfs.NewReader(f)
r, err := squashfs.NewReaderAtOffset(f, *offset)
if err != nil {
panic(err)
}
extractFil := r.File()
if *file != "" {
extractFil, err = r.OpenFile(*file)
if err != nil {
panic(err)
}
}
if *list || *long || *numeric {
printFile(&r, "", extractFil)
return
}
op := squashfs.DefaultOptions()
op.Verbose = *verbose
op.IgnorePerm = *ignore
n := time.Now()
err = r.ExtractWithOptions(flag.Arg(1), op)
err = extractFil.ExtractWithOptions(flag.Arg(1), op)
if err != nil {
panic(err)
}
+14 -26
View File
@@ -4,46 +4,34 @@ import (
"io"
"io/fs"
"runtime"
"github.com/CalebQ42/squashfs/internal/routinemanager"
"sync"
)
type ExtractionOptions struct {
manager *routinemanager.Manager
LogOutput io.Writer //Where the verbose log should write.
DereferenceSymlink bool //Replace symlinks with the target file.
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.
IgnorePerm bool //Ignore file's permissions and instead use Perm.
Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0777.
SimultaneousFiles uint16 //Number of files to process in parallel. Default set based on runtime.NumCPU().
ExtractionRoutines uint16 //Number of goroutines to use for each file's extraction. Only applies to regular files. Default set based on runtime.NumCPU().
dispatcher chan struct{} // Limits the amount of work being done simultaneously.
fullRdrPool sync.Pool // Pool for data.FullReader results.
LogOutput io.Writer //Where the verbose log should write.
DereferenceSymlink bool //Replace symlinks with the target file.
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.
IgnorePerm bool //Ignore file's permissions and instead use Perm.
Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0777.
ExtractionRoutines uint16 //The number of threads to use during extraction. Defaults to a number based on runtime.NumCPU().
SimultaneousFiles uint16 //Depreciated: Only use ExtractionRoutines
}
// The default extraction options.
// The default extraction options. Uses half of your CPU cores.
func DefaultOptions() *ExtractionOptions {
cores := uint16(runtime.NumCPU() / 2)
var files, routines uint16
if cores <= 4 {
files = 1
routines = cores
} else {
files = cores - 4
routines = 4
}
return &ExtractionOptions{
Perm: 0777,
SimultaneousFiles: files,
ExtractionRoutines: routines,
ExtractionRoutines: uint16(runtime.NumCPU() / 2),
}
}
// Less limited default options. Can run up 2x faster than DefaultOptions.
// Tends to use all available CPU resources.
// Faster extraction option. Uses all CPU cores.
func FastOptions() *ExtractionOptions {
return &ExtractionOptions{
Perm: 0777,
SimultaneousFiles: uint16(runtime.NumCPU()),
ExtractionRoutines: uint16(runtime.NumCPU()),
}
}
+101 -67
View File
@@ -10,8 +10,8 @@ import (
"path/filepath"
"runtime"
"strconv"
"sync"
"github.com/CalebQ42/squashfs/internal/routinemanager"
squashfslow "github.com/CalebQ42/squashfs/low"
"github.com/CalebQ42/squashfs/low/data"
"github.com/CalebQ42/squashfs/low/inode"
@@ -19,49 +19,48 @@ import (
// File represents a file inside a squashfs archive.
type File struct {
full *data.FullReader
rdr *data.Reader
parent *FS
full data.FullReader
rdr data.Reader
rdrInit bool
parent FS
r *Reader
b squashfslow.FileBase
Low 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,
func (r *Reader) FileFromBase(b squashfslow.FileBase, parent FS) File {
return File{
Low: b,
parent: parent,
r: r,
}
}
func (f *File) FS() (*FS, error) {
func (f File) FS() (FS, error) {
if !f.IsDir() {
return nil, errors.New("not a directory")
return FS{}, errors.New("not a directory")
}
d, err := f.b.ToDir(&f.r.Low)
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
return nil, err
return FS{}, err
}
return &FS{d: d, parent: f.parent, r: f.r}, nil
return FS{LowDir: 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 {
if f.rdr != nil {
return f.rdr.Close()
}
f.rdr = nil
f.full = nil
f.rdr.Close()
f.full.Close()
f.rdrInit = false
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 {
func (f File) GetSymlinkFile() fs.File {
if !f.IsSymlink() {
return nil
}
@@ -76,22 +75,22 @@ func (f *File) GetSymlinkFile() fs.File {
}
// Returns whether the file is a directory.
func (f *File) IsDir() bool {
return f.b.IsDir()
func (f File) IsDir() bool {
return f.Low.IsDir()
}
// Returns whether the file is a regular file.
func (f *File) IsRegular() bool {
return f.b.IsRegular()
func (f File) IsRegular() bool {
return f.Low.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) IsSymlink() bool {
return f.Low.Inode.Type == inode.Sym || f.Low.Inode.Type == inode.ESym
}
func (f *File) Mode() fs.FileMode {
return f.b.Inode.Mode()
func (f File) Mode() fs.FileMode {
return f.Low.Inode.Mode()
}
// Read reads the data from the file. Only works if file is a normal file.
@@ -99,7 +98,7 @@ func (f *File) Read(b []byte) (int, error) {
if !f.IsRegular() {
return 0, errors.New("file is not a regular file")
}
if f.rdr == nil {
if !f.rdrInit {
err := f.initializeReaders()
if err != nil {
return 0, err
@@ -114,7 +113,7 @@ 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)
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
return nil, err
}
@@ -127,7 +126,7 @@ func (f *File) ReadDir(n int) ([]fs.DirEntry, error) {
}
}
var out []fs.DirEntry
var fi fileInfo
var fi FileInfo
for _, e := range d.Entries[start:end] {
fi, err = f.r.newFileInfo(e)
if err != nil {
@@ -141,17 +140,25 @@ func (f *File) ReadDir(n int) ([]fs.DirEntry, error) {
}
// Returns the file's fs.FileInfo
func (f *File) Stat() (fs.FileInfo, error) {
return newFileInfo(f.b.Name, &f.b.Inode), nil
func (f File) Stat() (fs.FileInfo, error) {
uid, err := f.Low.Uid(&f.r.Low)
if err != nil {
return nil, err
}
gid, err := f.Low.Gid(&f.r.Low)
if err != nil {
return nil, err
}
return newFileInfo(f.Low.Name, uid, gid, &f.Low.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 {
func (f File) SymlinkPath() string {
switch f.Low.Inode.Type {
case inode.Sym:
return string(f.b.Inode.Data.(inode.Symlink).Target)
return string(f.Low.Inode.Data.(inode.Symlink).Target)
case inode.ESym:
return string(f.b.Inode.Data.(inode.ESymlink).Target)
return string(f.Low.Inode.Data.(inode.ESymlink).Target)
}
return ""
}
@@ -162,7 +169,7 @@ func (f *File) WriteTo(w io.Writer) (int64, error) {
if !f.IsRegular() {
return 0, errors.New("file is not a regular file")
}
if f.full == nil {
if !f.rdrInit {
err := f.initializeReaders()
if err != nil {
return 0, err
@@ -173,39 +180,53 @@ func (f *File) WriteTo(w io.Writer) (int64, error) {
func (f *File) initializeReaders() error {
var err error
f.rdr, f.full, err = f.b.GetRegFileReaders(&f.r.Low)
f.rdr, f.full, err = f.Low.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) {
func (f File) deviceDevices() (maj uint32, min uint32) {
var dev uint32
switch f.b.Inode.Type {
switch f.Low.Inode.Type {
case inode.Char, inode.Block:
dev = f.b.Inode.Data.(inode.Device).Dev
dev = f.Low.Inode.Data.(inode.Device).Dev
case inode.EChar, inode.EBlock:
dev = f.b.Inode.Data.(inode.EDevice).Dev
dev = f.Low.Inode.Data.(inode.EDevice).Dev
}
return dev >> 8, dev & 0x000FF
}
func (f *File) path() string {
if f.parent == nil {
return f.b.Name
func (f File) path() string {
if f.parent.LowDir.Name == "" {
return f.Low.Name
}
return filepath.Join(f.parent.path(), f.b.Name)
return filepath.Join(f.parent.path(), f.Low.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 {
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)
func (f File) ExtractWithOptions(path string, op *ExtractionOptions) error {
if op.dispatcher == nil {
op.fullRdrPool = sync.Pool{
New: func() any {
return &data.BlockResults{}
},
}
op.dispatcher = make(chan struct{}, op.ExtractionRoutines)
for range op.ExtractionRoutines {
op.dispatcher <- struct{}{}
}
if op.LogOutput != nil {
log.SetOutput(op.LogOutput)
}
@@ -217,13 +238,15 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
return err
}
}
switch f.b.Inode.Type {
switch f.Low.Inode.Type {
case inode.Dir, inode.EDir:
d, err := f.b.ToDir(&f.r.Low)
<-op.dispatcher
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
if op.Verbose {
log.Println("Failed to create squashfs.Directory for", path)
}
op.dispatcher <- struct{}{}
return errors.Join(errors.New("failed to create squashfs.Directory: "+path), err)
}
errChan := make(chan error, len(d.Entries))
@@ -236,19 +259,21 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
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() {
<-op.dispatcher
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)
}
op.dispatcher <- struct{}{}
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)
fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent))
op.dispatcher <- struct{}{}
err = fil.ExtractWithOptions(extDir, op)
if err != nil {
if op.Verbose {
log.Println("Failed to extract directory", path)
@@ -260,12 +285,12 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
} 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)
}
op.dispatcher <- struct{}{}
var errCache []error
for range d.Entries {
err := <-errChan
@@ -277,23 +302,28 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
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)
<-op.dispatcher
path = filepath.Join(path, f.Low.Name)
outFil, err := os.Create(path)
if err != nil {
if op.Verbose {
log.Println("Failed to create file", path)
}
op.dispatcher <- struct{}{}
return errors.Join(errors.New("failed to create file: "+path), err)
}
defer outFil.Close()
full, err := f.b.GetFullReader(&f.r.Low)
full, err := f.Low.GetFullReader(&f.r.Low)
defer full.Close()
if err != nil {
if op.Verbose {
log.Println("Failed to create full reader for", path)
}
op.dispatcher <- struct{}{}
return errors.Join(errors.New("failed to create full reader: "+path), err)
}
full.SetGoroutineLimit(op.ExtractionRoutines)
full.SetDispatcherPool(op.dispatcher, &op.fullRdrPool)
op.dispatcher <- struct{}{}
_, err = full.WriteTo(outFil)
if err != nil {
if op.Verbose {
@@ -302,6 +332,8 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
return errors.Join(errors.New("failed to write file: "+path), err)
}
case inode.Sym, inode.ESym:
<-op.dispatcher
defer func() { op.dispatcher <- struct{}{} }()
symPath := f.SymlinkPath()
if op.DereferenceSymlink {
filTmp := f.GetSymlinkFile()
@@ -312,11 +344,11 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
return errors.New("failed to get symlink's file")
}
fil := filTmp.(*File)
fil.b.Name = f.b.Name
fil.Low.Name = f.Low.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))
log.Println("Failed to extract symlink's file:", filepath.Join(path, f.Low.Name))
}
return errors.Join(errors.New("failed to extract symlink's file: "+path), err)
}
@@ -339,7 +371,7 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
return errors.Join(errors.New("failed to extract symlink's file: "+extractLoc), err)
}
}
path = filepath.Join(path, f.b.Name)
path = filepath.Join(path, f.Low.Name)
err := os.Symlink(f.SymlinkPath(), path)
if err != nil {
if op.Verbose {
@@ -349,6 +381,8 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
}
}
case inode.Char, inode.EChar, inode.Block, inode.EBlock, inode.Fifo, inode.EFifo:
<-op.dispatcher
defer func() { op.dispatcher <- struct{}{} }()
if runtime.GOOS == "windows" {
if op.Verbose {
log.Println(f.path(), "ignored. A device link and can't be created on Windows.")
@@ -362,9 +396,9 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
}
return errors.Join(errors.New("mknot command not found"), err)
}
path = filepath.Join(path, f.b.Name)
path = filepath.Join(path, f.Low.Name)
var typ string
switch f.b.Inode.Type {
switch f.Low.Inode.Type {
case inode.Char, inode.EChar:
typ = "c"
case inode.Block, inode.EBlock:
@@ -400,7 +434,7 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
}
return nil
default:
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.b.Inode.Type)))
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.Low.Inode.Type)))
}
if op.Verbose {
log.Println(f.path(), "extracted to", path)
@@ -408,7 +442,7 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
if op.IgnorePerm {
return nil
}
uid, err := f.b.Uid(&f.r.Low)
uid, err := f.Low.Uid(&f.r.Low)
if err != nil {
if op.Verbose {
log.Println("Failed to get uid for", path)
@@ -416,7 +450,7 @@ func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error {
}
return nil
}
gid, err := f.b.Gid(&f.r.Low)
gid, err := f.Low.Gid(&f.r.Low)
if err != nil {
if op.Verbose {
log.Println("Failed to get gid for", path)
+71 -14
View File
@@ -8,62 +8,119 @@ import (
"github.com/CalebQ42/squashfs/low/inode"
)
type fileInfo struct {
type FileInfo struct {
name string
uid uint32
gid uint32
size int64
target string
perm uint32
modTime uint32
fileType uint16
}
func (r Reader) newFileInfo(e directory.Entry) (fileInfo, error) {
i, err := r.Low.InodeFromEntry(e)
func (r Reader) newFileInfo(e directory.Entry) (FileInfo, error) {
b, err := r.Low.BaseFromEntry(e)
if err != nil {
return fileInfo{}, err
return FileInfo{}, err
}
return newFileInfo(e.Name, &i), nil
uid, err := b.Uid(&r.Low)
if err != nil {
return FileInfo{}, err
}
gid, err := b.Gid(&r.Low)
if err != nil {
return FileInfo{}, err
}
return newFileInfo(e.Name, uid, gid, &b.Inode), nil
}
func newFileInfo(name string, i *inode.Inode) fileInfo {
func newFileInfo(name string, uid, gid uint32, i *inode.Inode) FileInfo {
var size int64
var target string
switch i.Type {
case inode.Fil:
size = int64(i.Data.(inode.File).Size)
case inode.EFil:
size = int64(i.Data.(inode.EFile).Size)
case inode.Sym:
target = string(i.Data.(inode.Symlink).Target)
case inode.ESym:
target = string(i.Data.(inode.ESymlink).Target)
}
return fileInfo{
return FileInfo{
name: name,
uid: uid,
gid: gid,
size: size,
target: target,
perm: uint32(i.Perm),
modTime: i.ModTime,
fileType: i.Type,
}
}
func (f fileInfo) Name() string {
func (f FileInfo) Name() string {
return f.name
}
func (f fileInfo) Size() int64 {
func (f FileInfo) Uid() int {
return int(f.uid)
}
func (f FileInfo) Gid() int {
return int(f.gid)
}
func (f FileInfo) Size() int64 {
return f.size
}
func (f fileInfo) Mode() fs.FileMode {
if f.IsDir() {
func (f FileInfo) SymlinkPath() string {
return f.target
}
func (f FileInfo) Mode() fs.FileMode {
switch f.fileType {
case inode.Dir, inode.EDir:
return fs.FileMode(f.perm | uint32(fs.ModeDir))
case inode.Sym, inode.ESym:
return fs.FileMode(f.perm | uint32(fs.ModeSymlink))
case inode.Char, inode.EChar, inode.Block, inode.EBlock:
return fs.FileMode(f.perm | uint32(fs.ModeDevice))
case inode.Fifo, inode.EFifo:
return fs.FileMode(f.perm | uint32(fs.ModeNamedPipe))
case inode.Sock, inode.ESock:
return fs.FileMode(f.perm | uint32(fs.ModeSocket))
}
return fs.FileMode(f.perm)
}
func (f fileInfo) ModTime() time.Time {
func (f FileInfo) ModTime() time.Time {
return time.Unix(int64(f.modTime), 0)
}
func (f fileInfo) IsDir() bool {
func (f FileInfo) IsDir() bool {
return f.fileType == inode.Dir || f.fileType == inode.EDir
}
func (f fileInfo) Sys() any {
func (f FileInfo) IsSymlink() bool {
return f.fileType == inode.Sym || f.fileType == inode.ESym
}
func (f FileInfo) IsDevice() bool {
return f.fileType == inode.Block || f.fileType == inode.EBlock ||
f.fileType == inode.Char || f.fileType == inode.EChar
}
func (f FileInfo) IsFifo() bool {
return f.fileType == inode.Fifo || f.fileType == inode.EFifo
}
func (f FileInfo) IsSocket() bool {
return f.fileType == inode.Sock || f.fileType == inode.ESock
}
func (f FileInfo) Sys() any {
return nil
}
+39 -29
View File
@@ -17,15 +17,15 @@ import (
type FS struct {
r *Reader
parent *FS
d squashfslow.Directory
LowDir squashfslow.Directory
}
// Creates a new *FS from the given squashfs.directory
func (r *Reader) FSFromDirectory(d squashfslow.Directory, parent *FS) *FS {
return &FS{
d: d,
func (r *Reader) FSFromDirectory(d squashfslow.Directory, parent FS) FS {
return FS{
LowDir: d,
r: r,
parent: parent,
parent: &parent,
}
}
@@ -42,10 +42,10 @@ func (f *FS) Glob(pattern string) (out []string, err error) {
}
}
split := strings.Split(pattern, "/")
for i := range f.d.Entries {
if match, _ := path.Match(split[0], f.d.Entries[i].Name); match {
for i := range f.LowDir.Entries {
if match, _ := path.Match(split[0], f.LowDir.Entries[i].Name); match {
if len(split) == 1 {
out = append(out, f.d.Entries[i].Name)
out = append(out, f.LowDir.Entries[i].Name)
continue
}
sub, err := f.Sub(split[0])
@@ -81,7 +81,7 @@ func (f *FS) Glob(pattern string) (out []string, err error) {
}
}
for i := range subGlob {
subGlob[i] = f.d.Name + "/" + subGlob[i]
subGlob[i] = f.LowDir.Name + "/" + subGlob[i]
}
out = append(out, subGlob...)
}
@@ -90,7 +90,11 @@ func (f *FS) Glob(pattern string) (out []string, err error) {
}
// Opens the file at name. Returns a *File as an fs.File.
func (f *FS) Open(name string) (fs.File, error) {
func (f FS) Open(name string) (fs.File, error) {
return f.OpenFile(name)
}
func (f FS) OpenFile(name string) (*File, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
@@ -111,10 +115,10 @@ func (f *FS) Open(name string) (fs.File, error) {
Err: fs.ErrNotExist,
}
} else {
return f.parent.Open(strings.Join(split[1:], "/"))
return f.parent.OpenFile(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.LowDir.Entries, split[0], func(e directory.Entry, name string) int {
return strings.Compare(e.Name, name)
})
if !found {
@@ -124,13 +128,13 @@ func (f *FS) Open(name string) (fs.File, error) {
Err: fs.ErrNotExist,
}
}
b, err := f.r.Low.BaseFromEntry(f.d.Entries[i])
b, err := f.r.Low.BaseFromEntry(f.LowDir.Entries[i])
if err != nil {
return nil, err
}
if len(split) == 1 {
return &File{
b: b,
Low: b,
r: f.r,
parent: f,
}, nil
@@ -142,16 +146,16 @@ func (f *FS) Open(name string) (fs.File, error) {
Err: fs.ErrNotExist,
}
}
d, err := b.ToDir(&f.r.Low)
d, err := b.ToDir(f.r.Low)
if err != nil {
return nil, err
}
return f.r.FSFromDirectory(d, f).Open(strings.Join(split[1:], "/"))
return f.r.FSFromDirectory(d, f).OpenFile(strings.Join(split[1:], "/"))
}
// Returns all DirEntry's for the directory at name.
// If name is not a directory, returns an error.
func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
@@ -171,7 +175,7 @@ func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
}
// Returns the contents of the file at name.
func (f *FS) ReadFile(name string) (out []byte, err error) {
func (f FS) ReadFile(name string) (out []byte, err error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
@@ -194,7 +198,7 @@ func (f *FS) ReadFile(name string) (out []byte, err error) {
}
// Returns the fs.FileInfo for the file at name.
func (f *FS) Stat(name string) (fs.FileInfo, error) {
func (f FS) Stat(name string) (fs.FileInfo, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
@@ -214,7 +218,7 @@ func (f *FS) Stat(name string) (fs.FileInfo, error) {
}
// Returns the FS at dir
func (f *FS) Sub(dir string) (fs.FS, error) {
func (f FS) Sub(dir string) (fs.FS, error) {
dir = filepath.Clean(dir)
if !fs.ValidPath(dir) {
return nil, &fs.PathError{
@@ -242,28 +246,34 @@ func (f *FS) Sub(dir string) (fs.FS, error) {
// 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 {
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 {
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 {
func (f FS) File() *File {
if f.parent != nil {
return &File{
Low: f.LowDir.FileBase,
parent: *f.parent,
r: f.r,
}
}
return &File{
b: f.d.FileBase,
parent: f.parent,
r: f.r,
Low: f.LowDir.FileBase,
r: f.r,
}
}
func (f *FS) path() string {
func (f FS) path() string {
if f.parent == nil {
return f.d.Name
return f.LowDir.Name
}
return filepath.Join(f.parent.path(), f.d.Name)
return filepath.Join(f.parent.path(), f.LowDir.Name)
}
-4
View File
@@ -1,9 +1,5 @@
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs=
+18 -3
View File
@@ -3,13 +3,28 @@ package decompress
import (
"bytes"
"io"
"sync"
"github.com/pierrec/lz4/v4"
)
type Lz4 struct{}
type Lz4 struct {
pool sync.Pool
}
func (l Lz4) Decompress(data []byte) ([]byte, error) {
rdr := lz4.NewReader(bytes.NewReader(data))
func NewLz4() *Lz4 {
return &Lz4{
pool: sync.Pool{
New: func() any {
return lz4.NewReader(nil)
},
},
}
}
func (l *Lz4) Decompress(data []byte) ([]byte, error) {
rdr := l.pool.Get().(*lz4.Reader)
defer l.pool.Put(rdr)
rdr.Reset(bytes.NewReader(data))
return io.ReadAll(rdr)
}
+6
View File
@@ -1,3 +1,5 @@
//go:build !no_obsolete
package decompress
import (
@@ -9,6 +11,10 @@ import (
type Lzma struct{}
func NewLzma() (Lzma, error) {
return Lzma{}, nil
}
func (l Lzma) Decompress(data []byte) ([]byte, error) {
rdr, err := lzma.NewReader(bytes.NewReader(data))
if err != nil {
+17
View File
@@ -0,0 +1,17 @@
//go:build no_obsolete
package decompress
import (
"errors"
)
type Lzma struct{}
func NewLzma() (Lzma, error) {
return Lzma{}, errors.New("lzma compression is disable in this build with no_obsolete")
}
func (l Lzma) Decompress(data []byte) ([]byte, error) {
return nil, errors.New("lzma compression is disable in this build with no_obsolete")
}
+6
View File
@@ -1,3 +1,5 @@
//go:build !no_gpl
package decompress
import (
@@ -8,6 +10,10 @@ import (
type Lzo struct{}
func NewLzo() (Lzo, error) {
return Lzo{}, nil
}
func (l Lzo) Decompress(data []byte) ([]byte, error) {
return lzo.Decompress1X(bytes.NewReader(data), len(data), 0)
}
+15
View File
@@ -0,0 +1,15 @@
//go:build no_gpl
package decompress
import "errors"
type Lzo struct{}
func NewLzo() (Lzo, error) {
return Lzo{}, errors.New("lzo compression is disable in this build with no_gpl")
}
func (l Lzo) Decompress(data []byte) ([]byte, error) {
return nil, errors.New("lzo compression is disable in this build with no_gpl")
}
+19 -3
View File
@@ -3,14 +3,30 @@ package decompress
import (
"bytes"
"io"
"sync"
"github.com/therootcompany/xz"
)
type Xz struct{}
type Xz struct {
pool sync.Pool
}
func (x Xz) Decompress(data []byte) ([]byte, error) {
rdr, err := xz.NewReader(bytes.NewReader(data), 0)
func NewXz() *Xz {
return &Xz{
pool: sync.Pool{
New: func() any {
rdr, _ := xz.NewReader(nil, 0)
return rdr
},
},
}
}
func (x *Xz) Decompress(data []byte) ([]byte, error) {
rdr := x.pool.Get().(*xz.Reader)
defer x.pool.Put(rdr)
err := rdr.Reset(bytes.NewReader(data))
if err != nil {
return nil, err
}
+20 -6
View File
@@ -2,17 +2,31 @@ package decompress
import (
"bytes"
"compress/zlib"
"io"
"sync"
"github.com/klauspost/compress/zlib"
)
type Zlib struct{}
type Zlib struct {
pool sync.Pool
}
func (z Zlib) Decompress(data []byte) ([]byte, error) {
rdr, err := zlib.NewReader(bytes.NewReader(data))
func NewZlib() *Zlib {
return &Zlib{}
}
func (z *Zlib) Decompress(data []byte) ([]byte, error) {
rdr := z.pool.Get()
defer z.pool.Put(rdr)
var err error
if rdr == nil {
rdr, err = zlib.NewReader(bytes.NewReader(data))
} else {
err = rdr.(zlib.Resetter).Reset(bytes.NewReader(data), nil)
}
if err != nil {
return nil, err
}
defer rdr.Close()
return io.ReadAll(rdr)
return io.ReadAll(rdr.(io.ReadCloser))
}
+12 -7
View File
@@ -1,19 +1,24 @@
package decompress
import (
"bytes"
"io"
"github.com/klauspost/compress/zstd"
)
type Zstd struct{}
type Zstd struct {
rdr *zstd.Decoder
}
func NewZstd() Zstd {
rdr, _ := zstd.NewReader(nil, zstd.WithDecoderLowmem(true))
return Zstd{
rdr: rdr,
}
}
func (z Zstd) Decompress(data []byte) ([]byte, error) {
rdr, err := zstd.NewReader(bytes.NewReader(data))
dat, err := z.rdr.DecodeAll(data, nil)
if err != nil {
return nil, err
}
defer rdr.Close()
return io.ReadAll(rdr)
return dat, err
}
+6 -5
View File
@@ -14,8 +14,8 @@ type Reader struct {
curOffset uint16
}
func NewReader(r io.Reader, d decompress.Decompressor) *Reader {
return &Reader{
func NewReader(r io.Reader, d decompress.Decompressor) Reader {
return Reader{
r: r,
d: d,
}
@@ -23,14 +23,15 @@ func NewReader(r io.Reader, d decompress.Decompressor) *Reader {
func (r *Reader) advance() error {
r.curOffset = 0
var size uint16
err := binary.Read(r.r, binary.LittleEndian, &size)
dat := make([]byte, 2)
_, err := r.r.Read(dat)
if err != nil {
return err
}
size := binary.LittleEndian.Uint16(dat)
realSize := size &^ 0x8000
r.dat = make([]byte, realSize)
err = binary.Read(r.r, binary.LittleEndian, &r.dat)
_, err = r.r.Read(r.dat)
if err != nil {
return err
}
+2
View File
@@ -1,3 +1,5 @@
# Lower-Level Squashfs
This library is a lower level version of the main [squashfs](https://github.com/CalebQ42/squashfs) library that doesn't try to be easy to use and exposes a lot of information that is not necesary for must use cases.
I will try to keep the API stable, but it is not guarenteed.
+127
View File
@@ -0,0 +1,127 @@
package squashfslow
// TODO: Make work
// func requireNoError(t *testing.T, err error) {
// t.Helper()
// if err != nil {
// t.Fatal(err)
// }
// }
// func assertEqual(t *testing.T, want int, got int) {
// t.Helper()
// if want != got {
// t.Errorf("want %d, got %d", want, got)
// }
// }
// func assertLength(t *testing.T, want int, slice []int) {
// t.Helper()
// if len(slice) != want {
// t.Errorf("want len %d, got %d", want, len(slice))
// }
// }
// func assertErrorIs(t *testing.T, err error, wantErr error) {
// t.Helper()
// if err == nil {
// t.Errorf("want %s, got nil", wantErr)
// return
// }
// if !errors.Is(err, wantErr) {
// t.Errorf("want %s, got %v", wantErr, err)
// }
// }
// func TestCachingPagedReader(t *testing.T) {
// // Mock readBlocks function
// mockReadNMore := func(startBlock, numItems int) ([]int, error) {
// if startBlock < 0 {
// return nil, errors.New("invalid block start")
// }
// var result []int
// for i := 0; i < numItems; i++ {
// result = append(result, startBlock*512+i)
// }
// return result, nil
// }
// t.Run("ValidRequestWithinFirstBlock", func(t *testing.T) {
// tab := NewTable[int]()
// currentItems := make([]int, 0)
// item, err := readPagedItems(300, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 300, item)
// assertLength(t, 512, currentItems) // Ensure one block is read
// })
// t.Run("ValidRequestAcrossMultipleBlocks", func(t *testing.T) {
// currentItems := make([]int, 0)
// item, err := readPagedItems(600, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 600, item)
// assertLength(t, 1024, currentItems)
// })
// t.Run("SequentialRequestsWithinBlocks", func(t *testing.T) {
// currentItems := make([]int, 0)
// // First request
// item, err := readPagedItems(300, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 300, item)
// // Second request in the same block
// item, err = readPagedItems(400, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 400, item)
// assertLength(t, 512, currentItems)
// })
// t.Run("RequestExactBlockBoundary", func(t *testing.T) {
// currentItems := make([]int, 0)
// item, err := readPagedItems(511, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 511, item)
// assertLength(t, 512, currentItems)
// // Request the next block's first item
// item, err = readPagedItems(512, 512, &currentItems, 2048, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 512, item)
// assertLength(t, 1024, currentItems)
// })
// t.Run("OutOfBoundsRequest", func(t *testing.T) {
// currentItems := make([]int, 0)
// _, err := readPagedItems(2048, 512, &currentItems, 2048, mockReadNMore)
// assertErrorIs(t, err, errOutOfBounds)
// })
// t.Run("RequestBeyondReadBlocks", func(t *testing.T) {
// readFail := errors.New("failed to read block")
// failingReadBlocks := func(startBlock, numBlocks int) ([]int, error) {
// if startBlock > 1 {
// return nil, readFail
// }
// var result []int
// for i := 0; i < numBlocks*512; i++ {
// result = append(result, startBlock*512+i)
// }
// return result, nil
// }
// currentItems := make([]int, 0)
// _, err := readPagedItems(1024, 512, &currentItems, 2048, failingReadBlocks)
// assertErrorIs(t, err, readFail)
// })
// t.Run("partial last page", func(t *testing.T) {
// currentItems := make([]int, 0)
// // Request the next block's first item
// item, err := readPagedItems(512, 512, &currentItems, 612, mockReadNMore)
// requireNoError(t, err)
// assertEqual(t, 512, item)
// assertLength(t, 612, currentItems)
// })
// }
+194 -210
View File
@@ -1,248 +1,232 @@
package data
import (
"encoding/binary"
"errors"
"io"
"math"
"runtime"
"sync"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/toreader"
)
type FragReaderConstructor func() (io.Reader, error)
type FullReader struct {
r io.ReaderAt
d decompress.Decompressor
frag FragReaderConstructor
sizes []uint32
initialOffset int64
finalBlockSize uint64
blockSize uint32
goroutineLimit uint16
fileSize uint64
blockSize uint32
dispatcher chan struct{}
pool *sync.Pool
rdr io.ReaderAt
decomp decompress.Decompressor
sizes []uint32
blockOffsets []uint64
fragDat []byte
}
func NewFullReader(r io.ReaderAt, initialOffset int64, d decompress.Decompressor, sizes []uint32, finalBlockSize uint64, blockSize uint32) *FullReader {
return &FullReader{
r: r,
d: d,
sizes: sizes,
initialOffset: initialOffset,
goroutineLimit: uint16(runtime.NumCPU()),
finalBlockSize: finalBlockSize,
blockSize: blockSize,
func NewFullReader(rdr io.ReaderAt, decomp decompress.Decompressor, blockSize uint32, size uint64, start uint64, sizes []uint32) FullReader {
out := FullReader{
fileSize: size,
blockSize: blockSize,
rdr: rdr,
decomp: decomp,
sizes: sizes,
}
}
func (r *FullReader) AddFrag(frag FragReaderConstructor) {
r.frag = frag
}
func (r *FullReader) SetGoroutineLimit(limit uint16) {
if limit <= 0 {
r.goroutineLimit = 1
out.blockOffsets = make([]uint64, len(sizes))
curOffset := start
for i := range sizes {
out.blockOffsets[i] = curOffset
curOffset += uint64(sizes[i]) &^ (1 << 24)
}
r.goroutineLimit = limit
return out
}
type retValue struct {
err error
data []byte
index uint64
func (f *FullReader) Close() error {
f.fragDat = nil
f.sizes = nil
f.blockOffsets = nil
return nil
}
func (r FullReader) process(index uint64, fileOffset uint64, pool *sync.Pool, retChan chan *retValue) {
ret := pool.Get().(*retValue)
ret.index = index
realSize := r.sizes[index] &^ (1 << 24)
func (f *FullReader) AddFragData(blockStart uint64, blockSize uint32, offset uint32) error {
realSize := blockSize &^ (1 << 24)
dat := make([]byte, realSize)
_, err := f.rdr.ReadAt(dat, int64(blockStart))
if err != nil {
return err
}
if blockSize == realSize {
dat, err = f.decomp.Decompress(dat)
if err != nil {
return err
}
}
f.fragDat = make([]byte, f.fileSize%uint64(f.blockSize))
copy(f.fragDat, dat[offset:])
dat = nil
return nil
}
func (f *FullReader) SetDispatcherPool(dispatcher chan struct{}, pool *sync.Pool) {
f.dispatcher = dispatcher
f.pool = pool
}
// The number of blocks, including the fragment block if present
func (f FullReader) BlockNum() uint32 {
out := len(f.sizes)
if f.fragDat != nil {
out++
}
return uint32(out)
}
// Returns the data block at the given index
func (f FullReader) Block(i uint32) ([]byte, error) {
if i == uint32(len(f.sizes)) && f.fragDat != nil {
return f.fragDat, nil
}
if i >= uint32(len(f.sizes)) {
return nil, errors.New("invalid block index")
}
realSize := f.sizes[i] &^ (1 << 24)
if realSize == 0 {
if index == uint64(len(r.sizes))-1 && r.frag == nil {
ret.data = make([]byte, r.finalBlockSize)
} else {
ret.data = make([]byte, r.blockSize)
if i == uint32(len(f.sizes)-1) && f.fragDat == nil {
return make([]byte, f.fileSize%uint64(f.blockSize)), nil
}
ret.err = nil
retChan <- ret
return
return make([]byte, f.blockSize), nil
}
ret.data = make([]byte, realSize)
ret.err = binary.Read(toreader.NewReader(r.r, int64(r.initialOffset)+int64(fileOffset)), binary.LittleEndian, &ret.data)
if r.sizes[index] == realSize {
ret.data, ret.err = r.d.Decompress(ret.data)
dat := make([]byte, realSize)
_, err := f.rdr.ReadAt(dat, int64(f.blockOffsets[i]))
if err != nil {
return nil, err
}
retChan <- ret
if realSize == f.sizes[i] {
dat, err = f.decomp.Decompress(dat)
}
return dat, err
}
func (r FullReader) WriteTo(w io.Writer) (int64, error) {
// if wa, is := w.(io.WriterAt); is {
// return r.writeToWriteAt(wa)
// }
var curIndex uint64
var curOffset uint64
var toProcess uint16
var wrote int64
cache := make(map[uint64]*retValue)
var errCache []error
retChan := make(chan *retValue, r.goroutineLimit)
pool := &sync.Pool{
New: func() any {
return &retValue{}
},
func (f FullReader) blockFromPool(i uint32) *BlockResults {
out := f.pool.Get().(*BlockResults)
out.idx = i
out.err = nil
if i == uint32(len(f.sizes)) && f.fragDat != nil {
out.block = f.fragDat
return out
}
for i := uint64(0); i < uint64(math.Ceil(float64(len(r.sizes))/float64(r.goroutineLimit))); i++ {
toProcess = min(uint16(len(r.sizes))-(uint16(i)*r.goroutineLimit), r.goroutineLimit)
// Start all the goroutines
for j := uint16(0); j < toProcess; j++ {
go r.process((i*uint64(r.goroutineLimit))+uint64(j), curOffset, pool, retChan)
curOffset += uint64(r.sizes[(i*uint64(r.goroutineLimit))+uint64(j)]) &^ (1 << 24)
if i >= uint32(len(f.sizes)) {
out.err = errors.New("invalid block index")
return out
}
realSize := f.sizes[i] &^ (1 << 24)
if realSize == 0 {
if i == uint32(len(f.sizes)-1) && f.fragDat == nil {
out.block = make([]byte, f.fileSize%uint64(f.blockSize))
return out
}
// Then consume the results on retChan
for j := uint16(0); j < toProcess; j++ {
res := <-retChan
// If there's an error, we don't care about the results.
if res.err != nil {
errCache = append(errCache, res.err)
if len(cache) > 0 {
clear(cache)
}
continue
out.block = make([]byte, f.blockSize)
}
out.block = make([]byte, realSize)
_, out.err = f.rdr.ReadAt(out.block, int64(f.blockOffsets[i]))
if out.err != nil {
return out
}
if realSize == f.sizes[i] {
out.block, out.err = f.decomp.Decompress(out.block)
}
return out
}
type BlockResults struct {
idx uint32
block []byte
err error
}
func (f FullReader) WriteTo(w io.Writer) (wrote int64, err error) {
if f.dispatcher == nil {
f.dispatcher = make(chan struct{}, runtime.NumCPU())
for range runtime.NumCPU() {
f.dispatcher <- struct{}{}
}
}
if f.pool == nil {
f.pool = &sync.Pool{
New: func() any {
return &BlockResults{}
},
}
}
open := true
resChan := make(chan *BlockResults, len(f.dispatcher))
var results map[uint32]*BlockResults
if _, is := w.(io.WriterAt); !is {
results = make(map[uint32]*BlockResults)
}
for i := range f.BlockNum() {
go func(idx uint32) {
<-f.dispatcher
defer func() { f.dispatcher <- struct{}{} }()
if !open {
resChan <- f.pool.Get().(*BlockResults)
return
}
// If there has been an error previously, we don't care about the results.
// We still want to wait for all the goroutines to prevent resources being wasted.
if len(errCache) > 0 {
continue
}
// If we don't need the data yet, we cache it and move on
if res.index != curIndex {
cache[res.index] = res
continue
}
// If we do need the data, we write it
wr, err := w.Write(res.data)
wrote += int64(wr)
resChan <- f.blockFromPool(idx)
}(i)
}
out := int64(0)
errOut := make([]error, 0)
for i := uint32(0); i < f.BlockNum(); {
res := <-resChan
defer f.pool.Put(res)
if res.err != nil {
open = false
errOut = append(errOut, res.err)
}
if len(errOut) > 0 {
i++
continue
}
if wa, is := w.(io.WriterAt); is {
_, err := wa.WriteAt(res.block, int64(res.idx)*int64(f.blockSize))
if err != nil {
errCache = append(errCache, err)
if len(cache) > 0 {
clear(cache)
}
continue
errOut = append(errOut, err)
} else {
out = max(out, int64(res.idx)*int64(f.blockSize)+int64(len(res.block)))
}
pool.Put(res)
curIndex++
// Now we recursively try to clear the cache
for len(cache) > 0 {
res, ok := cache[curIndex]
if !ok {
break
}
wr, err := w.Write(res.data)
wrote += int64(wr)
i++
continue
}
var err error
if res.idx == i {
_, err = w.Write(res.block)
if err != nil {
errOut = append(errOut, err)
} else {
out = max(out, int64(res.idx)*int64(f.blockSize)+int64(len(res.block)))
}
i++
} else {
results[res.idx] = res
}
var has bool
for {
res, has = results[i]
if has {
_, err = w.Write(res.block)
if err != nil {
errCache = append(errCache, err)
if len(cache) > 0 {
clear(cache)
}
break
errOut = append(errOut, err)
} else {
out = max(out, int64(res.idx)*int64(f.blockSize)+int64(len(res.block)))
}
delete(cache, curIndex)
pool.Put(res)
curIndex++
i++
delete(results, i)
f.pool.Put(res)
} else {
break
}
}
if len(errCache) > 0 {
return wrote, errors.Join(errCache...)
}
}
if r.frag != nil {
rdr, err := r.frag()
if err != nil {
return wrote, err
}
wr, err := io.Copy(w, rdr)
wrote += wr
if l, ok := rdr.(*io.LimitedReader); ok {
if cl, ok := l.R.(io.Closer); ok {
cl.Close()
}
}
if err != nil {
return wrote, err
}
if len(errOut) > 0 {
return out, errors.Join(errOut...)
}
return wrote, nil
return out, nil
}
// func (r FullReader) writeToWriteAt(w io.WriterAt) (out int64, outErr error) {
// wait := &sync.WaitGroup{}
// wait.Add(len(r.sizes))
// mgr := routinemanager.NewManager(r.goroutineLimit)
// curOffset := r.initialOffset
// for i := uint64(0); i < uint64(len(r.sizes)); i++ {
// go func(index uint64, fileOffset int64) {
// lckNum := mgr.Lock()
// defer mgr.Unlock(lckNum)
// defer wait.Done()
// realSize := r.sizes[index] &^ (1 << 24)
// if realSize == 0 {
// if index == uint64(len(r.sizes))-1 && r.frag == nil {
// _, err := w.WriteAt([]byte{0}, int64((uint64(r.blockSize)*index)+r.finalBlockSize)-1)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = max(out, int64((uint64(r.blockSize)*index)+r.finalBlockSize))
// }
// return
// }
// data := make([]byte, realSize)
// err := binary.Read(toreader.NewReader(r.r, int64(fileOffset)), binary.LittleEndian, &data)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// if r.sizes[index] == realSize {
// data, err = r.d.Decompress(data)
// }
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// _, err = w.WriteAt(data, int64(uint64(r.blockSize)*index))
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = max(out, int64(uint64(r.blockSize)*(index+1)))
// }(i, curOffset)
// curOffset += int64(r.sizes[i]) &^ (1 << 24)
// }
// if r.frag != nil {
// wait.Add(1)
// go func() {
// lckNum := mgr.Lock()
// defer mgr.Unlock(lckNum)
// defer wait.Done()
// rdr, err := r.frag()
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// dat, err := io.ReadAll(rdr)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// _, err = w.WriteAt(dat, int64(int(r.blockSize)*len(r.sizes)))
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = int64(int(r.blockSize)*len(r.sizes)) + int64(r.finalBlockSize)
// }()
// }
// wait.Wait()
// return
// }
+42 -77
View File
@@ -1,95 +1,60 @@
package data
import (
"encoding/binary"
"io"
"github.com/CalebQ42/squashfs/internal/decompress"
)
import "io"
type Reader struct {
r io.Reader
d decompress.Decompressor
frag io.Reader
sizes []uint32
dat []byte
curOffset int
curIndex uint64
finalBlockSize uint64
blockSize uint32
f *FullReader
curBlock []byte
nextIdx uint32
curOffset uint32
}
func NewReader(r io.Reader, d decompress.Decompressor, sizes []uint32, finalBlockSize uint64, blockSize uint32) *Reader {
return &Reader{
r: r,
d: d,
sizes: sizes,
finalBlockSize: finalBlockSize,
blockSize: blockSize,
func NewReader(f *FullReader) (Reader, error) {
dat, err := f.Block(0)
if err != nil {
return Reader{}, err
}
return Reader{
f: f,
curBlock: dat,
nextIdx: 1,
curOffset: 0,
}, nil
}
func (r *Reader) AddFrag(fragRdr io.Reader) {
r.frag = fragRdr
func (d *Reader) Close() error {
d.curBlock = nil
return nil
}
func (r *Reader) advance() error {
r.curOffset = 0
defer func() { r.curIndex++ }()
var err error
if r.curIndex == uint64(len(r.sizes)) && r.frag != nil {
r.dat, err = io.ReadAll(r.frag)
return err
} else if r.curIndex >= uint64(len(r.sizes)) {
r.dat = []byte{}
func (d *Reader) advanceBlock() error {
if d.nextIdx >= d.f.BlockNum() {
d.curBlock = nil
return io.EOF
}
realSize := r.sizes[r.curIndex] &^ (1 << 24)
if realSize == 0 {
if r.curIndex == uint64(len(r.sizes))-1 && r.frag == nil {
r.dat = make([]byte, r.finalBlockSize)
} else {
r.dat = make([]byte, r.blockSize)
}
return nil
}
r.dat = make([]byte, realSize)
err = binary.Read(r.r, binary.LittleEndian, &r.dat)
var err error
d.curBlock, err = d.f.Block(d.nextIdx)
if err != nil {
return err
}
if r.sizes[r.curIndex] != realSize {
return nil
}
r.dat, err = r.d.Decompress(r.dat)
return err
}
func (r *Reader) Read(b []byte) (int, error) {
curRead := 0
var toRead int
for curRead < len(b) {
if r.curOffset >= len(r.dat) {
if err := r.advance(); err != nil {
return curRead, err
}
}
toRead = min(len(b)-curRead, len(r.dat)-r.curOffset)
toRead = copy(b[curRead:], r.dat[r.curOffset:r.curOffset+toRead])
r.curOffset += toRead
curRead += toRead
}
return curRead, nil
}
func (r *Reader) Close() error {
if r.frag != nil {
if l, ok := r.frag.(*io.LimitedReader); ok {
if cl, ok := l.R.(io.Closer); ok {
cl.Close()
}
}
}
r.dat = nil
d.nextIdx++
d.curOffset = 0
return nil
}
func (d *Reader) Read(buf []byte) (int, error) {
totRed := 0
toRead := 0
var err error
for totRed < len(buf) {
if int(d.curOffset) >= len(d.curBlock) {
err = d.advanceBlock()
if err != nil {
return totRed, err
}
}
toRead = min(len(d.curBlock)-int(d.curOffset), len(buf)-totRed)
copy(buf[totRed:], d.curBlock[d.curOffset:d.curOffset+uint32(toRead)])
}
return totRed, nil
}
+3 -3
View File
@@ -18,7 +18,7 @@ type Directory struct {
Entries []directory.Entry
}
func (r *Reader) directoryFromRef(ref uint64, name string) (Directory, error) {
func (r Reader) directoryFromRef(ref uint64, name string) (Directory, error) {
i, err := r.InodeFromRef(ref)
if err != nil {
return Directory{}, err
@@ -44,7 +44,7 @@ func (r *Reader) directoryFromRef(ref uint64, name string) (Directory, error) {
if err != nil {
return Directory{}, err
}
entries, err := directory.ReadDirectory(dirRdr, size)
entries, err := directory.ReadDirectory(&dirRdr, size)
if err != nil {
return Directory{}, err
}
@@ -54,7 +54,7 @@ func (r *Reader) directoryFromRef(ref uint64, name string) (Directory, error) {
}, nil
}
func (d *Directory) Open(r *Reader, path string) (FileBase, error) {
func (d Directory) Open(r Reader, path string) (FileBase, error) {
path = filepath.Clean(path)
if path == "." || path == "" {
return d.FileBase, nil
+39 -11
View File
@@ -11,12 +11,45 @@ type header struct {
Num uint32
}
type decEntry struct {
func readHeader(r io.Reader) (h header, err error) {
dat := make([]byte, 12)
_, err = r.Read(dat)
if err != nil {
return
}
h.Count = binary.LittleEndian.Uint32(dat)
h.BlockStart = binary.LittleEndian.Uint32(dat[4:])
h.Num = binary.LittleEndian.Uint32(dat[8:])
return
}
type dirEntry struct {
Offset uint16
NumOffset int16
InodeType uint16
NameSize uint16
// Name []byte (not decoded along with decEntry)
Name []byte
}
func readEntry(r io.Reader) (e dirEntry, err error) {
dat := make([]byte, 8)
_, err = r.Read(dat)
if err != nil {
return
}
e.Offset = binary.LittleEndian.Uint16(dat)
_, err = binary.Decode(dat[2:], binary.LittleEndian, &e.NumOffset)
if err != nil {
return
}
e.InodeType = binary.LittleEndian.Uint16(dat[4:])
e.NameSize = binary.LittleEndian.Uint16(dat[6:])
e.Name = make([]byte, e.NameSize+1)
_, err = r.Read(e.Name)
if err != nil {
return
}
return
}
type Entry struct {
@@ -31,20 +64,15 @@ func ReadDirectory(r io.Reader, size uint32) (out []Entry, err error) {
size -= 3
var curRead uint32
var h header
var de decEntry
var de dirEntry
for curRead < size {
err = binary.Read(r, binary.LittleEndian, &h)
h, err = readHeader(r)
if err != nil {
return
}
curRead += 12
for i := uint32(0); i < h.Count+1 && curRead < size; i++ {
err = binary.Read(r, binary.LittleEndian, &de)
if err != nil {
return
}
nameTmp := make([]byte, de.NameSize+1)
err = binary.Read(r, binary.LittleEndian, &nameTmp)
de, err = readEntry(r)
if err != nil {
return
}
@@ -52,7 +80,7 @@ func ReadDirectory(r io.Reader, size uint32) (out []Entry, err error) {
out = append(out, Entry{
BlockStart: h.BlockStart,
Offset: de.Offset,
Name: string(nameTmp),
Name: string(de.Name),
InodeType: de.InodeType,
Num: h.Num + uint32(de.NumOffset),
})
+36 -83
View File
@@ -2,7 +2,6 @@ package squashfslow
import (
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
@@ -16,11 +15,11 @@ type FileBase struct {
Name string
}
func (r *Reader) BaseFromInode(i inode.Inode, name string) FileBase {
func (r Reader) BaseFromInode(i inode.Inode, name string) FileBase {
return FileBase{Inode: i, Name: name}
}
func (r *Reader) BaseFromEntry(e directory.Entry) (FileBase, error) {
func (r Reader) BaseFromEntry(e directory.Entry) (FileBase, error) {
in, err := r.InodeFromEntry(e)
if err != nil {
return FileBase{}, err
@@ -28,7 +27,7 @@ func (r *Reader) BaseFromEntry(e directory.Entry) (FileBase, error) {
return FileBase{Inode: in, Name: e.Name}, nil
}
func (r *Reader) BaseFromRef(ref uint64, name string) (FileBase, error) {
func (r Reader) BaseFromRef(ref uint64, name string) (FileBase, error) {
in, err := r.InodeFromRef(ref)
if err != nil {
return FileBase{}, err
@@ -36,19 +35,19 @@ func (r *Reader) BaseFromRef(ref uint64, name string) (FileBase, error) {
return FileBase{Inode: in, Name: name}, nil
}
func (b *FileBase) Uid(r *Reader) (uint32, error) {
func (b FileBase) Uid(r *Reader) (uint32, error) {
return r.Id(b.Inode.UidInd)
}
func (b *FileBase) Gid(r *Reader) (uint32, error) {
func (b FileBase) Gid(r *Reader) (uint32, error) {
return r.Id(b.Inode.GidInd)
}
func (b *FileBase) IsDir() bool {
func (b FileBase) IsDir() bool {
return b.Inode.Type == inode.Dir || b.Inode.Type == inode.EDir
}
func (b *FileBase) ToDir(r *Reader) (Directory, error) {
func (b FileBase) ToDir(r Reader) (Directory, error) {
var blockStart uint32
var size uint32
var offset uint16
@@ -70,134 +69,88 @@ func (b *FileBase) ToDir(r *Reader) (Directory, error) {
if err != nil {
return Directory{}, err
}
entries, err := directory.ReadDirectory(dirRdr, size)
entries, err := directory.ReadDirectory(&dirRdr, size)
if err != nil {
return Directory{}, err
}
return Directory{
FileBase: *b,
FileBase: b,
Entries: entries,
}, nil
}
func (b *FileBase) IsRegular() bool {
func (b FileBase) IsRegular() bool {
return b.Inode.Type == inode.Fil || b.Inode.Type == inode.EFil
}
func (b *FileBase) GetRegFileReaders(r *Reader) (*data.Reader, *data.FullReader, error) {
// Returns a regular file's readers. They are linked, so the data.Reader calls to the data.FullReader.
// Aka: closing the FullReader breaks the Reader
func (b FileBase) GetRegFileReaders(r Reader) (data.Reader, data.FullReader, error) {
if !b.IsRegular() {
return nil, nil, errors.New("not a regular file")
return data.Reader{}, data.FullReader{}, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
var fileSize uint64
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)
fileSize = uint64(b.Inode.Data.(inode.File).Size)
} 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)
fileSize = b.Inode.Data.(inode.EFile).Size
}
frag := func() (io.Reader, error) {
outFull := data.NewFullReader(r.r, r.d, r.Superblock.BlockSize, fileSize, blockStart, sizes)
if fragIndex != 0xFFFFFFFF {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return nil, err
return data.Reader{}, data.FullReader{}, 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
outFull.AddFragData(ent.Start, ent.Size, fragOffset)
}
outRdr := data.NewReader(toreader.NewReader(r.r, int64(blockStart)), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
f, err := frag()
if err != nil {
return nil, nil, err
}
outRdr.AddFrag(f)
}
outFull := data.NewFullReader(r.r, int64(blockStart), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
outFull.AddFrag(frag)
outRdr, err := data.NewReader(&outFull)
if err != nil {
return data.Reader{}, data.FullReader{}, err
}
return outRdr, outFull, nil
}
func (b *FileBase) GetFullReader(r *Reader) (*data.FullReader, error) {
func (b FileBase) GetFullReader(r *Reader) (data.FullReader, error) {
if !b.IsRegular() {
return nil, errors.New("not a regular file")
return data.FullReader{}, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
var fileSize uint64
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)
fileSize = uint64(b.Inode.Data.(inode.File).Size)
} 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)
fileSize = b.Inode.Data.(inode.EFile).Size
}
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
})
outFull := data.NewFullReader(r.r, r.d, r.Superblock.BlockSize, fileSize, blockStart, sizes)
if fragIndex != 0xFFFFFFFF {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return data.FullReader{}, err
}
outFull.AddFragData(ent.Start, ent.Size, fragOffset)
}
return outFull, nil
}
func (b *FileBase) GetReader(r *Reader) (*data.Reader, 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)
}
outRdr := data.NewReader(toreader.NewReader(r.r, int64(blockStart)), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
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))
outRdr.AddFrag(io.LimitReader(frag, int64(fragSize)))
}
return outRdr, nil
}
+6 -4
View File
@@ -7,7 +7,9 @@ import (
"github.com/CalebQ42/squashfs/low/inode"
)
func (r *Reader) InodeFromRef(ref uint64) (inode.Inode, error) {
type InodeRef = uint64
func (r Reader) InodeFromRef(ref InodeRef) (inode.Inode, error) {
offset, meta := (ref>>16)+r.Superblock.InodeTableStart, ref&0xFFFF
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
defer rdr.Close()
@@ -15,12 +17,12 @@ func (r *Reader) InodeFromRef(ref uint64) (inode.Inode, error) {
if err != nil {
return inode.Inode{}, err
}
return inode.Read(rdr, r.Superblock.BlockSize)
return inode.Read(&rdr, r.Superblock.BlockSize)
}
func (r *Reader) InodeFromEntry(e directory.Entry) (inode.Inode, error) {
func (r Reader) InodeFromEntry(e directory.Entry) (inode.Inode, error) {
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.InodeTableStart)+int64(e.BlockStart)), r.d)
defer rdr.Close()
rdr.Read(make([]byte, e.Offset))
return inode.Read(rdr, r.Superblock.BlockSize)
return inode.Read(&rdr, r.Superblock.BlockSize)
}
+36 -23
View File
@@ -13,7 +13,21 @@ type Directory struct {
ParentNum uint32
}
type eDirectoryInit struct {
func ReadDir(r io.Reader) (d Directory, err error) {
dat := make([]byte, 16)
_, err = r.Read(dat)
if err != nil {
return
}
d.BlockStart = binary.LittleEndian.Uint32(dat)
d.LinkCount = binary.LittleEndian.Uint32(dat[4:])
d.Size = binary.LittleEndian.Uint16(dat[8:])
d.Offset = binary.LittleEndian.Uint16(dat[10:])
d.ParentNum = binary.LittleEndian.Uint32(dat[12:])
return
}
type EDirectory struct {
LinkCount uint32
Size uint32
BlockStart uint32
@@ -21,42 +35,41 @@ type eDirectoryInit struct {
IndCount uint16
Offset uint16
XattrInd uint32
}
type EDirectory struct {
eDirectoryInit
Indexes []DirectoryIndex
}
type directoryIndexInit struct {
Ind uint32
Start uint32
NameSize uint32
Indexes []DirectoryIndex
}
type DirectoryIndex struct {
directoryIndexInit
Name []byte
}
func ReadDir(r io.Reader) (d Directory, err error) {
err = binary.Read(r, binary.LittleEndian, &d)
return
Ind uint32
Start uint32
NameSize uint32
Name []byte
}
func ReadEDir(r io.Reader) (d EDirectory, err error) {
err = binary.Read(r, binary.LittleEndian, &d.eDirectoryInit)
dat := make([]byte, 24)
_, err = r.Read(dat)
if err != nil {
return
}
d.LinkCount = binary.LittleEndian.Uint32(dat)
d.Size = binary.LittleEndian.Uint32(dat[4:])
d.BlockStart = binary.LittleEndian.Uint32(dat[8:])
d.ParentNum = binary.LittleEndian.Uint32(dat[12:])
d.IndCount = binary.LittleEndian.Uint16(dat[16:])
d.Offset = binary.LittleEndian.Uint16(dat[18:])
d.XattrInd = binary.LittleEndian.Uint32(dat[20:])
d.Indexes = make([]DirectoryIndex, d.IndCount)
for i := range d.Indexes {
err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].directoryIndexInit)
for i := range d.IndCount {
dat = make([]byte, 12)
_, err = r.Read(dat)
if err != nil {
return
}
d.Indexes[i].Ind = binary.LittleEndian.Uint32(dat)
d.Indexes[i].Start = binary.LittleEndian.Uint32(dat[4:])
d.Indexes[i].NameSize = binary.LittleEndian.Uint32(dat[8:])
d.Indexes[i].Name = make([]byte, d.Indexes[i].NameSize+1)
err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].Name)
_, err = r.Read(d.Indexes[i].Name)
if err != nil {
return
}
+46 -27
View File
@@ -6,19 +6,41 @@ import (
"math"
)
type fileInit struct {
type File struct {
BlockStart uint32
FragInd uint32
FragOffset uint32
Size uint32
}
type File struct {
fileInit
BlockSizes []uint32
}
type eFileInit struct {
func ReadFile(r io.Reader, blockSize uint32) (f File, err error) {
dat := make([]byte, 16)
_, err = r.Read(dat)
if err != nil {
return
}
f.BlockStart = binary.LittleEndian.Uint32(dat[0:4])
f.FragInd = binary.LittleEndian.Uint32(dat[4:8])
f.FragOffset = binary.LittleEndian.Uint32(dat[8:12])
f.Size = binary.LittleEndian.Uint32(dat[12:16])
toRead := int(math.Floor(float64(f.Size) / float64(blockSize)))
if f.FragInd == 0xFFFFFFFF && f.Size%blockSize > 0 {
toRead++
}
dat = make([]byte, toRead*4)
_, err = r.Read(dat)
if err != nil {
return
}
f.BlockSizes = make([]uint32, toRead)
for i := range toRead {
f.BlockSizes[i] = binary.LittleEndian.Uint32(dat[i*4:])
}
return
}
type EFile struct {
BlockStart uint64
Size uint64
Sparse uint64
@@ -26,37 +48,34 @@ type eFileInit struct {
FragInd uint32
FragOffset uint32
XattrInd uint32
}
type EFile struct {
eFileInit
BlockSizes []uint32
}
func ReadFile(r io.Reader, blockSize uint32) (f File, err error) {
err = binary.Read(r, binary.LittleEndian, &f.fileInit)
if err != nil {
return
}
toRead := int(math.Floor(float64(f.Size) / float64(blockSize)))
if f.FragInd == 0xFFFFFFFF && f.Size%blockSize > 0 {
toRead++
}
f.BlockSizes = make([]uint32, toRead)
err = binary.Read(r, binary.LittleEndian, &f.BlockSizes)
return
}
func ReadEFile(r io.Reader, blockSize uint32) (f EFile, err error) {
err = binary.Read(r, binary.LittleEndian, &f.eFileInit)
dat := make([]byte, 40)
_, err = r.Read(dat)
if err != nil {
return
}
toRead := int(math.Floor(float64(f.Size) / float64(blockSize)))
f.BlockStart = binary.LittleEndian.Uint64(dat[0:8])
f.Size = binary.LittleEndian.Uint64(dat[8:16])
f.Sparse = binary.LittleEndian.Uint64(dat[16:24])
f.LinkCount = binary.LittleEndian.Uint32(dat[24:28])
f.FragInd = binary.LittleEndian.Uint32(dat[28:32])
f.FragOffset = binary.LittleEndian.Uint32(dat[32:36])
f.XattrInd = binary.LittleEndian.Uint32(dat[36:40])
toRead := f.Size / uint64(blockSize)
if f.FragInd == 0xFFFFFFFF && f.Size%uint64(blockSize) > 0 {
toRead++
}
dat = make([]byte, toRead*4)
_, err = r.Read(dat)
if err != nil {
return
}
f.BlockSizes = make([]uint32, toRead)
err = binary.Read(r, binary.LittleEndian, &f.BlockSizes)
for i := range toRead {
f.BlockSizes[i] = binary.LittleEndian.Uint32(dat[i*4:])
}
return
}
+15 -14
View File
@@ -40,10 +40,17 @@ type Inode struct {
}
func Read(r io.Reader, blockSize uint32) (i Inode, err error) {
err = binary.Read(r, binary.LittleEndian, &i.Header)
dat := make([]byte, 16)
_, err = r.Read(dat)
if err != nil {
return
}
i.Type = binary.LittleEndian.Uint16(dat[0:2])
i.Perm = binary.LittleEndian.Uint16(dat[2:4])
i.UidInd = binary.LittleEndian.Uint16(dat[4:6])
i.GidInd = binary.LittleEndian.Uint16(dat[6:8])
i.ModTime = binary.LittleEndian.Uint32(dat[8:12])
i.Num = binary.LittleEndian.Uint32(dat[12:16])
switch i.Type {
case Dir:
i.Data, err = ReadDir(r)
@@ -81,23 +88,17 @@ func Read(r io.Reader, blockSize uint32) (i Inode, err error) {
func (i Inode) Mode() (out fs.FileMode) {
out = fs.FileMode(i.Perm)
switch i.Data.(type) {
case Directory:
switch i.Type {
case Dir, EDir:
out |= fs.ModeDir
case EDirectory:
out |= fs.ModeDir
case Symlink:
case Sym, ESym:
out |= fs.ModeSymlink
case ESymlink:
out |= fs.ModeSymlink
case Device:
case Char, EChar, Block, EBlock:
out |= fs.ModeDevice
case EDevice:
out |= fs.ModeDevice
case IPC:
out |= fs.ModeNamedPipe
case EIPC:
case Fifo, EFifo:
out |= fs.ModeNamedPipe
case Sock, ESock:
out |= fs.ModeSocket
}
return
}
+36 -12
View File
@@ -10,18 +10,31 @@ type Device struct {
Dev uint32
}
func ReadDevice(r io.Reader) (d Device, err error) {
dat := make([]byte, 8)
_, err = r.Read(dat)
if err != nil {
return
}
d.LinkCount = binary.LittleEndian.Uint32(dat)
d.Dev = binary.LittleEndian.Uint32(dat[4:])
return
}
type EDevice struct {
Device
XattrInd uint32
}
func ReadDevice(r io.Reader) (d Device, err error) {
err = binary.Read(r, binary.LittleEndian, &d)
return
}
func ReadEDevice(r io.Reader) (d EDevice, err error) {
err = binary.Read(r, binary.LittleEndian, &d)
dat := make([]byte, 12)
_, err = r.Read(dat)
if err != nil {
return
}
d.LinkCount = binary.LittleEndian.Uint32(dat)
d.Dev = binary.LittleEndian.Uint32(dat[4:])
d.XattrInd = binary.LittleEndian.Uint32(dat[8:])
return
}
@@ -29,17 +42,28 @@ type IPC struct {
LinkCount uint32
}
func ReadIPC(r io.Reader) (i IPC, err error) {
dat := make([]byte, 4)
_, err = r.Read(dat)
if err != nil {
return
}
i.LinkCount = binary.LittleEndian.Uint32(dat)
return
}
type EIPC struct {
IPC
XattrInd uint32
}
func ReadIPC(r io.Reader) (i IPC, err error) {
err = binary.Read(r, binary.LittleEndian, &i)
return
}
func ReadEIPC(r io.Reader) (i EIPC, err error) {
err = binary.Read(r, binary.LittleEndian, &i)
dat := make([]byte, 8)
_, err = r.Read(dat)
if err != nil {
return
}
i.LinkCount = binary.LittleEndian.Uint32(dat)
i.XattrInd = binary.LittleEndian.Uint32(dat[4:])
return
}
+25 -17
View File
@@ -5,42 +5,50 @@ import (
"io"
)
type symlinkInit struct {
type Symlink struct {
LinkCount uint32
TargetSize uint32
}
type Symlink struct {
symlinkInit
Target []byte
}
type ESymlink struct {
symlinkInit
Target []byte
XattrInd uint32
Target []byte
}
func ReadSym(r io.Reader) (s Symlink, err error) {
err = binary.Read(r, binary.LittleEndian, &s.symlinkInit)
dat := make([]byte, 8)
_, err = r.Read(dat)
if err != nil {
return
}
s.LinkCount = binary.LittleEndian.Uint32(dat)
s.TargetSize = binary.LittleEndian.Uint32(dat[4:])
s.Target = make([]byte, s.TargetSize)
err = binary.Read(r, binary.LittleEndian, &s.Target)
_, err = r.Read(s.Target)
return
}
type ESymlink struct {
LinkCount uint32
TargetSize uint32
Target []byte
XattrInd uint32
}
func ReadESym(r io.Reader) (s ESymlink, err error) {
err = binary.Read(r, binary.LittleEndian, &s.symlinkInit)
dat := make([]byte, 8)
_, err = r.Read(dat)
if err != nil {
return
}
s.LinkCount = binary.LittleEndian.Uint32(dat)
s.TargetSize = binary.LittleEndian.Uint32(dat[4:])
s.Target = make([]byte, s.TargetSize)
err = binary.Read(r, binary.LittleEndian, &s.Target)
_, err = r.Read(s.Target)
if err != nil {
return
}
err = binary.Read(r, binary.LittleEndian, &s.XattrInd)
dat = make([]byte, 4)
_, err = r.Read(dat)
if err != nil {
return
}
s.XattrInd = binary.LittleEndian.Uint32(dat)
return
}
+63 -135
View File
@@ -4,10 +4,8 @@ import (
"encoding/binary"
"errors"
"io"
"math"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/inode"
)
@@ -30,179 +28,109 @@ var (
)
type Reader struct {
Root Directory
Superblock superblock
r io.ReaderAt
d decompress.Decompressor
Root Directory
fragTable []fragEntry
idTable []uint32
exportTable []uint64
Superblock superblock
fragTable *Table[fragEntry]
idTable *Table[uint32]
exportTable *Table[InodeRef]
}
func NewReader(r io.ReaderAt) (rdr *Reader, err error) {
rdr = new(Reader)
func NewReader(r io.ReaderAt) (rdr Reader, err error) {
rdr.r = r
err = binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &rdr.Superblock)
if err != nil {
return nil, errors.Join(errors.New("failed to read superblock"), err)
return rdr, errors.Join(errors.New("failed to read superblock"), err)
}
if !rdr.Superblock.ValidMagic() {
return nil, ErrorMagic
return rdr, ErrorMagic
}
if !rdr.Superblock.ValidBlockLog() {
return nil, ErrorLog
return rdr, ErrorLog
}
if !rdr.Superblock.ValidVersion() {
return nil, ErrorVersion
return rdr, ErrorVersion
}
switch rdr.Superblock.CompType {
case ZlibCompression:
rdr.d = decompress.Zlib{}
rdr.d = decompress.NewZlib()
case LZMACompression:
rdr.d = decompress.Lzma{}
rdr.d, err = decompress.NewLzma()
if err != nil {
return rdr, err
}
case LZOCompression:
rdr.d = decompress.Lzo{}
rdr.d, err = decompress.NewLzo()
if err != nil {
return rdr, err
}
case XZCompression:
rdr.d = decompress.Xz{}
rdr.d = decompress.NewXz()
case LZ4Compression:
rdr.d = decompress.Lz4{}
rdr.d = decompress.NewLz4()
case ZSTDCompression:
rdr.d = &decompress.Zstd{}
rdr.d = decompress.NewZstd()
default:
return nil, errors.New("invalid compression type. possible corrupted archive")
return rdr, errors.New("invalid compression type. possible corrupted archive")
}
rdr.Root, err = rdr.directoryFromRef(rdr.Superblock.RootInodeRef, "")
if err != nil {
return nil, errors.Join(errors.New("failed to read root directory"), err)
return rdr, errors.Join(errors.New("failed to read root directory"), err)
}
rdr.fragTable = NewTable(&rdr, rdr.Superblock.FragTableStart, rdr.Superblock.FragCount, readFrag)
rdr.idTable = NewTable(&rdr, rdr.Superblock.IdTableStart, uint32(rdr.Superblock.IdCount), readId)
rdr.exportTable = NewTable(&rdr, rdr.Superblock.ExportTableStart, rdr.Superblock.InodeCount, readRef)
return
}
func readFrag(r io.Reader) (fragEntry, error) {
dat := make([]byte, 16)
_, err := r.Read(dat)
if err != nil {
return fragEntry{}, err
}
return fragEntry{
Start: binary.LittleEndian.Uint64(dat[0:8]),
Size: binary.LittleEndian.Uint32(dat[8:12]),
}, nil
}
func readId(r io.Reader) (uint32, error) {
dat := make([]byte, 4)
_, err := r.Read(dat)
if err != nil {
return 0, err
}
return binary.LittleEndian.Uint32(dat), nil
}
func readRef(r io.Reader) (InodeRef, error) {
dat := make([]byte, 8)
_, err := r.Read(dat)
if err != nil {
return 0, err
}
return binary.LittleEndian.Uint64(dat), nil
}
// Get a uid/gid at the given index. Lazily populates the reader's Id table as necessary.
func (r *Reader) Id(i uint16) (uint32, error) {
if len(r.idTable) > int(i) {
return r.idTable[i], nil
} else if i >= r.Superblock.IdCount {
return 0, errors.New("id out of bounds")
}
// Populate the id table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/2048)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.idTable) / 2048
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var idsToRead uint16
var idsTmp []uint32
var err error
var rdr *metadata.Reader
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.IdTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
idsToRead = min(r.Superblock.IdCount-uint16(len(r.idTable)), 2048)
idsTmp = make([]uint32, idsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(rdr, binary.LittleEndian, &idsTmp)
rdr.Close()
if err != nil {
return 0, err
}
r.idTable = append(r.idTable, idsTmp...)
}
return r.idTable[i], nil
return r.idTable.Get(uint32(i))
}
// Get a fragment entry at the given index. Lazily populates the reader's fragment table as necessary.
func (r *Reader) fragEntry(i uint32) (fragEntry, error) {
if len(r.fragTable) > int(i) {
return r.fragTable[i], nil
} else if i >= r.Superblock.FragCount {
return fragEntry{}, errors.New("fragment out of bounds")
}
// Populate the fragment table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/512)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.fragTable) / 512
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var fragsToRead uint32
var fragsTmp []fragEntry
var err error
var rdr *metadata.Reader
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.FragTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return fragEntry{}, err
}
fragsToRead = min(r.Superblock.FragCount-uint32(len(r.fragTable)), 512)
fragsTmp = make([]fragEntry, fragsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(rdr, binary.LittleEndian, &fragsTmp)
rdr.Close()
if err != nil {
return fragEntry{}, err
}
r.fragTable = append(r.fragTable, fragsTmp...)
}
return r.fragTable[i], nil
return r.fragTable.Get(i)
}
// Get an inode reference at the given index. Lazily populates the reader's export table as necessary.
func (r *Reader) inodeRef(i uint32) (uint64, error) {
if !r.Superblock.Exportable() {
return 0, ErrorNotExportable
}
if len(r.exportTable) > int(i) {
return r.exportTable[i], nil
} else if i >= r.Superblock.InodeCount {
return 0, errors.New("inode out of bounds")
}
// Populate the export table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/1024)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.exportTable) / 1024
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var refsToRead uint32
var refsTmp []uint64
var err error
var rdr *metadata.Reader
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.ExportTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
refsToRead = min(r.Superblock.InodeCount-uint32(len(r.exportTable)), 1024)
refsTmp = make([]uint64, refsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(rdr, binary.LittleEndian, &refsTmp)
rdr.Close()
if err != nil {
return 0, err
}
r.exportTable = append(r.exportTable, refsTmp...)
}
return r.exportTable[i], nil
func (r *Reader) inodeRef(i uint32) (InodeRef, error) {
return r.exportTable.Get(i)
}
func (r *Reader) Inode(i uint32) (inode.Inode, error) {
ref, err := r.inodeRef(i)
func (r Reader) Inode(i uint32) (inode.Inode, error) {
ref, err := r.inodeRef(i - 1) // Inode table is 1 indexed
if err != nil {
return inode.Inode{}, err
}
+10 -6
View File
@@ -77,8 +77,10 @@ func TestReader(t *testing.T) {
path := filepath.Join(tmpDir, "extractTest")
os.RemoveAll(path)
os.MkdirAll(path, 0777)
err = extractToDir(rdr, &rdr.Root.FileBase, path)
t.Fatal(err)
err = extractToDir(rdr, rdr.Root.FileBase, path)
if err != nil {
t.Fatal(err)
}
}
var singleFile = "PortableApps/CPU-X/CPU-X-v4.2.0-x86_64.AppImage"
@@ -101,11 +103,13 @@ func TestSingleFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = extractToDir(rdr, &b, path)
t.Fatal(err)
err = extractToDir(rdr, b, path)
if err != nil {
t.Fatal(err)
}
}
func extractToDir(rdr *Reader, b *FileBase, folder string) error {
func extractToDir(rdr Reader, b FileBase, folder string) error {
path := filepath.Join(folder, b.Name)
if b.IsDir() {
d, err := b.ToDir(rdr)
@@ -122,7 +126,7 @@ func extractToDir(rdr *Reader, b *FileBase, folder string) error {
if err != nil {
return err
}
err = extractToDir(rdr, &nestBast, path)
err = extractToDir(rdr, nestBast, path)
if err != nil {
return err
}
+1 -1
View File
@@ -14,7 +14,7 @@ type superblock struct {
IdCount uint16
VerMaj uint16
VerMin uint16
RootInodeRef uint64
RootInodeRef InodeRef
Size uint64
IdTableStart uint64
XattrTableStart uint64
+85
View File
@@ -0,0 +1,85 @@
package squashfslow
import (
"encoding/binary"
"errors"
"io"
"sync"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
)
var errOutOfBounds = errors.New("out of bounds")
var errUnexpectedOutOfBounds = errors.New("unexpected out of bounds")
var errNilCollection = errors.New("nil collection")
type CreateFunction[T any] = func(io.Reader) (T, error)
type Table[T any] struct {
totalItems uint32
itemsPerBlock uint32
offset uint64
mut sync.RWMutex
currentItems []T
rdr *Reader
createFunc CreateFunction[T]
}
func NewTable[T any](rdr *Reader, start uint64, totalItems uint32, createFunc CreateFunction[T]) *Table[T] {
var zero T
return &Table[T]{
totalItems: totalItems,
itemsPerBlock: 8192 / uint32(binary.Size(zero)),
offset: start,
mut: sync.RWMutex{},
rdr: rdr,
createFunc: createFunc,
}
}
func (t *Table[T]) Get(requestedItemIndex uint32) (T, error) {
t.mut.RLock()
if requestedItemIndex >= t.totalItems {
t.mut.RUnlock()
var zero T
return zero, errOutOfBounds
}
if uint32(len(t.currentItems)) > requestedItemIndex {
t.mut.RUnlock()
return t.currentItems[requestedItemIndex], nil
}
t.mut.RUnlock()
return t.fillAndGet(requestedItemIndex)
}
func (t *Table[T]) fillAndGet(requestedItemIndex uint32) (T, error) {
t.mut.Lock()
defer t.mut.Unlock()
var offset uint64
var toRead uint32
var rdr *toreader.Reader
var metaRdr metadata.Reader
var err error
for uint32(len(t.currentItems)) <= requestedItemIndex {
rdr = toreader.NewReader(t.rdr.r, int64(t.offset))
err = binary.Read(rdr, binary.LittleEndian, &offset)
if err != nil {
var zero T
return zero, err
}
t.offset += 8
toRead = min(t.itemsPerBlock, t.totalItems-uint32(len(t.currentItems)))
oldLen := uint32(len(t.currentItems))
t.currentItems = append(t.currentItems, make([]T, toRead)...)
metaRdr = metadata.NewReader(toreader.NewReader(t.rdr.r, int64(offset)), t.rdr.d)
for i := range toRead {
t.currentItems[oldLen+i], err = t.createFunc(&metaRdr)
if err != nil {
var zero T
return zero, err
}
}
}
return t.currentItems[requestedItemIndex], nil
}
+9 -9
View File
@@ -9,26 +9,26 @@ import (
)
type Reader struct {
*FS
FS
Low squashfslow.Reader
}
func NewReader(r io.ReaderAt) (*Reader, error) {
func NewReader(r io.ReaderAt) (Reader, error) {
rdr, err := squashfslow.NewReader(r)
if err != nil {
return nil, err
return Reader{}, err
}
out := &Reader{
Low: *rdr,
out := Reader{
Low: rdr,
}
out.FS = &FS{
d: rdr.Root,
r: out,
out.FS = FS{
LowDir: rdr.Root,
r: &out,
}
return out, nil
}
func NewReaderAtOffset(r io.ReaderAt, offset int64) (*Reader, error) {
func NewReaderAtOffset(r io.ReaderAt, offset int64) (Reader, error) {
return NewReader(toreader.NewOffsetReader(r, offset))
}
+26 -11
View File
@@ -17,7 +17,7 @@ import (
const (
squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs"
squashfsName = "LinuxPATest.sfs"
squashfsName = "tensorflow.sqfs"
)
func preTest(dir string) (fil *os.File, err error) {
@@ -68,6 +68,24 @@ func TestMisc(t *testing.T) {
// t.Fatal("UM")
}
func BenchmarkExtract(b *testing.B) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
b.Fatal(err)
}
libPath := filepath.Join(tmpDir, "ExtractLib")
os.RemoveAll(libPath)
rdr, err := NewReader(fil)
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractWithOptions(libPath, FastOptions())
if err != nil {
b.Fatal(err)
}
}
func BenchmarkRace(b *testing.B) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
@@ -79,19 +97,17 @@ func BenchmarkRace(b *testing.B) {
os.RemoveAll(libPath)
os.RemoveAll(unsquashPath)
var libTime, unsquashTime time.Duration
op := FastOptions()
op.IgnorePerm = true
start := time.Now()
rdr, err := NewReader(fil)
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractWithOptions(libPath, op)
err = rdr.ExtractWithOptions(libPath, FastOptions())
if err != nil {
b.Fatal(err)
}
libTime = time.Since(start)
cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name())
cmd := exec.Command("unsquashfs", "-q", "-d", unsquashPath, fil.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
start = time.Now()
@@ -100,8 +116,8 @@ func BenchmarkRace(b *testing.B) {
b.Log("Unsquashfs error:", err)
}
unsquashTime = time.Since(start)
b.Log("Library took:", libTime.Round(time.Millisecond))
b.Log("unsquashfs took:", unsquashTime.Round(time.Millisecond))
// b.Log("Library took:", libTime.Round(time.Millisecond))
// b.Log("unsquashfs took:", unsquashTime.Round(time.Millisecond))
b.Log("unsquashfs is", strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64), "times faster")
}
@@ -167,7 +183,7 @@ func TestExtractQuick(t *testing.T) {
}
}
var filePath = "Start.exe"
var filePath = "usr/sbin/add-shell"
func TestSingleFile(t *testing.T) {
tmpDir := "testing"
@@ -175,7 +191,7 @@ func TestSingleFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
os.Remove(filepath.Join(tmpDir, filePath))
os.RemoveAll("testing/stuff")
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
@@ -186,9 +202,8 @@ func TestSingleFile(t *testing.T) {
}
op := DefaultOptions()
op.Verbose = true
err = f.(*File).ExtractWithOptions("testing", op)
err = f.(*File).ExtractWithOptions("testing/stuff", op)
if err != nil {
t.Fatal(err)
}
t.Fatal("HI")
}