pax_global_header00006660000000000000000000000064147643170700014523gustar00rootroot0000000000000052 comment=25e732bd5a668946250c51528f80359b41df0cbb sylabs-squashfs-97df6f5/000077500000000000000000000000001476431707000153075ustar00rootroot00000000000000sylabs-squashfs-97df6f5/.github/000077500000000000000000000000001476431707000166475ustar00rootroot00000000000000sylabs-squashfs-97df6f5/.github/FUNDING.yml000066400000000000000000000001001476431707000204530ustar00rootroot00000000000000# These are supported funding model platforms github: CalebQ42 sylabs-squashfs-97df6f5/.gitignore000066400000000000000000000000271476431707000172760ustar00rootroot00000000000000testing /go-unsquashfs sylabs-squashfs-97df6f5/LICENSE000066400000000000000000000020561476431707000163170ustar00rootroot00000000000000MIT License Copyright (c) 2020 Caleb Gardner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sylabs-squashfs-97df6f5/README.md000066400000000000000000000055731476431707000166000ustar00rootroot00000000000000# squashfs [![PkgGoDev](https://pkg.go.dev/badge/github.com/CalebQ42/squashfs)](https://pkg.go.dev/github.com/CalebQ42/squashfs) [![Go Report Card](https://goreportcard.com/badge/github.com/CalebQ42/squashfs)](https://goreportcard.com/report/github.com/CalebQ42/squashfs) This is a fork of `CalebQ42/squashfs` for the purpose of maintaing a package that removes the lzo dependency, so that it does not contain GPL code. ## Branches * `remove-lzo` - `main` from `CalebQ42/squashfs` with LZO support removed. * `remove-lzo-vX.Y.Z` - `vX.Y.Z` from `CalebQ42/squashfs` with LZO support removed. ## Tags * `vX.Y.Z` - `vX.Y.Z` from `CalebQ42/squashfs` with LZO support removed. ----- A PURE Go library to read squashfs. There is currently no plans to add archive creation support as it will almost always be better to just call `mksquashfs`. I could see some possible use cases, but probably won't spend time on it unless it's requested (open a discussion if you want this feature). The library has two parts with this `github.com/CalebQ42/squashfs` being easy to use as it implements `io/fs` interfaces and doesn't expose unnecessary information. 95% this is the library you want. If you need lower level access to the information, use `github.com/CalebQ42/squashfs/low` where far more information is exposed. Currently has support for reading squashfs files and extracting files and folders. Special thanks to 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). ## FUSE As of `v1.0`, FUSE capabilities has been moved to [a separate library](https://github.com/CalebQ42/squashfuse). ## Limitations * No Xattr parsing. * Socket files are not extracted. * From my research, it seems like a socket file would be useless if it could be created. * Fifo files are ignored on `darwin` ## Issues * Significantly slower then `unsquashfs` when nested 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. Note: These numbers are using `FastOptions()`. `DefaultOptions()` takes about 2x longer. ## Recommendations on Usage Due to the above performance consideration, this library should only be used to access files within the archive without extraction, or to mount it via Fuse. * Neither of these use cases are largely effected by the issue above. sylabs-squashfs-97df6f5/cmd/000077500000000000000000000000001476431707000160525ustar00rootroot00000000000000sylabs-squashfs-97df6f5/cmd/go-unsquashfs/000077500000000000000000000000001476431707000206555ustar00rootroot00000000000000sylabs-squashfs-97df6f5/cmd/go-unsquashfs/main.go000066400000000000000000000013041476431707000221260ustar00rootroot00000000000000package main import ( "flag" "fmt" "os" "time" "github.com/sylabs/squashfs" ) func main() { verbose := flag.Bool("v", false, "Verbose") ignore := flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755") flag.Parse() if len(flag.Args()) < 2 { fmt.Println("Please provide a file name and extraction path") os.Exit(0) } f, err := os.Open(flag.Arg(0)) if err != nil { panic(err) } r, err := squashfs.NewReader(f) if err != nil { panic(err) } op := squashfs.DefaultOptions() op.Verbose = *verbose op.IgnorePerm = *ignore n := time.Now() err = r.ExtractWithOptions(flag.Arg(1), op) if err != nil { panic(err) } fmt.Println("Took:", time.Since(n)) } sylabs-squashfs-97df6f5/extraction_options.go000066400000000000000000000031621476431707000215730ustar00rootroot00000000000000package squashfs import ( "io" "io/fs" "runtime" "github.com/sylabs/squashfs/internal/routinemanager" ) 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(). } // The default extraction options. 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, } } // Less limited default options. Can run up 2x faster than DefaultOptions. // Tends to use all available CPU resources. func FastOptions() *ExtractionOptions { return &ExtractionOptions{ Perm: 0777, SimultaneousFiles: uint16(runtime.NumCPU()), ExtractionRoutines: uint16(runtime.NumCPU()), } } sylabs-squashfs-97df6f5/file.go000066400000000000000000000260231476431707000165600ustar00rootroot00000000000000package squashfs import ( "errors" "io" "io/fs" "log" "os" "os/exec" "path/filepath" "runtime" "strconv" "github.com/sylabs/squashfs/internal/routinemanager" squashfslow "github.com/sylabs/squashfs/low" "github.com/sylabs/squashfs/low/data" "github.com/sylabs/squashfs/low/inode" ) // File represents a file inside a squashfs archive. type File struct { full *data.FullReader rdr *data.Reader parent *FS r *Reader b squashfslow.FileBase dirsRead int } // Creates a new *File from the given *squashfs.Base func (r *Reader) FileFromBase(b squashfslow.FileBase, parent *FS) *File { return &File{ b: b, parent: parent, r: r, } } func (f *File) FS() (*FS, error) { if !f.IsDir() { return nil, errors.New("not a directory") } d, err := f.b.ToDir(&f.r.Low) if err != nil { return nil, err } return &FS{d: d, parent: f.parent, r: f.r}, nil } // Closes the underlying readers. // Further calls to Read and WriteTo will re-create the readers. // Never returns an error. func (f *File) Close() error { if f.rdr != nil { return f.rdr.Close() } f.rdr = nil f.full = nil return nil } // Returns the file the symlink points to. // If the file isn't a symlink, or points to a file outside the archive, returns nil. func (f *File) GetSymlinkFile() fs.File { if !f.IsSymlink() { return nil } if filepath.IsAbs(f.SymlinkPath()) { return nil } fil, err := f.parent.Open(f.SymlinkPath()) if err != nil { return nil } return fil } // Returns whether the file is a directory. func (f *File) IsDir() bool { return f.b.IsDir() } // Returns whether the file is a regular file. func (f *File) IsRegular() bool { return f.b.IsRegular() } // Returns whether the file is a symlink. func (f *File) IsSymlink() bool { return f.b.Inode.Type == inode.Sym || f.b.Inode.Type == inode.ESym } func (f *File) Mode() fs.FileMode { return f.b.Inode.Mode() } // Read reads the data from the file. Only works if file is a normal file. func (f *File) Read(b []byte) (int, error) { if !f.IsRegular() { return 0, errors.New("file is not a regular file") } if f.rdr == nil { err := f.initializeReaders() if err != nil { return 0, err } } return f.rdr.Read(b) } // ReadDir returns n fs.DirEntry's that's contained in the File (if it's a directory). // If n <= 0 all fs.DirEntry's are returned. func (f *File) ReadDir(n int) ([]fs.DirEntry, error) { if !f.IsDir() { return nil, errors.New("file is not a directory") } d, err := f.b.ToDir(&f.r.Low) if err != nil { return nil, err } start, end := 0, len(d.Entries) if n > 0 { start, end = f.dirsRead, f.dirsRead+n if end > len(d.Entries) { end = len(d.Entries) err = io.EOF } } var out []fs.DirEntry var fi fileInfo for _, e := range d.Entries[start:end] { fi, err = f.r.newFileInfo(e) if err != nil { f.dirsRead += len(out) return out, err } out = append(out, fs.FileInfoToDirEntry(fi)) } f.dirsRead += len(out) return out, err } // Returns the file's fs.FileInfo func (f *File) Stat() (fs.FileInfo, error) { return newFileInfo(f.b.Name, &f.b.Inode), nil } // SymlinkPath returns the symlink's target path. Is the File isn't a symlink, returns an empty string. func (f *File) SymlinkPath() string { switch f.b.Inode.Type { case inode.Sym: return string(f.b.Inode.Data.(inode.Symlink).Target) case inode.ESym: return string(f.b.Inode.Data.(inode.ESymlink).Target) } return "" } // Writes all data from the file to the given writer in a multi-threaded manner. // The underlying reader is separate func (f *File) WriteTo(w io.Writer) (int64, error) { if !f.IsRegular() { return 0, errors.New("file is not a regular file") } if f.full == nil { err := f.initializeReaders() if err != nil { return 0, err } } return f.full.WriteTo(w) } func (f *File) initializeReaders() error { var err error f.rdr, f.full, err = f.b.GetRegFileReaders(&f.r.Low) return err } func (f *File) deviceDevices() (maj uint32, min uint32) { var dev uint32 switch f.b.Inode.Type { case inode.Char, inode.Block: dev = f.b.Inode.Data.(inode.Device).Dev case inode.EChar, inode.EBlock: dev = f.b.Inode.Data.(inode.EDevice).Dev } return dev >> 8, dev & 0x000FF } func (f *File) path() string { if f.parent == nil { return f.b.Name } return filepath.Join(f.parent.path(), f.b.Name) } // Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Uses default extraction options. func (f *File) Extract(folder string) error { return f.ExtractWithOptions(folder, DefaultOptions()) } // Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Allows setting various extraction options via ExtractionOptions. func (f *File) ExtractWithOptions(path string, op *ExtractionOptions) error { if op.manager == nil { op.manager = routinemanager.NewManager(op.SimultaneousFiles) if op.LogOutput != nil { log.SetOutput(op.LogOutput) } err := os.MkdirAll(path, 0777) if err != nil { if op.Verbose { log.Println("Failed to create initial directory", path) } return err } } switch f.b.Inode.Type { case inode.Dir, inode.EDir: d, err := f.b.ToDir(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to create squashfs.Directory for", path) } return errors.Join(errors.New("failed to create squashfs.Directory: "+path), err) } errChan := make(chan error, len(d.Entries)) for i := range d.Entries { b, err := f.r.Low.BaseFromEntry(d.Entries[i]) if err != nil { if op.Verbose { log.Println("Failed to get squashfs.Base from entry for", path) } return errors.Join(errors.New("failed to get base from entry: "+path), err) } go func(b squashfslow.FileBase, path string) { i := op.manager.Lock() if b.IsDir() { extDir := filepath.Join(path, b.Name) err = os.Mkdir(extDir, 0777) op.manager.Unlock(i) if err != nil { if op.Verbose { log.Println("Failed to create directory", path) } errChan <- errors.Join(errors.New("failed to create directory: "+path), err) return } err = f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)).ExtractWithOptions(extDir, op) if err != nil { if op.Verbose { log.Println("Failed to extract directory", path) } errChan <- errors.Join(errors.New("failed to extract directory: "+path), err) return } errChan <- nil } else { fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)) err = fil.ExtractWithOptions(path, op) op.manager.Unlock(i) fil.Close() errChan <- err } }(b, path) } var errCache []error for range d.Entries { err := <-errChan if err != nil { errCache = append(errCache, err) } } if len(errCache) > 0 { return errors.Join(errors.New("failed to extract folder: "+path), errors.Join(errCache...)) } case inode.Fil, inode.EFil: path = filepath.Join(path, f.b.Name) outFil, err := os.Create(path) if err != nil { if op.Verbose { log.Println("Failed to create file", path) } return errors.Join(errors.New("failed to create file: "+path), err) } defer outFil.Close() full, err := f.b.GetFullReader(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to create full reader for", path) } return errors.Join(errors.New("failed to create full reader: "+path), err) } full.SetGoroutineLimit(op.ExtractionRoutines) _, err = full.WriteTo(outFil) if err != nil { if op.Verbose { log.Println("Failed to write file", path) } return errors.Join(errors.New("failed to write file: "+path), err) } case inode.Sym, inode.ESym: symPath := f.SymlinkPath() if op.DereferenceSymlink { filTmp := f.GetSymlinkFile() if filTmp == nil { if op.Verbose { log.Println("Failed to get symlink's file:", f.path()) } return errors.New("failed to get symlink's file") } fil := filTmp.(*File) fil.b.Name = f.b.Name err := fil.ExtractWithOptions(path, op) if err != nil { if op.Verbose { log.Println("Failed to extract symlink's file:", filepath.Join(path, f.b.Name)) } return errors.Join(errors.New("failed to extract symlink's file: "+path), err) } } else { if op.UnbreakSymlink { filTmp := f.GetSymlinkFile() if filTmp == nil { if op.Verbose { log.Println("Failed to get symlink's file:", f.path()) } return errors.New("failed to get symlink's file") } extractLoc := filepath.Join(path, filepath.Dir(symPath)) fil := filTmp.(*File) err := fil.ExtractWithOptions(extractLoc, op) if err != nil { if op.Verbose { log.Println("Error while extracting", fil.path(), "to make sure symlink at", f.path(), "is unbroken") } return errors.Join(errors.New("failed to extract symlink's file: "+extractLoc), err) } } path = filepath.Join(path, f.b.Name) err := os.Symlink(f.SymlinkPath(), path) if err != nil { if op.Verbose { log.Println("Failed to create symlink:", path) } return errors.Join(errors.New("failed to create symlink: "+path), err) } } case inode.Char, inode.EChar, inode.Block, inode.EBlock, inode.Fifo, inode.EFifo: if runtime.GOOS == "windows" { if op.Verbose { log.Println(f.path(), "ignored. A device link and can't be created on Windows.") } return nil } _, err := exec.LookPath("mknod") if err != nil { if op.Verbose { log.Println("mknot command not found, cannot create device link for", f.path()) } return errors.Join(errors.New("mknot command not found"), err) } path = filepath.Join(path, f.b.Name) var typ string switch f.b.Inode.Type { case inode.Char, inode.EChar: typ = "c" case inode.Block, inode.EBlock: typ = "b" default: //Fifo IPC if runtime.GOOS == "darwin" { if op.Verbose { log.Println(f.path(), "ignored. A Fifo file and can't be created on Darwin.") } return nil } typ = "p" } cmd := exec.Command("mknod", path, typ) if typ != "p" { maj, min := f.deviceDevices() cmd.Args = append(cmd.Args, strconv.Itoa(int(maj)), strconv.Itoa(int(min))) } if op.Verbose { cmd.Stdout = op.LogOutput cmd.Stderr = op.LogOutput } err = cmd.Run() if err != nil { if op.Verbose { log.Println("Error while running mknod for", path) } return errors.Join(errors.New("error while running mknod for "+path), err) } case inode.Sock, inode.ESock: if op.Verbose { log.Println(f.path(), "ignored since it's a socket file.") } return nil default: return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.b.Inode.Type))) } if op.Verbose { log.Println(f.path(), "extracted to", path) } if op.IgnorePerm { return nil } uid, err := f.b.Uid(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to get uid for", path) log.Println(err) } return nil } gid, err := f.b.Gid(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to get gid for", path) log.Println(err) } return nil } os.Chmod(path, f.Mode()) os.Chown(path, int(uid), int(gid)) return nil } sylabs-squashfs-97df6f5/file_info.go000066400000000000000000000040601476431707000175700ustar00rootroot00000000000000package squashfs import ( "io/fs" "time" "github.com/sylabs/squashfs/low/directory" "github.com/sylabs/squashfs/low/inode" ) type fileInfo struct { name string size int64 perm uint32 modTime uint32 fileType uint16 } func (r Reader) newFileInfo(e directory.Entry) (fileInfo, error) { i, err := r.Low.InodeFromEntry(e) if err != nil { return fileInfo{}, err } return newFileInfo(e.Name, &i), nil } func newFileInfo(name string, i *inode.Inode) fileInfo { var size int64 switch i.Type { case inode.Fil: size = int64(i.Data.(inode.File).Size) case inode.EFil: size = int64(i.Data.(inode.EFile).Size) } return fileInfo{ name: name, size: size, perm: uint32(i.Perm), modTime: i.ModTime, fileType: i.Type, } } func (f fileInfo) Name() string { return f.name } func (f fileInfo) Size() int64 { return f.size } 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 { return time.Unix(int64(f.modTime), 0) } func (f fileInfo) IsDir() bool { return f.fileType == inode.Dir || f.fileType == inode.EDir } 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 } sylabs-squashfs-97df6f5/fs.go000066400000000000000000000134311476431707000162500ustar00rootroot00000000000000package squashfs import ( "io" "io/fs" "path" "path/filepath" "slices" "strings" squashfslow "github.com/sylabs/squashfs/low" "github.com/sylabs/squashfs/low/directory" ) // FS is a fs.FS representation of a squashfs directory. // Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS type FS struct { r *Reader parent *FS d 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, r: r, parent: parent, } } // Glob returns the name of the files at the given pattern. // All paths are relative to the FS. // Uses filepath.Match to compare names. func (f *FS) Glob(pattern string) (out []string, err error) { pattern = filepath.Clean(pattern) if !fs.ValidPath(pattern) { return nil, &fs.PathError{ Op: "glob", Path: pattern, Err: fs.ErrInvalid, } } split := strings.Split(pattern, "/") for i := range f.d.Entries { if match, _ := path.Match(split[0], f.d.Entries[i].Name); match { if len(split) == 1 { out = append(out, f.d.Entries[i].Name) continue } sub, err := f.Sub(split[0]) if err != nil { if pathErr, ok := err.(*fs.PathError); ok { if pathErr.Err == fs.ErrNotExist { continue } pathErr.Op = "glob" pathErr.Path = pattern return nil, pathErr } return nil, &fs.PathError{ Op: "glob", Path: pattern, Err: err, } } subGlob, err := sub.(fs.GlobFS).Glob(strings.Join(split[1:], "/")) if err != nil { if pathErr, ok := err.(*fs.PathError); ok { if pathErr.Err == fs.ErrNotExist { continue } pathErr.Op = "glob" pathErr.Path = pattern return nil, pathErr } return nil, &fs.PathError{ Op: "glob", Path: pattern, Err: err, } } for i := range subGlob { subGlob[i] = f.d.Name + "/" + subGlob[i] } out = append(out, subGlob...) } } return } // Opens the file at name. Returns a *File as an fs.File. func (f *FS) Open(name string) (fs.File, error) { name = filepath.Clean(name) if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrInvalid, } } if name == "." || name == "" { return f.File(), nil } split := strings.Split(name, "/") if split[0] == ".." { if f.parent == nil { // root directory return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } else { return f.parent.Open(strings.Join(split[1:], "/")) } } i, found := slices.BinarySearchFunc(f.d.Entries, split[0], func(e directory.Entry, name string) int { return strings.Compare(e.Name, name) }) if !found { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } b, err := f.r.Low.BaseFromEntry(f.d.Entries[i]) if err != nil { return nil, err } if len(split) == 1 { return &File{ b: b, r: f.r, parent: f, }, nil } if !b.IsDir() { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } d, err := b.ToDir(&f.r.Low) if err != nil { return nil, err } return f.r.FSFromDirectory(d, f).Open(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) { name = filepath.Clean(name) if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "readdir", Path: name, Err: fs.ErrInvalid, } } if name == "." || name == "" { return f.File().ReadDir(-1) } fil, err := f.Open(name) if err != nil { return nil, err } return fil.(*File).ReadDir(-1) } // Returns the contents of the file at name. func (f *FS) ReadFile(name string) (out []byte, err error) { name = filepath.Clean(name) if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "readfile", Path: name, Err: fs.ErrInvalid, } } if name == "." || name == "" { return nil, fs.ErrInvalid } fil, err := f.Open(name) if err != nil { return nil, err } if !fil.(*File).IsRegular() { return nil, fs.ErrInvalid } return io.ReadAll(fil) } // Returns the fs.FileInfo for the file at name. func (f *FS) Stat(name string) (fs.FileInfo, error) { name = filepath.Clean(name) if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "stat", Path: name, Err: fs.ErrInvalid, } } if name == "." || name == "" { return f.File().Stat() } fil, err := f.Open(name) if err != nil { return nil, err } return fil.(*File).Stat() } // Returns the FS at dir func (f *FS) Sub(dir string) (fs.FS, error) { dir = filepath.Clean(dir) if !fs.ValidPath(dir) { return nil, &fs.PathError{ Op: "dir", Path: dir, Err: fs.ErrInvalid, } } if dir == "." || dir == "" { return f, nil } fil, err := f.Open(dir) if err != nil { return nil, err } if !fil.(*File).IsDir() { return nil, &fs.PathError{ Op: "dir", Path: dir, Err: fs.ErrInvalid, } } return fil.(*File).FS() } // Extract the FS to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Uses default extraction options. func (f *FS) Extract(folder string) error { return f.File().Extract(folder) } // Extract the FS to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Allows setting various extraction options via ExtractionOptions. func (f *FS) ExtractWithOptions(folder string, op *ExtractionOptions) error { return f.File().ExtractWithOptions(folder, op) } // Returns the FS as a *File func (f *FS) File() *File { return &File{ b: f.d.FileBase, parent: f.parent, r: f.r, } } func (f *FS) path() string { if f.parent == nil { return f.d.Name } return filepath.Join(f.parent.path(), f.d.Name) } sylabs-squashfs-97df6f5/go.mod000066400000000000000000000003121476431707000164110ustar00rootroot00000000000000module github.com/sylabs/squashfs go 1.24.0 require ( github.com/klauspost/compress v1.18.0 github.com/pierrec/lz4/v4 v4.1.22 github.com/therootcompany/xz v1.0.1 github.com/ulikunitz/xz v0.5.12 ) sylabs-squashfs-97df6f5/go.sum000066400000000000000000000012641476431707000164450ustar00rootroot00000000000000github.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.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= sylabs-squashfs-97df6f5/internal/000077500000000000000000000000001476431707000171235ustar00rootroot00000000000000sylabs-squashfs-97df6f5/internal/decompress/000077500000000000000000000000001476431707000212675ustar00rootroot00000000000000sylabs-squashfs-97df6f5/internal/decompress/decompress.go000066400000000000000000000001301476431707000237540ustar00rootroot00000000000000package decompress type Decompressor interface { Decompress([]byte) ([]byte, error) } sylabs-squashfs-97df6f5/internal/decompress/lz4.go000066400000000000000000000003361476431707000223310ustar00rootroot00000000000000package decompress import ( "bytes" "io" "github.com/pierrec/lz4/v4" ) type Lz4 struct{} func (l Lz4) Decompress(data []byte) ([]byte, error) { rdr := lz4.NewReader(bytes.NewReader(data)) return io.ReadAll(rdr) } sylabs-squashfs-97df6f5/internal/decompress/lzma.go000066400000000000000000000004171476431707000225630ustar00rootroot00000000000000package decompress import ( "bytes" "io" "github.com/ulikunitz/xz/lzma" ) type Lzma struct{} func (l Lzma) Decompress(data []byte) ([]byte, error) { rdr, err := lzma.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } return io.ReadAll(rdr) } sylabs-squashfs-97df6f5/internal/decompress/xz.go000066400000000000000000000004141476431707000222560ustar00rootroot00000000000000package decompress import ( "bytes" "io" "github.com/therootcompany/xz" ) type Xz struct{} func (x Xz) Decompress(data []byte) ([]byte, error) { rdr, err := xz.NewReader(bytes.NewReader(data), 0) if err != nil { return nil, err } return io.ReadAll(rdr) } sylabs-squashfs-97df6f5/internal/decompress/zlib.go000066400000000000000000000004221476431707000225540ustar00rootroot00000000000000package decompress import ( "bytes" "compress/zlib" "io" ) type Zlib struct{} func (z Zlib) Decompress(data []byte) ([]byte, error) { rdr, err := zlib.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer rdr.Close() return io.ReadAll(rdr) } sylabs-squashfs-97df6f5/internal/decompress/zstd.go000066400000000000000000000004501476431707000226010ustar00rootroot00000000000000package decompress import ( "bytes" "io" "github.com/klauspost/compress/zstd" ) type Zstd struct{} func (z Zstd) Decompress(data []byte) ([]byte, error) { rdr, err := zstd.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer rdr.Close() return io.ReadAll(rdr) } sylabs-squashfs-97df6f5/internal/metadata/000077500000000000000000000000001476431707000207035ustar00rootroot00000000000000sylabs-squashfs-97df6f5/internal/metadata/reader.go000066400000000000000000000022521476431707000224750ustar00rootroot00000000000000package metadata import ( "encoding/binary" "io" "github.com/sylabs/squashfs/internal/decompress" ) type Reader struct { r io.Reader d decompress.Decompressor dat []byte curOffset uint16 } func NewReader(r io.Reader, d decompress.Decompressor) *Reader { return &Reader{ r: r, d: d, } } func (r *Reader) advance() error { r.curOffset = 0 var size uint16 err := binary.Read(r.r, binary.LittleEndian, &size) if err != nil { return err } realSize := size &^ 0x8000 r.dat = make([]byte, realSize) err = binary.Read(r.r, binary.LittleEndian, &r.dat) if err != nil { return err } if size != 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 >= uint16(len(r.dat)) { if err := r.advance(); err != nil { return curRead, err } } toRead = min(len(b)-curRead, len(r.dat)-int(r.curOffset)) copy(b[curRead:], r.dat[r.curOffset:int(r.curOffset)+toRead]) r.curOffset += uint16(toRead) curRead += toRead } return curRead, nil } func (r *Reader) Close() error { r.dat = nil return nil } sylabs-squashfs-97df6f5/internal/routinemanager/000077500000000000000000000000001476431707000221435ustar00rootroot00000000000000sylabs-squashfs-97df6f5/internal/routinemanager/manager.go000066400000000000000000000006501476431707000241050ustar00rootroot00000000000000package routinemanager type Manager struct { channel chan uint16 maxRoutines uint16 } func NewManager(maxRoutines uint16) *Manager { m := &Manager{ maxRoutines: maxRoutines, channel: make(chan uint16, maxRoutines), } for i := uint16(0); i < maxRoutines; i++ { m.channel <- i } return m } func (m *Manager) Lock() uint16 { return <-m.channel } func (m *Manager) Unlock(i uint16) { m.channel <- i } sylabs-squashfs-97df6f5/internal/toreader/000077500000000000000000000000001476431707000207305ustar00rootroot00000000000000sylabs-squashfs-97df6f5/internal/toreader/offsetreader.go000066400000000000000000000004631476431707000237330ustar00rootroot00000000000000package toreader import "io" type OffsetReader struct { r io.ReaderAt off int64 } func NewOffsetReader(r io.ReaderAt, off int64) *OffsetReader { return &OffsetReader{ r: r, off: off, } } func (r OffsetReader) ReadAt(p []byte, off int64) (n int, e error) { return r.r.ReadAt(p, off+r.off) } sylabs-squashfs-97df6f5/internal/toreader/toreader.go000066400000000000000000000004741476431707000230710ustar00rootroot00000000000000package toreader import "io" type Reader struct { r io.ReaderAt offset int64 } func NewReader(r io.ReaderAt, start int64) *Reader { return &Reader{ r: r, offset: start, } } func (r *Reader) Read(b []byte) (int, error) { n, err := r.r.ReadAt(b, r.offset) r.offset += int64(n) return n, err } sylabs-squashfs-97df6f5/low/000077500000000000000000000000001476431707000161105ustar00rootroot00000000000000sylabs-squashfs-97df6f5/low/README.md000066400000000000000000000003601476431707000173660ustar00rootroot00000000000000# 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. sylabs-squashfs-97df6f5/low/data/000077500000000000000000000000001476431707000170215ustar00rootroot00000000000000sylabs-squashfs-97df6f5/low/data/fullreader.go000066400000000000000000000144731476431707000215060ustar00rootroot00000000000000package data import ( "encoding/binary" "errors" "io" "math" "runtime" "sync" "github.com/sylabs/squashfs/internal/decompress" "github.com/sylabs/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 } 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 (r *FullReader) AddFrag(frag FragReaderConstructor) { r.frag = frag } func (r *FullReader) SetGoroutineLimit(limit uint16) { if limit <= 0 { r.goroutineLimit = 1 } r.goroutineLimit = limit } type retValue struct { err error data []byte index uint64 } 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) 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) } ret.err = nil retChan <- ret return } 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) } retChan <- ret } 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{} }, } 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) } // 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 } // 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) if err != nil { errCache = append(errCache, err) if len(cache) > 0 { clear(cache) } continue } 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) if err != nil { errCache = append(errCache, err) if len(cache) > 0 { clear(cache) } break } delete(cache, curIndex) pool.Put(res) curIndex++ } } 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 } } return wrote, 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 // } sylabs-squashfs-97df6f5/low/data/reader.go000066400000000000000000000037701476431707000206210ustar00rootroot00000000000000package data import ( "encoding/binary" "io" "github.com/sylabs/squashfs/internal/decompress" ) type Reader struct { r io.Reader d decompress.Decompressor frag io.Reader sizes []uint32 dat []byte curOffset int curIndex uint64 finalBlockSize uint64 blockSize 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 (r *Reader) AddFrag(fragRdr io.Reader) { r.frag = fragRdr } 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{} 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) 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 return nil } sylabs-squashfs-97df6f5/low/directory.go000066400000000000000000000040111476431707000204370ustar00rootroot00000000000000package squashfslow import ( "errors" "io/fs" "path/filepath" "slices" "strings" "github.com/sylabs/squashfs/internal/metadata" "github.com/sylabs/squashfs/internal/toreader" "github.com/sylabs/squashfs/low/directory" "github.com/sylabs/squashfs/low/inode" ) type Directory struct { FileBase Entries []directory.Entry } func (r *Reader) directoryFromRef(ref uint64, name string) (Directory, error) { i, err := r.InodeFromRef(ref) if err != nil { return Directory{}, err } var blockStart uint32 var size uint32 var offset uint16 switch i.Type { case inode.Dir: blockStart = i.Data.(inode.Directory).BlockStart size = uint32(i.Data.(inode.Directory).Size) offset = i.Data.(inode.Directory).Offset case inode.EDir: blockStart = i.Data.(inode.EDirectory).BlockStart size = i.Data.(inode.EDirectory).Size offset = i.Data.(inode.EDirectory).Offset default: return Directory{}, errors.New("not a directory") } dirRdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.DirTableStart)+int64(blockStart)), r.d) defer dirRdr.Close() _, err = dirRdr.Read(make([]byte, offset)) if err != nil { return Directory{}, err } entries, err := directory.ReadDirectory(dirRdr, size) if err != nil { return Directory{}, err } return Directory{ FileBase: r.BaseFromInode(i, name), Entries: entries, }, nil } func (d *Directory) Open(r *Reader, path string) (FileBase, error) { path = filepath.Clean(path) if path == "." || path == "" { return d.FileBase, nil } split := strings.Split(path, "/") i, found := slices.BinarySearchFunc(d.Entries, split[0], func(e directory.Entry, name string) int { return strings.Compare(e.Name, name) }) if !found { return FileBase{}, fs.ErrNotExist } b, err := r.BaseFromEntry(d.Entries[i]) if err != nil { return FileBase{}, err } if len(split) == 1 { return b, nil } else if !b.IsDir() { return FileBase{}, fs.ErrNotExist } dir, err := b.ToDir(r) if err != nil { return FileBase{}, err } return dir.Open(r, strings.Join(split[1:], "/")) } sylabs-squashfs-97df6f5/low/directory/000077500000000000000000000000001476431707000201145ustar00rootroot00000000000000sylabs-squashfs-97df6f5/low/directory/directory.go000066400000000000000000000022471476431707000224540ustar00rootroot00000000000000package directory import ( "encoding/binary" "io" ) type header struct { Count uint32 BlockStart uint32 Num uint32 } type decEntry struct { Offset uint16 NumOffset int16 InodeType uint16 NameSize uint16 // Name []byte (not decoded along with decEntry) } type Entry struct { Name string BlockStart uint32 Offset uint16 InodeType uint16 Num uint32 } func ReadDirectory(r io.Reader, size uint32) (out []Entry, err error) { size -= 3 var curRead uint32 var h header var de decEntry for curRead < size { err = binary.Read(r, binary.LittleEndian, &h) 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) if err != nil { return } curRead += 8 + uint32(de.NameSize) + 1 out = append(out, Entry{ BlockStart: h.BlockStart, Offset: de.Offset, Name: string(nameTmp), InodeType: de.InodeType, Num: h.Num + uint32(de.NumOffset), }) } } return } sylabs-squashfs-97df6f5/low/file_base.go000066400000000000000000000146431476431707000203600ustar00rootroot00000000000000package squashfslow import ( "errors" "io" "github.com/sylabs/squashfs/internal/metadata" "github.com/sylabs/squashfs/internal/toreader" "github.com/sylabs/squashfs/low/data" "github.com/sylabs/squashfs/low/directory" "github.com/sylabs/squashfs/low/inode" ) type FileBase struct { Inode inode.Inode Name string } 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) { in, err := r.InodeFromEntry(e) if err != nil { return FileBase{}, err } return FileBase{Inode: in, Name: e.Name}, nil } func (r *Reader) BaseFromRef(ref uint64, name string) (FileBase, error) { in, err := r.InodeFromRef(ref) if err != nil { return FileBase{}, err } return FileBase{Inode: in, Name: name}, nil } func (b *FileBase) Uid(r *Reader) (uint32, error) { return r.Id(b.Inode.UidInd) } func (b *FileBase) Gid(r *Reader) (uint32, error) { return r.Id(b.Inode.GidInd) } func (b *FileBase) IsDir() bool { return b.Inode.Type == inode.Dir || b.Inode.Type == inode.EDir } func (b *FileBase) ToDir(r *Reader) (Directory, error) { var blockStart uint32 var size uint32 var offset uint16 switch b.Inode.Type { case inode.Dir: blockStart = b.Inode.Data.(inode.Directory).BlockStart size = uint32(b.Inode.Data.(inode.Directory).Size) offset = b.Inode.Data.(inode.Directory).Offset case inode.EDir: blockStart = b.Inode.Data.(inode.EDirectory).BlockStart size = b.Inode.Data.(inode.EDirectory).Size offset = b.Inode.Data.(inode.EDirectory).Offset default: return Directory{}, errors.New("not a directory") } dirRdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.DirTableStart)+int64(blockStart)), r.d) defer dirRdr.Close() _, err := dirRdr.Read(make([]byte, offset)) if err != nil { return Directory{}, err } entries, err := directory.ReadDirectory(dirRdr, size) if err != nil { return Directory{}, err } return Directory{ FileBase: *b, Entries: entries, }, nil } 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) { if !b.IsRegular() { return nil, 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) } frag := 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 } 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) } return outRdr, outFull, nil } func (b *FileBase) GetFullReader(r *Reader) (*data.FullReader, error) { if !b.IsRegular() { return nil, errors.New("not a regular file") } var blockStart uint64 var fragIndex uint32 var fragOffset uint32 var fragSize uint64 var sizes []uint32 if b.Inode.Type == inode.Fil { blockStart = uint64(b.Inode.Data.(inode.File).BlockStart) fragIndex = b.Inode.Data.(inode.File).FragInd fragOffset = b.Inode.Data.(inode.File).FragOffset sizes = b.Inode.Data.(inode.File).BlockSizes fragSize = uint64(b.Inode.Data.(inode.File).Size % r.Superblock.BlockSize) } else { blockStart = b.Inode.Data.(inode.EFile).BlockStart fragIndex = b.Inode.Data.(inode.EFile).FragInd fragOffset = b.Inode.Data.(inode.EFile).FragOffset sizes = b.Inode.Data.(inode.EFile).BlockSizes fragSize = b.Inode.Data.(inode.EFile).Size % uint64(r.Superblock.BlockSize) } outFull := data.NewFullReader(r.r, int64(blockStart), r.d, sizes, fragSize, r.Superblock.BlockSize) if fragIndex != 0xffffffff { outFull.AddFrag(func() (io.Reader, error) { ent, err := r.fragEntry(fragIndex) if err != nil { return nil, err } frag := data.NewReader(toreader.NewReader(r.r, int64(ent.Start)), r.d, []uint32{ent.Size}, uint64(r.Superblock.BlockSize), r.Superblock.BlockSize) frag.Read(make([]byte, fragOffset)) return io.LimitReader(frag, int64(fragSize)), nil }) } return outFull, nil } 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 } sylabs-squashfs-97df6f5/low/fragment.go000066400000000000000000000001311476431707000202350ustar00rootroot00000000000000package squashfslow type fragEntry struct { Start uint64 Size uint32 _ uint32 } sylabs-squashfs-97df6f5/low/inode.go000066400000000000000000000015431476431707000175400ustar00rootroot00000000000000package squashfslow import ( "github.com/sylabs/squashfs/internal/metadata" "github.com/sylabs/squashfs/internal/toreader" "github.com/sylabs/squashfs/low/directory" "github.com/sylabs/squashfs/low/inode" ) func (r *Reader) InodeFromRef(ref uint64) (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() _, err := rdr.Read(make([]byte, meta)) if err != nil { return inode.Inode{}, err } return inode.Read(rdr, r.Superblock.BlockSize) } 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) } sylabs-squashfs-97df6f5/low/inode/000077500000000000000000000000001476431707000172065ustar00rootroot00000000000000sylabs-squashfs-97df6f5/low/inode/dir.go000066400000000000000000000022251476431707000203140ustar00rootroot00000000000000package inode import ( "encoding/binary" "io" ) type Directory struct { BlockStart uint32 LinkCount uint32 Size uint16 Offset uint16 ParentNum uint32 } type eDirectoryInit struct { LinkCount uint32 Size uint32 BlockStart uint32 ParentNum uint32 IndCount uint16 Offset uint16 XattrInd uint32 } type EDirectory struct { eDirectoryInit Indexes []DirectoryIndex } type directoryIndexInit struct { Ind uint32 Start uint32 NameSize uint32 } type DirectoryIndex struct { directoryIndexInit Name []byte } func ReadDir(r io.Reader) (d Directory, err error) { err = binary.Read(r, binary.LittleEndian, &d) return } func ReadEDir(r io.Reader) (d EDirectory, err error) { err = binary.Read(r, binary.LittleEndian, &d.eDirectoryInit) if err != nil { return } d.Indexes = make([]DirectoryIndex, d.IndCount) for i := range d.Indexes { err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].directoryIndexInit) if err != nil { return } d.Indexes[i].Name = make([]byte, d.Indexes[i].NameSize+1) err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].Name) if err != nil { return } } return } sylabs-squashfs-97df6f5/low/inode/file.go000066400000000000000000000023121476431707000204520ustar00rootroot00000000000000package inode import ( "encoding/binary" "io" "math" ) type fileInit struct { BlockStart uint32 FragInd uint32 FragOffset uint32 Size uint32 } type File struct { fileInit BlockSizes []uint32 } type eFileInit struct { BlockStart uint64 Size uint64 Sparse uint64 LinkCount uint32 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) if err != nil { return } toRead := int(math.Floor(float64(f.Size) / float64(blockSize))) if f.FragInd == 0xFFFFFFFF && f.Size%uint64(blockSize) > 0 { toRead++ } f.BlockSizes = make([]uint32, toRead) err = binary.Read(r, binary.LittleEndian, &f.BlockSizes) return } sylabs-squashfs-97df6f5/low/inode/inode.go000066400000000000000000000044131476431707000206350ustar00rootroot00000000000000package inode import ( "encoding/binary" "errors" "io" "io/fs" "strconv" ) const ( Dir = uint16(iota + 1) Fil Sym Block Char Fifo Sock EDir EFil ESym EBlock EChar EFifo ESock ) type Header struct { Type uint16 Perm uint16 UidInd uint16 GidInd uint16 ModTime uint32 Num uint32 } type Inode struct { Header Data any } func Read(r io.Reader, blockSize uint32) (i Inode, err error) { err = binary.Read(r, binary.LittleEndian, &i.Header) if err != nil { return } switch i.Type { case Dir: i.Data, err = ReadDir(r) case Fil: i.Data, err = ReadFile(r, blockSize) case Sym: i.Data, err = ReadSym(r) case Block: fallthrough case Char: i.Data, err = ReadDevice(r) case Fifo: fallthrough case Sock: i.Data, err = ReadIPC(r) case EDir: i.Data, err = ReadEDir(r) case EFil: i.Data, err = ReadEFile(r, blockSize) case ESym: i.Data, err = ReadESym(r) case EBlock: fallthrough case EChar: i.Data, err = ReadEDevice(r) case EFifo: fallthrough case ESock: i.Data, err = ReadEIPC(r) default: return i, errors.New("invalid inode type " + strconv.Itoa(int(i.Type))) } return } func (i Inode) Mode() (out fs.FileMode) { out = fs.FileMode(i.Perm) switch i.Type { case Dir, EDir: out |= fs.ModeDir case Sym, ESym: out |= fs.ModeSymlink case Char, EChar, Block, EBlock: out |= fs.ModeDevice case Fifo, EFifo: out |= fs.ModeNamedPipe case Sock, ESock: out |= fs.ModeSocket } return } func (i Inode) LinkCount() uint32 { switch i.Data.(type) { case EFile: return i.Data.(EFile).LinkCount case Directory: return i.Data.(Directory).LinkCount case EDirectory: return i.Data.(EDirectory).LinkCount case Device: return i.Data.(Device).LinkCount case EDevice: return i.Data.(EDevice).LinkCount case IPC: return i.Data.(IPC).LinkCount case EIPC: return i.Data.(EIPC).LinkCount case Symlink: return i.Data.(Symlink).LinkCount case ESymlink: return i.Data.(ESymlink).LinkCount default: return 0 } } func (i Inode) Size() uint64 { switch i.Data.(type) { case File: return uint64(i.Data.(File).Size) case EFile: return i.Data.(EFile).Size // case Directory: // return uint64(i.Data.(Directory).Size) // case EDirectory: // return uint64(i.Data.(EDirectory).Size) default: return 0 } } sylabs-squashfs-97df6f5/low/inode/misc.go000066400000000000000000000012501476431707000204660ustar00rootroot00000000000000package inode import ( "encoding/binary" "io" ) type Device struct { LinkCount uint32 Dev uint32 } 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) return } type IPC struct { LinkCount uint32 } 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) return } sylabs-squashfs-97df6f5/low/inode/sym.go000066400000000000000000000014611476431707000203470ustar00rootroot00000000000000package inode import ( "encoding/binary" "io" ) type symlinkInit struct { LinkCount uint32 TargetSize uint32 } type Symlink struct { symlinkInit Target []byte } type ESymlink struct { symlinkInit Target []byte XattrInd uint32 } func ReadSym(r io.Reader) (s Symlink, err error) { err = binary.Read(r, binary.LittleEndian, &s.symlinkInit) if err != nil { return } s.Target = make([]byte, s.TargetSize) err = binary.Read(r, binary.LittleEndian, &s.Target) return } func ReadESym(r io.Reader) (s ESymlink, err error) { err = binary.Read(r, binary.LittleEndian, &s.symlinkInit) if err != nil { return } s.Target = make([]byte, s.TargetSize) err = binary.Read(r, binary.LittleEndian, &s.Target) if err != nil { return } err = binary.Read(r, binary.LittleEndian, &s.XattrInd) return } sylabs-squashfs-97df6f5/low/reader.go000066400000000000000000000141111476431707000176770ustar00rootroot00000000000000package squashfslow import ( "encoding/binary" "errors" "io" "math" "github.com/sylabs/squashfs/internal/decompress" "github.com/sylabs/squashfs/internal/metadata" "github.com/sylabs/squashfs/internal/toreader" "github.com/sylabs/squashfs/low/inode" ) // The types of compression supported by squashfs const ( ZlibCompression = uint16(iota + 1) LZMACompression LZOCompression XZCompression LZ4Compression ZSTDCompression ) var ( ErrorMagic = errors.New("magic incorrect. probably not reading squashfs archive or archive is corrupted") ErrorLog = errors.New("block log is incorrect. possible corrupted archive") ErrorVersion = errors.New("squashfs version of archive is not 4.0. may be corrupted") ErrorNotExportable = errors.New("archive does not have an export table") ) type Reader struct { r io.ReaderAt d decompress.Decompressor Root Directory fragTable []fragEntry idTable []uint32 exportTable []uint64 Superblock superblock } func NewReader(r io.ReaderAt) (rdr *Reader, err error) { rdr = new(Reader) 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) } if !rdr.Superblock.ValidMagic() { return nil, ErrorMagic } if !rdr.Superblock.ValidBlockLog() { return nil, ErrorLog } if !rdr.Superblock.ValidVersion() { return nil, ErrorVersion } switch rdr.Superblock.CompType { case ZlibCompression: rdr.d = decompress.Zlib{} case LZMACompression: rdr.d = decompress.Lzma{} case XZCompression: rdr.d = decompress.Xz{} case LZ4Compression: rdr.d = decompress.Lz4{} case ZSTDCompression: rdr.d = &decompress.Zstd{} default: return nil, 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 } // 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 } // 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 } // 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) Inode(i uint32) (inode.Inode, error) { ref, err := r.inodeRef(i) if err != nil { return inode.Inode{}, err } return r.InodeFromRef(ref) } sylabs-squashfs-97df6f5/low/reader_test.go000066400000000000000000000053321476431707000207430ustar00rootroot00000000000000package squashfslow import ( "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "testing" ) const ( squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs" squashfsName = "airootfs.sfs" ) func preTest(dir string) (fil *os.File, err error) { fil, err = os.Open(filepath.Join(dir, squashfsName)) if err != nil { _, err = os.Open(dir) if os.IsNotExist(err) { err = os.Mkdir(dir, 0755) } if err != nil { return } os.Remove(filepath.Join(dir, squashfsName)) fil, err = os.Create(filepath.Join(dir, squashfsName)) if err != nil { return } var resp *http.Response resp, err = http.DefaultClient.Get(squashfsURL) if err != nil { return } _, err = io.Copy(fil, resp.Body) if err != nil { return } } _, err = exec.LookPath("unsquashfs") if err != nil { return } _, err = exec.LookPath("mksquashfs") return } func TestMisc(t *testing.T) { tmpDir := "../testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } defer fil.Close() rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } t.Log(rdr.Superblock.FragCount) t.Fatal(rdr.fragEntry(1233)) } func TestReader(t *testing.T) { tmpDir := "../testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } defer fil.Close() rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } path := filepath.Join(tmpDir, "extractTest") os.RemoveAll(path) os.MkdirAll(path, 0777) err = extractToDir(rdr, &rdr.Root.FileBase, path) t.Fatal(err) } var singleFile = "PortableApps/CPU-X/CPU-X-v4.2.0-x86_64.AppImage" func TestSingleFile(t *testing.T) { tmpDir := "../testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } defer fil.Close() rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } path := filepath.Join(tmpDir, "extractTest") os.RemoveAll(path) os.MkdirAll(path, 0777) b, err := rdr.Root.Open(rdr, singleFile) if err != nil { t.Fatal(err) } err = extractToDir(rdr, &b, path) t.Fatal(err) } func extractToDir(rdr *Reader, b *FileBase, folder string) error { path := filepath.Join(folder, b.Name) if b.IsDir() { d, err := b.ToDir(rdr) if err != nil { return err } err = os.MkdirAll(path, 0777) if err != nil { return err } var nestBast FileBase for _, e := range d.Entries { nestBast, err = rdr.BaseFromEntry(e) if err != nil { return err } err = extractToDir(rdr, &nestBast, path) if err != nil { return err } } } else if b.IsRegular() { _, full, err := b.GetRegFileReaders(rdr) if err != nil { return err } fil, err := os.Create(path) if err != nil { return err } _, err = full.WriteTo(fil) if err != nil { return err } fmt.Println("Successfully extracted file:", b.Name) } return nil } sylabs-squashfs-97df6f5/low/superblock.go000066400000000000000000000031551476431707000206140ustar00rootroot00000000000000package squashfslow import "math" type superblock struct { Magic uint32 InodeCount uint32 ModTime uint32 BlockSize uint32 FragCount uint32 CompType uint16 BlockLog uint16 Flags uint16 IdCount uint16 VerMaj uint16 VerMin uint16 RootInodeRef uint64 Size uint64 IdTableStart uint64 XattrTableStart uint64 InodeTableStart uint64 DirTableStart uint64 FragTableStart uint64 ExportTableStart uint64 } func (s superblock) ValidMagic() bool { return s.Magic == 0x73717368 } func (s superblock) ValidBlockLog() bool { return s.BlockLog == uint16(math.Log2(float64(s.BlockSize))) } func (s superblock) ValidVersion() bool { return s.VerMaj == 4 && s.VerMin == 0 } func (s superblock) UncompressedInodes() bool { return s.Flags&0x1 == 0x1 } func (s superblock) UncompressedData() bool { return s.Flags&0x2 == 0x2 } func (s superblock) UncompressedFragments() bool { return s.Flags&0x8 == 0x8 } func (s superblock) NoFragments() bool { return s.Flags&0x10 == 0x10 } func (s superblock) AlwaysFragment() bool { return s.Flags&0x20 == 0x20 } func (s superblock) Duplicates() bool { return s.Flags&0x40 == 0x40 } func (s superblock) Exportable() bool { return s.Flags&0x80 == 0x80 } func (s superblock) UncompressedXattrs() bool { return s.Flags&0x100 == 0x100 } func (s superblock) NoXattrs() bool { return s.Flags&0x200 == 0x200 } func (s superblock) CompressionOptions() bool { return s.Flags&0x400 == 0x400 } func (s superblock) UncompressedIDs() bool { return s.Flags&0x800 == 0x800 } sylabs-squashfs-97df6f5/reader.go000066400000000000000000000011751476431707000171040ustar00rootroot00000000000000package squashfs import ( "io" "time" "github.com/sylabs/squashfs/internal/toreader" squashfslow "github.com/sylabs/squashfs/low" ) type Reader struct { *FS Low squashfslow.Reader } func NewReader(r io.ReaderAt) (*Reader, error) { rdr, err := squashfslow.NewReader(r) if err != nil { return nil, err } out := &Reader{ Low: *rdr, } out.FS = &FS{ d: rdr.Root, r: out, } return out, nil } func NewReaderAtOffset(r io.ReaderAt, offset int64) (*Reader, error) { return NewReader(toreader.NewOffsetReader(r, offset)) } func (r *Reader) ModTime() time.Time { return time.Unix(int64(r.Low.Superblock.ModTime), 0) } sylabs-squashfs-97df6f5/squashfs_test.go000066400000000000000000000104511476431707000205330ustar00rootroot00000000000000package squashfs //Actually proper tests go here. import ( "errors" "io" "io/fs" "net/http" "os" "os/exec" "path/filepath" "strconv" "testing" "time" ) const ( squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs" squashfsName = "LinuxPATest.sfs" ) func preTest(dir string) (fil *os.File, err error) { fil, err = os.Open(filepath.Join(dir, squashfsName)) if err != nil { _, err = os.Open(dir) if os.IsNotExist(err) { err = os.Mkdir(dir, 0755) } if err != nil { return } os.Remove(filepath.Join(dir, squashfsName)) fil, err = os.Create(filepath.Join(dir, squashfsName)) if err != nil { return } var resp *http.Response resp, err = http.DefaultClient.Get(squashfsURL) if err != nil { return } _, err = io.Copy(fil, resp.Body) if err != nil { return } } _, err = exec.LookPath("unsquashfs") if err != nil { return } _, err = exec.LookPath("mksquashfs") return } func TestMisc(t *testing.T) { tmpDir := "testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } _ = rdr // Put testing here // t.Fatal("UM") } func BenchmarkRace(b *testing.B) { tmpDir := "testing" fil, err := preTest(tmpDir) if err != nil { b.Fatal(err) } libPath := filepath.Join(tmpDir, "ExtractLib") unsquashPath := filepath.Join(tmpDir, "ExtractSquashfs") 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) if err != nil { b.Fatal(err) } libTime = time.Since(start) cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr start = time.Now() err = cmd.Run() if err != nil { 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("unsquashfs is", strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64), "times faster") } func TestExtractQuick(t *testing.T) { //First, setup everything and extract the archive using the library and unsquashfs // tmpDir := bTempDir() tmpDir := "testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } libPath := filepath.Join(tmpDir, "ExtractLib") unsquashPath := filepath.Join(tmpDir, "ExtractSquashfs") os.RemoveAll(libPath) os.RemoveAll(unsquashPath) rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } os.RemoveAll(filepath.Join(tmpDir, "testLog.txt")) logFil, _ := os.Create(filepath.Join(tmpDir, "testLog.txt")) op := FastOptions() op.Verbose = true op.IgnorePerm = true op.LogOutput = logFil err = rdr.ExtractWithOptions(libPath, op) if err != nil { t.Fatal(err) } cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name()) err = cmd.Run() if err != nil { t.Fatal(err) } //Then compare the sizes and existance between the two (using unsquashfs as a reference). //If the file doesn't exist, or the size is different, we exit. //TODO: Add long test that checks contents. squashFils := os.DirFS(unsquashPath) err = fs.WalkDir(squashFils, ".", func(path string, _ fs.DirEntry, _ error) error { libFil, e := os.Open(filepath.Join(libPath, path)) if e != nil { return e } sfsFile, e := os.Open(filepath.Join(unsquashPath, path)) if e != nil { return e } sfsStat, _ := sfsFile.Stat() libStat, _ := libFil.Stat() if sfsStat.Size() != libStat.Size() { t.Log(libFil.Name(), "not the same size between library and unsquashfs") t.Log("File is", libStat.Size()) t.Log("Should be", sfsStat.Size()) return errors.New("file not the correct size") } return nil }) if err != nil { t.Fatal(err) } } var filePath = "Start.exe" func TestSingleFile(t *testing.T) { tmpDir := "testing" fil, err := preTest(tmpDir) if err != nil { t.Fatal(err) } os.Remove(filepath.Join(tmpDir, filePath)) rdr, err := NewReader(fil) if err != nil { t.Fatal(err) } f, err := rdr.Open(filePath) if err != nil { t.Fatal(err) } op := DefaultOptions() op.Verbose = true err = f.(*File).ExtractWithOptions("testing", op) if err != nil { t.Fatal(err) } }