From 0253a76dbe11f959ef67348cc4cb0cfe60b9e395 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 26 Nov 2024 07:07:00 -0500 Subject: [PATCH 1/4] 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. --- low/caching_paged_reader.go | 72 +++++++++++++++++ low/caching_paged_reader_test.go | 130 +++++++++++++++++++++++++++++++ low/reader.go | 56 +++++-------- low/reader_test.go | 8 +- 4 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 low/caching_paged_reader.go create mode 100644 low/caching_paged_reader_test.go diff --git a/low/caching_paged_reader.go b/low/caching_paged_reader.go new file mode 100644 index 0000000..9c94aea --- /dev/null +++ b/low/caching_paged_reader.go @@ -0,0 +1,72 @@ +package squashfslow + +import ( + "errors" + "math" +) + +var errOutOfBounds = errors.New("out of bounds") +var errUnexpectedOutOfBounds = errors.New("unexpected out of bounds") +var errNilCollection = errors.New("nil collection") + +// readPagedItems calls readBLockOrPartial the correct number of times to cache +// requestedItemIndex in currentItems, and then returns currentItems[requestedItemIndex]. +// Parameters: +// - requestedItemIndex: The index of the item to be retrieved. +// - blockSize: The number of items per block. +// - currentItems: A slice of already-read items to manage in-memory storage. Must not be nil. +// - readBlockOrPartial: A callback function that reads the next block. It takes the index of the block +// to be read, and the number of items to read. It is normally passed block size, but if the last +// block is incomplete, it will be passed the number of items in the last block. +// Returns: +// - the T at requestedItemIndex +// - a non-nil error and the zero value of T if an error was encountered. +func readPagedItems[T any]( + requestedItemIndex int, + blockSize int, + currentItems *[]T, + totalItems int, + readBlockOrPartial func(idxBlock, numItems int) ([]T, error), +) (T, error) { + var zero T // Zero value for the item type, used for default return in error cases. + if currentItems == nil { + return zero, errNilCollection + } + + if requestedItemIndex < 0 || requestedItemIndex >= totalItems { + return zero, errOutOfBounds + } + + if len(*currentItems) > requestedItemIndex { + return (*currentItems)[requestedItemIndex], nil + } + + // Calculate which block contains the requested item + blockNum := int(math.Ceil(float64(requestedItemIndex+1)/float64(blockSize))) - 1 + + // Calculate blocks to read + blocksRead := len(*currentItems) / blockSize + blocksToRead := blockNum - blocksRead + 1 + + // Read and append new blocks + for i := 0; i < blocksToRead; i++ { + startBlock := blocksRead + i + itemsLeft := totalItems - len(*currentItems) + itemsToRead := blockSize + if itemsToRead > itemsLeft { + itemsToRead = itemsLeft + } + items, err := readBlockOrPartial(startBlock, itemsToRead) + if err != nil { + return zero, err + } + *currentItems = append(*currentItems, items...) + } + + // Ensure the slice contains the requested index after reading + if len(*currentItems) <= requestedItemIndex { + return zero, errUnexpectedOutOfBounds + } + + return (*currentItems)[requestedItemIndex], nil +} diff --git a/low/caching_paged_reader_test.go b/low/caching_paged_reader_test.go new file mode 100644 index 0000000..51e8ba9 --- /dev/null +++ b/low/caching_paged_reader_test.go @@ -0,0 +1,130 @@ +package squashfslow + +import ( + "errors" + "testing" +) + +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) { + currentItems := make([]int, 0) + item, err := readPagedItems(300, 512, ¤tItems, 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, ¤tItems, 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, ¤tItems, 2048, mockReadNMore) + requireNoError(t, err) + assertEqual(t, 300, item) + + // Second request in the same block + item, err = readPagedItems(400, 512, ¤tItems, 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, ¤tItems, 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, ¤tItems, 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, ¤tItems, 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, ¤tItems, 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, ¤tItems, 612, mockReadNMore) + requireNoError(t, err) + assertEqual(t, 512, item) + assertLength(t, 612, currentItems) + }) +} diff --git a/low/reader.go b/low/reader.go index ea71895..e74652d 100644 --- a/low/reader.go +++ b/low/reader.go @@ -123,45 +123,25 @@ func (r *Reader) Id(i uint16) (uint32, error) { // 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)/512)) - 1 - } else { - blockNum = 0 - } - blocksRead := len(r.fragTable) / 512 - blocksToRead := int(blockNum) - blocksRead + 1 + return readPagedItems(int(i), 512, &r.fragTable, int(r.Superblock.FragCount), + func(startBlock, fragsToRead int) ([]fragEntry, error) { + // get the offset of the next block of fragments + var offset uint64 + err := binary.Read(toreader.NewReader(r.r, int64(r.Superblock.FragTableStart)+int64(8*startBlock)), binary.LittleEndian, &offset) + if err != nil { + return nil, err + } - 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 = r.Superblock.FragCount - uint32(len(r.fragTable)) - if fragsToRead > 512 { - fragsToRead = 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 + fragsTmp := make([]fragEntry, fragsToRead) + rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d) + defer rdr.Close() + err = binary.Read(rdr, binary.LittleEndian, &fragsTmp) + if err != nil { + return nil, err + } + + return fragsTmp, nil + }) } // Get an inode reference at the given index. Lazily populates the reader's export table as necessary. diff --git a/low/reader_test.go b/low/reader_test.go index 6a4a4d0..9564559 100644 --- a/low/reader_test.go +++ b/low/reader_test.go @@ -65,7 +65,9 @@ func TestReader(t *testing.T) { os.RemoveAll(path) os.MkdirAll(path, 0777) err = extractToDir(rdr, &rdr.Root.FileBase, path) - t.Fatal(err) + if err != nil { + t.Fatal(err) + } } var singleFile = "PortableApps/CPU-X/CPU-X-v4.2.0-x86_64.AppImage" @@ -89,7 +91,9 @@ func TestSingleFile(t *testing.T) { t.Fatal(err) } err = extractToDir(rdr, &b, path) - t.Fatal(err) + if err != nil { + t.Fatal(err) + } } func extractToDir(rdr *squashfslow.Reader, b *squashfslow.FileBase, folder string) error { From ada61a391c154a95f45dc0c0bc06bdaa317e3575 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Wed, 21 May 2025 01:11:24 -0500 Subject: [PATCH 2/4] 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 --- cmd/go-unsquashfs/main.go | 44 ++++++++++++++++++++++++++++----------- fs.go | 8 +++++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cmd/go-unsquashfs/main.go b/cmd/go-unsquashfs/main.go index 7dc84e5..8a7a344 100644 --- a/cmd/go-unsquashfs/main.go +++ b/cmd/go-unsquashfs/main.go @@ -36,7 +36,7 @@ func groupName(gid int, numeric bool) string { return gs } -func printEntry(root, path string, d fs.DirEntry, numeric bool) { +func printEntry(path string, d fs.DirEntry, numeric bool) { fi, _ := d.Info() sfi := fi.(squashfs.FileInfo) owner := fmt.Sprintf("%s/%s", @@ -50,7 +50,7 @@ func printEntry(root, path string, d fs.DirEntry, numeric bool) { strings.ToLower(fi.Mode().String()), owner, 26-len(owner), fi.Size(), fi.ModTime().Format("2006-01-02 15:04"), - filepath.Join(root, path), link) + filepath.Join(path), link) } func main() { @@ -60,8 +60,12 @@ func main() { numeric := flag.Bool("lln", false, "List with attributes and numeric ids") 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) } @@ -73,26 +77,42 @@ func main() { if err != nil { panic(err) } + extractFil := r.File() + if *file != "" { + extractFil, err = r.OpenFile(*file) + if err != nil { + panic(err) + } + } if *list || *long || *numeric { - root := flag.Arg(1) - fs.WalkDir(r, ".", func(path string, d fs.DirEntry, err error) error { + if extractFil.IsDir() { + var filFs squashfs.FS + filFs, err = extractFil.FS() if err != nil { panic(err) } - if *long || *numeric { - printEntry(root, path, d, *numeric) - } else { - fmt.Println(filepath.Join(root, path)) + err = fs.WalkDir(filFs, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + panic(err) + } + if *long || *numeric { + printEntry(path, d, *numeric) + } else { + fmt.Println(filepath.Clean(path)) + } + return nil + }) + if err != nil { + panic(err) } - return nil - }) + } 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) } diff --git a/fs.go b/fs.go index 5971916..e30bee8 100644 --- a/fs.go +++ b/fs.go @@ -91,6 +91,10 @@ 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) { + 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,7 +115,7 @@ 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 { @@ -146,7 +150,7 @@ func (f FS) Open(name string) (fs.File, error) { 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. From 7930f4402bf4cce105c4a0a1e8fd1deb718402a1 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sun, 25 May 2025 13:35:40 -0500 Subject: [PATCH 3/4] Added -show-hard-links to go-unsquashfs Exposed the underlying squashfslow values for File and FS --- cmd/go-unsquashfs/main.go | 94 ++++++++++++++++++++++++--------------- file.go | 68 ++++++++++++++-------------- fs.go | 28 ++++++------ reader.go | 4 +- 4 files changed, 108 insertions(+), 86 deletions(-) diff --git a/cmd/go-unsquashfs/main.go b/cmd/go-unsquashfs/main.go index 8a7a344..e56e564 100644 --- a/cmd/go-unsquashfs/main.go +++ b/cmd/go-unsquashfs/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "io/fs" "os" "os/user" "path/filepath" @@ -12,6 +11,7 @@ import ( "time" "github.com/CalebQ42/squashfs" + squashfslow "github.com/CalebQ42/squashfs/low" ) func userName(uid int, numeric bool) string { @@ -36,31 +36,73 @@ func groupName(gid int, numeric bool) string { return gs } -func printEntry(path string, d fs.DirEntry, numeric bool) { - fi, _ := d.Info() +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)) - link := "" + userName(sfi.Uid(), *numeric), + groupName(sfi.Gid(), *numeric)) + link, isHardLink := hardLinks[f.Low.Inode.Num] + var size int64 + if isHardLink { + size = 0 + } else { + size = fi.Size() + hardLinks[f.Low.Inode.Num] = path + } 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), fi.Size(), + owner, 26-len(owner), size, fi.ModTime().Format("2006-01-02 15:04"), - filepath.Join(path), link) + 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") - 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") - 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") + 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 (*list || *long || *numeric) && flag.NArg() < 1 { fmt.Println("Please provide a file name") @@ -85,27 +127,7 @@ func main() { } } if *list || *long || *numeric { - if extractFil.IsDir() { - var filFs squashfs.FS - filFs, err = extractFil.FS() - if err != nil { - panic(err) - } - err = fs.WalkDir(filFs, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - panic(err) - } - if *long || *numeric { - printEntry(path, d, *numeric) - } else { - fmt.Println(filepath.Clean(path)) - } - return nil - }) - if err != nil { - panic(err) - } - } + printFile(&r, "", extractFil) return } op := squashfs.DefaultOptions() diff --git a/file.go b/file.go index 8c99b27..f595d12 100644 --- a/file.go +++ b/file.go @@ -24,14 +24,14 @@ type File struct { 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, + Low: b, parent: parent, r: r, } @@ -41,11 +41,11 @@ func (f File) FS() (FS, error) { if !f.IsDir() { 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 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. @@ -75,21 +75,21 @@ func (f File) GetSymlinkFile() fs.File { // Returns whether the file is a directory. func (f File) IsDir() bool { - return f.b.IsDir() + return f.Low.IsDir() } // Returns whether the file is a regular file. func (f File) IsRegular() bool { - return f.b.IsRegular() + 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 + return f.Low.Inode.Type == inode.Sym || f.Low.Inode.Type == inode.ESym } func (f File) Mode() fs.FileMode { - return f.b.Inode.Mode() + return f.Low.Inode.Mode() } // Read reads the data from the file. Only works if file is a normal file. @@ -112,7 +112,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 } @@ -140,24 +140,24 @@ func (f *File) ReadDir(n int) ([]fs.DirEntry, error) { // Returns the file's fs.FileInfo func (f File) Stat() (fs.FileInfo, error) { - uid, err := f.b.Uid(&f.r.Low) + uid, err := f.Low.Uid(&f.r.Low) if err != nil { return nil, err } - gid, err := f.b.Gid(&f.r.Low) + gid, err := f.Low.Gid(&f.r.Low) if err != nil { return nil, err } - return newFileInfo(f.b.Name, uid, gid, &f.b.Inode), nil + 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 { + 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 "" } @@ -179,7 +179,7 @@ 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 { @@ -191,20 +191,20 @@ func (f *File) initializeReaders() error { 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.d.Name == "" { - return f.b.Name + 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. @@ -229,9 +229,9 @@ 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) + d, err := f.Low.ToDir(f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to create squashfs.Directory for", path) @@ -289,7 +289,7 @@ 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) + path = filepath.Join(path, f.Low.Name) outFil, err := os.Create(path) if err != nil { if op.Verbose { @@ -298,7 +298,7 @@ func (f File) ExtractWithOptions(path string, op *ExtractionOptions) error { 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) if err != nil { if op.Verbose { log.Println("Failed to create full reader for", path) @@ -324,11 +324,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) } @@ -351,7 +351,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 { @@ -374,9 +374,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: @@ -412,7 +412,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) @@ -420,7 +420,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) @@ -428,7 +428,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) diff --git a/fs.go b/fs.go index e30bee8..203162d 100644 --- a/fs.go +++ b/fs.go @@ -17,13 +17,13 @@ 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, + LowDir: d, r: r, 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...) } @@ -118,7 +118,7 @@ func (f FS) OpenFile(name string) (*File, error) { 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 { @@ -128,13 +128,13 @@ func (f FS) OpenFile(name string) (*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 @@ -260,20 +260,20 @@ func (f FS) ExtractWithOptions(folder string, op *ExtractionOptions) error { func (f FS) File() *File { if f.parent != nil { return &File{ - b: f.d.FileBase, + Low: f.LowDir.FileBase, parent: *f.parent, r: f.r, } } return &File{ - b: f.d.FileBase, - r: f.r, + Low: f.LowDir.FileBase, + r: f.r, } } 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) } diff --git a/reader.go b/reader.go index 94361f0..2e04a89 100644 --- a/reader.go +++ b/reader.go @@ -22,8 +22,8 @@ func NewReader(r io.ReaderAt) (Reader, error) { Low: rdr, } out.FS = FS{ - d: rdr.Root, - r: &out, + LowDir: rdr.Root, + r: &out, } return out, nil } From 155999a8e3f59e2c914546c4dd88e9dbe769ef2d Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sun, 25 May 2025 13:45:51 -0500 Subject: [PATCH 4/4] Fixed always showing hardlink info --- cmd/go-unsquashfs/main.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/go-unsquashfs/main.go b/cmd/go-unsquashfs/main.go index e56e564..7400958 100644 --- a/cmd/go-unsquashfs/main.go +++ b/cmd/go-unsquashfs/main.go @@ -45,13 +45,19 @@ func printFile(rdr *squashfs.Reader, path string, f *squashfs.File) { owner := fmt.Sprintf("%s/%s", userName(sfi.Uid(), *numeric), groupName(sfi.Gid(), *numeric)) - link, isHardLink := hardLinks[f.Low.Inode.Num] + 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() - hardLinks[f.Low.Inode.Num] = path } if sfi.IsSymlink() { link = " -> " + sfi.SymlinkPath()