Compare commits

...

81 Commits

Author SHA1 Message Date
Caleb Gardner 327781d86e Fixed issues with fragments 2022-08-26 12:11:27 -05:00
Caleb Gardner 4efd2ee49d Merge pull request #16 from tri-adam/0.6.0-fixes
v0.6.0 fixes
2022-08-26 11:44:16 -05:00
Caleb Gardner 392193993c Added single file test 2022-08-26 11:43:46 -05:00
Adam Hughes 2230a449ec fix: use fs interfaces in type assertions
Previous code would panic due to invalid type assertions (presumably due
to change of type returned by func Sub). Switching to relevant fs
interface types fixes the issue and should work going forward, even if
the type is changed.

Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:10:51 +00:00
Adam Hughes 0e50efea64 fix: use correct count when reading fragments
Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:00:00 +00:00
Caleb Gardner 7a22538623 Finishing touches? 2022-08-26 05:01:17 -05:00
Caleb Gardner 3bf851852f Updated README (limitations) 2022-06-21 01:25:00 -05:00
Caleb Gardner ac89ff7275 Updated README. Performance may not be very good... 2022-06-21 01:23:41 -05:00
Caleb Gardner 83dfa77b7d Potential workaround for poor zstd performance
Performance is still not great, but better
2022-06-21 01:09:33 -05:00
Caleb Gardner 1b934de04d Messing with stuff 2022-06-19 16:25:50 -05:00
Caleb Gardner 981f1697ab Added benchmark race to proper tests 2022-06-19 02:02:31 -05:00
Caleb Gardner 214419b5c3 IT WORKS (again) 2022-06-19 00:32:33 -05:00
Caleb Gardner 8f5e1fef96 NEW ISSUES 2022-06-18 14:40:33 -05:00
Caleb Gardner 49595de3f2 Re-wrote metadata reader. Seems to work now.
Need to work on test now.
2022-06-18 14:31:17 -05:00
Caleb Gardner 96b38935a6 Found the problem file.
NOW TO DEBUG
2022-06-18 06:46:00 -05:00
Caleb Gardner 9ac8fef3b2 Fixing issues 2022-06-18 06:30:04 -05:00
Caleb Gardner cde6a265a1 Started work on proper tests.
STILL HAVING STUPID UNEXPLAINABLE NIL POINTERS.
2022-06-18 01:32:51 -05:00
Caleb Gardner 8613e35221 Fixed some bugs
THINGS ARE BROKEN FOR NO REASON
2022-05-10 01:40:32 -05:00
Caleb Gardner 16ef5838c3 Move changes from exp2 to main
This is largely a move to simplify a lot of the readers
Also further breaks out functions.
2022-05-10 01:12:13 -05:00
Caleb Gardner 0a2ced9072 Merge pull request #11 from tri-adam/path-fix
fix: handle paths with special characters
2022-04-22 04:25:42 -05:00
Adam Hughes a908d69987 fix: handle paths with special characters
Use direct comparison of filenames rather than path.Match, which gives
characters such as '[' special meaning, resulting in unexpected failures
when calling Open, ReadDir, Stat, or Sub.
2022-04-22 05:02:57 +00:00
Caleb Gardner 6ada4f3b49 Create FUNDING.yml 2021-12-31 00:35:04 -06:00
Caleb Gardner 89f28cec6e Merge pull request #6 from stffabi/feature/support-reading-dot
Support reading "." for fs.FS
2021-12-02 07:40:27 -06:00
stffabi c988309edc Support reading "." for fs.FS 2021-12-02 13:43:18 +01:00
Caleb Gardner e8a8c531a9 Tweaks to make FromReader work 2021-09-27 01:27:58 -05:00
Caleb Gardner 80ff4466ae Added new test 2021-09-26 22:36:30 -05:00
Caleb Gardner 64055a8a63 Improved testing 2021-09-26 19:10:43 -05:00
Caleb Gardner 0402b0a2ee Bringing rawreader from expiremental branch.
Now allows creation of a squashfs.Reader from an io.Reader
2021-09-26 18:30:08 -05:00
Caleb Gardner 305f261d10 Add Lzo decompressor and Xz decompressor with filters 2021-09-12 05:26:47 -05:00
Caleb Gardner 70e3d81427 Some musings on what to do. 2021-04-30 03:32:31 -05:00
Caleb Gardner 6ad6857d8d Renamed files to make them more clear
Trying to figure out how to write. Might have to keep tables uncompressed for now.
2021-04-30 02:52:27 -05:00
Caleb Gardner 7cf15d48c7 Getting back into it. Maybe. 2021-04-04 09:39:28 -05:00
Caleb Gardner 28f39cf315 Updated libraries.
Replaced builtin zlib with faster library.
2021-04-03 10:14:12 -05:00
Caleb Gardner 7f5fa3ba1f Some quick fixes for "correctness" 2021-04-03 01:17:09 -05:00
Caleb Gardner 2a8310a724 Updated README 2021-02-27 01:58:01 -06:00
Caleb Gardner 8ad4d30bc7 Merge branch 'main' of https://github.com/CalebQ42/squashfs 2021-02-27 00:50:40 -06:00
Caleb Gardner b6fbd63ba4 Makes sure folders are created when files are added
Added some comments
2021-02-27 00:49:40 -06:00
Caleb Gardner 9913b848c6 Added some data writing logic. 2021-02-26 01:53:16 -06:00
Caleb Gardner 65bc4a5d78 More work on fragments. 2021-02-25 06:22:57 -06:00
Caleb Gardner c9d451e24c Added fragment calculations (untested). 2021-02-25 03:17:20 -06:00
Caleb Gardner ae5ade0683 Merge pull request #3 from CalebQ42/go1.16_fs3
Added 1.16 io/fs interfaces
2021-02-24 23:40:20 -06:00
Caleb Gardner a7b6801d2b Merge branch 'main' into go1.16_fs3 2021-02-24 23:39:45 -06:00
Caleb Gardner a123935250 Added more necessary parts to compression. 2021-02-24 05:58:18 -06:00
Caleb Gardner 8a91e1a1c1 Getting back into it... 2021-02-23 22:26:15 -06:00
Caleb Gardner 07962426b2 Minor work on the Writer 2021-02-03 14:03:21 -06:00
Caleb Gardner d89153c3e2 Finished io/FS interface 2021-01-30 06:30:00 -06:00
Caleb Gardner 3f1b2a8d1e Restructure for 1.16 io/fs interface 2021-01-29 12:55:57 -06:00
Caleb Gardner 3691e1f486 Merge pull request #2 from srevinsaju/patch-1
fix: typo in readme
2021-01-27 08:35:41 -06:00
Srevin Saju 8ab566521d fix: typo in readme 2021-01-27 12:54:29 +03:00
Caleb Gardner dd08d3516d Reader will now check BlockLog 2021-01-20 14:10:14 -06:00
Caleb Gardner 8dba30e24f Some setup for 1.16 2021-01-20 12:55:39 -06:00
Caleb Gardner d4e2577075 More work on writing 2021-01-17 02:09:13 -06:00
Caleb Gardner 23371163c0 Merge branch 'main' of https://github.com/CalebQ42/squashfs 2021-01-16 15:43:04 -06:00
Caleb Gardner 69f56d6951 Working on how to actually write the archive.
Made SuperblockFlags public so you can set options when writing
2021-01-16 15:42:55 -06:00
Caleb Gardner 17e1d65488 Fixed Extended Files 2021-01-16 03:09:48 -06:00
Caleb Gardner 80946f58e7 Fixed issue with Extended Symlinks
Removed some shadowed err's
2021-01-16 01:32:00 -06:00
Caleb Gardner 4187598783 A couple of fixes.
GetChildrenRecursively is no longer threaded so it's more consistent
Fixed GetFileAtPath, specifically when getting the root dir
2021-01-15 10:57:03 -06:00
Caleb Gardner 9cf92c4916 Removed some shadowed values (thanks gopls) 2021-01-13 11:45:10 -06:00
Caleb Gardner 407d649b3d Finished FixSymlinks (in theory) 2021-01-13 06:02:15 -06:00
Caleb Gardner dcf59a4261 The root inode is now only initialized once.
Privated File.Reader because it really shouldn't be public.
2021-01-12 01:43:03 -06:00
Caleb Gardner 1506ca0ac3 updated dependencies 2021-01-10 04:39:19 -06:00
Caleb Gardner fe9344b633 Fixed issue with not all data being extracted with ExtractTo 2021-01-10 04:25:09 -06:00
Caleb Gardner 76649fde7f Implemented WriteTo which halves decompress times.
Added a drag race benchmark (for the fun of it)
2021-01-10 03:33:33 -06:00
Caleb Gardner ee9406513c Added a new test for fun to compare vs unsquashfs 2021-01-09 10:15:56 -06:00
Caleb Gardner 18092c63aa Some cleanup, no change in functionality 2021-01-09 09:53:58 -06:00
Caleb Gardner b2d6ff56f6 Added uid/guid support.
Added permission support.
2021-01-09 02:30:04 -06:00
Caleb Gardner 4b3d5d12f8 Setup for differnet types of compression for Writer
Added some TODOs
2021-01-08 14:35:32 -06:00
Caleb Gardner 3f71404a2a Re-Write of the Writer to make it simpler 2021-01-08 05:09:38 -06:00
Caleb Gardner 35d22b4bd0 Removed close function from squashfs.File
This seems to cause issues in ver specific circumstances and ultimately, isn't needed.
2021-01-06 12:56:09 -06:00
Caleb Gardner 97b12090c6 Trying to figure out the best way to convert files 2021-01-05 05:53:16 -06:00
Caleb Gardner 43fe4f91a2 More work on Writer
Adding files to the Writer should work properly now (except symlinks)
Threaded the adding of files.
Added ability to ignore errors when adding files.
2021-01-03 04:39:39 -06:00
Caleb Gardner 9524a2c192 More work on Writer 2021-01-02 17:10:21 -06:00
Caleb Gardner 162b228881 Some beginning work on squashfs.Writer
Just laying out some of the functions and what they do.
2021-01-01 10:54:09 -06:00
Caleb Gardner 7f87999a8f Merge branch 'main' of https://github.com/CalebQ42/squashfs into main 2020-12-28 11:51:08 -06:00
Caleb Gardner 47c28baf87 Improved data structure for structs.
Thanks gopls with VS Code
2020-12-28 11:50:56 -06:00
Caleb Gardner a298a3d7b5 Fixed first byte of data blocks being cut off 2020-12-27 02:10:51 -06:00
Caleb Gardner 89ec7eb0fb Fixed GetSymlinkFile
Added GetSymlinkFileRecursive
2020-12-16 02:39:35 -06:00
Caleb Gardner d63ba47818 Added Reader.ModTime 2020-12-11 01:39:08 -06:00
Caleb Gardner 495d2345a4 File now implements os.FileInfo 2020-12-11 01:38:53 -06:00
Caleb Gardner 6dfcb1cf80 Merge branch 'main' of https://github.com/CalebQ42/squashfs into main 2020-12-10 08:52:58 -06:00
Caleb Gardner c7593eaff3 Added Reader.ExtractTo for ease of use 2020-12-10 08:52:49 -06:00
44 changed files with 2235 additions and 2200 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: CalebQ42
+11 -7
View File
@@ -4,15 +4,19 @@
A PURE Go library to read and write squashfs.
Currently has support for reading squashfs files and extracting files and folders. Supports all compression types except LZO, but additional compression options are hit or miss.
Currently has support for reading squashfs files and extracting files and folders.
The only major thing missing from squashfs reading is Xattr parsing.
Special thanks to https://dr-emann.github.io/squashfs/ for some VERY important information in an easy to understand format.
Special thanks to <https://dr-emann.github.io/squashfs/> for some VERY important information in an easy to understand format.
Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree/master/internal/squashfs) as I referenced it to figure some things out (and double check others).
# [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
## [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
# Where I'm at
## Limitations
* Working on the File interface that should make it easier to deal with squashfs files. I'm also trying to make them capable for when I get squashing working.
* No Xattr parsing. This is simply because I haven't done any research on it and how to apply these in a pure go way.
## Performance
This library, decompressing the Firefox AppImage and using go tests, takes about twice as long as `unsquashfs` on my quad core laptop. (~1 second with the library and about half a second with `unsquashfs`).
**My recents tests have shown the Firefox AppImage might be an outlier and this library might be considerably slower (4x ~ 6x time slower then `unsquashfs`)**
-190
View File
@@ -1,190 +0,0 @@
package squashfs
import (
"bytes"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
var (
//ErrInodeNotFile is given when giving an inode, but the function requires a file inode.
errInodeNotFile = errors.New("Given inode is NOT a file type")
//ErrInodeOnlyFragment is given when trying to make a DataReader from an inode, but the inode only had data in a fragment
errInodeOnlyFragment = errors.New("Given inode ONLY has fragment data")
)
//DataReader reads data from data blocks.
type dataReader struct {
r *Reader
offset int64 //offset relative to the beginning of the squash file
blocks []dataBlock
curBlock int //Which block in sizes is currently cached
curData []byte
curReadOffset int //offset relative to the currently cached data
}
//DataBlock holds info about a given data block from it's size
type dataBlock struct {
begOffset int64 //The offset relative to the beginning of the squash file. Makes it easier to seek to it.
size uint32
compressed bool
uncompressedSize uint32
}
//NewDataBlock creates a new squashfs.datablock from a given size.
func newDataBlock(raw uint32) (dbs dataBlock) {
dbs.compressed = raw&(1<<24) != (1 << 24)
dbs.size = raw &^ (1 << 24)
if !dbs.compressed {
dbs.uncompressedSize = dbs.size
}
return
}
//NewDataReader creates a new data reader at the given offset, with the blocks defined by sizes
func (r *Reader) newDataReader(offset int64, sizes []uint32) (*dataReader, error) {
var dr dataReader
dr.r = r
dr.offset = offset
for _, size := range sizes {
dr.blocks = append(dr.blocks, newDataBlock(size))
}
err := dr.readCurBlock()
if err != nil {
return nil, err
}
return &dr, nil
}
//NewDataReaderFromInode creates a new DataReader from a given inode. Inode must be of BasicFile or ExtendedFile types
func (r *Reader) newDataReaderFromInode(i *inode.Inode) (*dataReader, error) {
var rdr dataReader
rdr.r = r
switch i.Type {
case inode.BasicFileType:
fil := i.Info.(inode.BasicFile)
if fil.Init.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.Init.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.blocks = append(rdr.blocks, newDataBlock(sizes))
}
if fil.Fragmented {
rdr.blocks = rdr.blocks[:len(rdr.blocks)-1]
}
case inode.ExtFileType:
fil := i.Info.(inode.ExtendedFile)
if fil.Init.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.Init.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.blocks = append(rdr.blocks, newDataBlock(sizes))
}
if fil.Fragmented {
rdr.blocks = rdr.blocks[:len(rdr.blocks)-1]
}
default:
return nil, errInodeNotFile
}
err := rdr.readCurBlock()
if err != nil {
return nil, err
}
return &rdr, nil
}
func (d *dataReader) readNextBlock() error {
d.curBlock++
if d.curBlock >= len(d.blocks) {
d.curBlock--
return io.EOF
}
err := d.readCurBlock()
if err != nil {
d.curBlock--
d.readCurBlock()
return err
}
return nil
}
func (d *dataReader) readCurBlock() error {
if d.curBlock >= len(d.blocks) {
return io.EOF
}
if d.blocks[d.curBlock].size == 0 {
d.curData = make([]byte, d.r.super.BlockSize)
d.blocks[d.curBlock].uncompressedSize = d.r.super.BlockSize
d.blocks[d.curBlock].begOffset = d.offset
return nil
}
sec := io.NewSectionReader(d.r.r, d.offset, int64(d.blocks[d.curBlock].size))
if d.blocks[d.curBlock].compressed {
btys, err := d.r.decompressor.Decompress(sec)
if err != nil {
return err
}
d.blocks[d.curBlock].uncompressedSize = uint32(len(btys))
d.curData = btys
d.blocks[d.curBlock].begOffset = d.offset
d.offset += int64(d.blocks[d.curBlock].size)
return nil
}
var buf bytes.Buffer
_, err := io.Copy(&buf, sec)
if err != nil {
return err
}
d.curData = buf.Bytes()
d.blocks[d.curBlock].begOffset = d.offset
d.offset += int64(d.blocks[d.curBlock].size)
return err
}
//Close frees up the curData from memory
func (d *dataReader) Close() error {
d.curData = nil
return nil
}
func (d *dataReader) Read(p []byte) (int, error) {
if d.curData == nil {
err := d.readCurBlock()
if err != nil {
return 0, err
}
}
if d.curReadOffset+len(p) <= len(d.curData) {
for i := 0; i < len(p); i++ {
p[i] = d.curData[d.curReadOffset+i]
}
d.curReadOffset += len(p)
return len(p), nil
}
read := 0
for read < len(p) {
if d.curReadOffset == len(d.curData) {
err := d.readNextBlock()
if err != nil {
return read, err
}
d.curReadOffset = 0
}
for ; read < len(p); read++ {
d.curReadOffset++
if d.curReadOffset < len(d.curData) {
p[read] = d.curData[d.curReadOffset]
} else {
break
}
}
}
if read != len(p) {
return read, errors.New("Didn't read enough data")
}
return read, nil
}
-497
View File
@@ -1,497 +0,0 @@
package squashfs
import (
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
var (
//ErrNotDirectory is returned when you're trying to do directory things with a non-directory
errNotDirectory = errors.New("File is not a directory")
//ErrNotFile is returned when you're trying to do file things with a directory
errNotFile = errors.New("File is not a file")
//ErrNotReading is returned when running functions that are only meant to be used when reading a squashfs
errNotReading = errors.New("Function only supported when reading a squashfs")
//ErrBrokenSymlink is returned when using ExtractWithOptions with the unbreakSymlink set to true, but the symlink's file cannot be extracted.
ErrBrokenSymlink = errors.New("Extracted symlink is probably broken")
)
//File is the main way to interact with files within squashfs, or when putting files into a squashfs.
//File can be either a file or folder. When reading from a squashfs, it reads from the datablocks.
//When writing, this holds the information on WHERE the file will be placed inside the archive.
type File struct {
Name string //The name of the file or folder. Root folder will not have a name ("")
Parent *File //The parent directory. Should ALWAYS be a folder. If it's the root directory, will be nil
Reader io.Reader //Underlying reader. When writing, will probably be an os.File. When reading this is kept nil UNTIL reading to save memory.
path string //The path to the folder the File is located in.
r *Reader //The squashfs.Reader where this file is contained.
in *inode.Inode //Underlyting inode when reading.
filType int //The file's type, using inode types.
}
//get a File from a directory.entry
func (r *Reader) newFileFromDirEntry(entry *directory.Entry) (fil *File, err error) {
fil = new(File)
fil.in, err = r.getInodeFromEntry(entry)
if err != nil {
return nil, err
}
fil.Name = entry.Name
fil.r = r
fil.filType = fil.in.Type
return
}
//GetChildren returns a *squashfs.File slice of every direct child of the directory. If the File is not a directory, will return ErrNotDirectory
func (f *File) GetChildren() (children []*File, err error) {
children = make([]*File, 0)
if f.r == nil {
return nil, errNotReading
}
if !f.IsDir() {
return nil, errNotDirectory
}
dir, err := f.r.readDirFromInode(f.in)
if err != nil {
return
}
var fil *File
for _, entry := range dir.Entries {
fil, err = f.r.newFileFromDirEntry(&entry)
if err != nil {
return
}
fil.Parent = f
if f.Name != "" {
fil.path = f.Path()
}
children = append(children, fil)
}
return
}
//GetChildrenRecursively returns ALL children. Goes down ALL folder paths.
func (f *File) GetChildrenRecursively() (children []*File, err error) {
children = make([]*File, 0)
if f.r == nil {
return nil, errNotReading
}
if !f.IsDir() {
return nil, errNotDirectory
}
children, err = f.GetChildren()
if err != nil {
return
}
var childFolders []*File
for _, child := range children {
if child.IsDir() {
childFolders = append(childFolders, child)
}
}
foldChil := make(chan []*File)
errChan := make(chan error)
for _, folds := range childFolders {
go func(fil *File) {
childs, err := fil.GetChildrenRecursively()
errChan <- err
foldChil <- childs
}(folds)
}
for range childFolders {
err = <-errChan
if err != nil {
return
}
children = append(children, <-foldChil...)
}
return
}
//Path returns the path of the file within the archive.
func (f *File) Path() string {
if f.Name == "" {
return f.path
}
return f.path + "/" + f.Name
}
//GetFileAtPath tries to return the File at the given path, relative to the file.
//Returns nil if called on something other then a folder, OR if the path goes oustide the archive.
//Allows wildcards supported by path.Match (namely * and ?).
func (f *File) GetFileAtPath(dirPath string) *File {
if dirPath == "" {
return f
}
dirPath = strings.TrimSuffix(strings.TrimPrefix(dirPath, "/"), "/")
if dirPath != "" && !f.IsDir() {
return nil
}
for strings.HasSuffix(dirPath, "./") {
//since you can TECHNICALLY have an infinite amount of ./ and it would still be valid.
dirPath = strings.TrimPrefix(dirPath, "./")
}
split := strings.Split(dirPath, "/")
if split[0] == ".." && f.Name == "" {
return nil
} else if split[0] == ".." {
if f.Parent != nil {
return f.Parent.GetFileAtPath(strings.Join(split[1:], "/"))
}
return nil
}
children, err := f.GetChildren()
if err != nil {
return nil
}
for _, child := range children {
eq, _ := path.Match(split[0], child.Name)
if eq {
return child.GetFileAtPath(strings.Join(split[1:], "/"))
}
}
return nil
}
//IsDir returns if the file is a directory.
func (f *File) IsDir() bool {
return f.filType == inode.BasicDirectoryType || f.filType == inode.ExtDirType
}
//IsSymlink returns if the file is a symlink.
func (f *File) IsSymlink() bool {
return f.filType == inode.BasicSymlinkType || f.filType == inode.ExtSymlinkType
}
//IsFile returns if the file is a file.
func (f *File) IsFile() bool {
return f.filType == inode.BasicFileType || f.filType == inode.ExtFileType
}
//SymlinkPath returns the path the symlink is pointing to. If the file ISN'T a symlink, will return an empty string.
//If a path begins with "/" then the symlink is pointing to an absolute path (starting from root, and not a file inside the archive)
func (f *File) SymlinkPath() string {
switch f.filType {
case inode.BasicSymlinkType:
return f.in.Info.(inode.BasicSymlink).Path
case inode.ExtSymlinkType:
return f.in.Info.(inode.ExtendedSymlink).Path
default:
return ""
}
}
//GetSymlinkFile tries to return the squashfs.File associated with the symlink
func (f *File) GetSymlinkFile() *File {
if !f.IsSymlink() {
return nil
}
if strings.HasSuffix(f.SymlinkPath(), "/") {
return nil
}
return f.r.GetFileAtPath(f.SymlinkPath())
}
//Permission returns the os.FileMode of the File. Sets mode bits for directories and symlinks.
func (f *File) Permission() os.FileMode {
mode := os.FileMode(f.in.Header.Permissions)
switch {
case f.IsDir():
mode = mode | os.ModeDir
case f.IsSymlink():
mode = mode | os.ModeSymlink
}
return mode
}
//ExtractTo extracts the file to the given path. This is the same as ExtractWithOptions(path, false, false, os.ModePerm, false).
//Will NOT try to keep symlinks valid, folders extracted will have the permissions set by the squashfs, but the folder to make path will have full permissions (777).
//
//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned.
func (f *File) ExtractTo(path string) []error {
return f.ExtractWithOptions(path, false, false, os.ModePerm, false)
}
//ExtractSymlink is similar to ExtractTo, but when it extracts a symlink, it instead extracts the file associated with the symlink in it's place.
//This is the same as ExtractWithOptions(path, true, false, os.ModePerm, false)
func (f *File) ExtractSymlink(path string) []error {
return f.ExtractWithOptions(path, true, false, os.ModePerm, false)
}
//ExtractWithOptions will extract the file to the given path, while allowing customization on how it works. ExtractTo is the "default" options.
//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned.
//Should only return multiple errors if extracting a folder.
//
//If dereferenceSymlink is set, instead of extracting a symlink, it will extract the file the symlink is pointed to in it's place.
//If both dereferenceSymlink and unbreakSymlink is set, dereferenceSymlink takes precendence.
//
//If unbreakSymlink is set, it will also try to extract the symlink's associated file. WARNING: the symlink's file may have to go up the directory to work.
//If unbreakSymlink is set and the file cannot be extracted, a ErrBrokenSymlink will be appended to the returned error slice.
//
//folderPerm only applies to the folders created to get to path. Folders from the archive are given the correct permissions defined by the archive.
func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlink bool, folderPerm os.FileMode, verbose bool) (errs []error) {
errs = make([]error, 0)
err := os.MkdirAll(path, folderPerm)
if err != nil {
return []error{err}
}
switch {
case f.IsDir():
if f.Name != "" {
//TODO: check if folder is present, and if so, try to set it's permission
err = os.Mkdir(path+"/"+f.Name, os.ModePerm)
if err != nil {
if verbose {
fmt.Println("Error while making: ", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err := os.Open(path + "/" + f.Name)
if err != nil {
if verbose {
fmt.Println("Error while opening:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID]))
//don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0
// if err != nil {
// if verbose {
// fmt.Println("Error while changing owner:", path+"/"+f.Name)
// fmt.Println(err)
// }
// errs = append(errs, err)
// }
err = fil.Chmod(f.Permission())
if err != nil {
if verbose {
fmt.Println("Error while changing owner:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
}
}
children, err := f.GetChildren()
if err != nil {
if verbose {
fmt.Println("Error getting children for:", f.Path())
fmt.Println(err)
}
errs = append(errs, err)
return
}
finishChan := make(chan []error)
defer close(finishChan)
for _, child := range children {
go func(child *File) {
if f.Name == "" {
finishChan <- child.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
} else {
finishChan <- child.ExtractWithOptions(path+"/"+f.Name, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
}
}(child)
}
for range children {
errs = append(errs, (<-finishChan)...)
}
return
case f.IsFile():
fil, err := os.Create(path + "/" + f.Name)
if os.IsExist(err) {
err = os.Remove(path + "/" + f.Name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err = os.Create(path + "/" + f.Name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
} else if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
} //Since we will be reading from the file
_, err = io.Copy(fil, f)
if err != nil {
if verbose {
fmt.Println("Error while Copying data to:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
f.Close()
fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID]))
//don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0
// if err != nil {
// if verbose {
// fmt.Println("Error while changing owner:", path+"/"+f.Name)
// fmt.Println(err)
// }
// errs = append(errs, err)
// return
// }
err = fil.Chmod(f.Permission())
if err != nil {
if verbose {
fmt.Println("Error while setting permissions for:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
}
return
case f.IsSymlink():
symPath := f.SymlinkPath()
if dereferenceSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.Name)
}
return
}
fil.Name = f.Name
extracSymErrs := fil.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.Name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
}
return
} else if unbreakSymlink {
fil := f.GetSymlinkFile()
if fil != nil {
symPath = path + "/" + symPath
paths := strings.Split(symPath, "/")
extracSymErrs := fil.ExtractWithOptions(strings.Join(paths[:len(paths)-1], "/"), dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.Name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
}
} else {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.Name)
}
return
}
}
err = os.Symlink(f.SymlinkPath(), path+"/"+f.Name)
if err != nil {
if verbose {
fmt.Println("Error while making symlink:", path+"/"+f.Name)
fmt.Println(err)
}
errs = append(errs, err)
}
}
return
}
//Close frees up the memory held up by the underlying reader. Should NOT be called when writing.
//When reading, Close is safe to use, but any subsequent Read calls resets to the beginning of the file.
func (f *File) Close() error {
if f.IsDir() {
return errNotFile
}
if f.Reader != nil {
if closer, is := f.Reader.(io.Closer); is {
closer.Close()
}
f.Reader = nil
}
return nil
}
//Read from the file. Doesn't do anything fancy, just pases it to the underlying io.Reader. If a directory, return io.EOF.
func (f *File) Read(p []byte) (int, error) {
if f.IsDir() {
return 0, io.EOF
}
var err error
if f.Reader == nil && f.r != nil {
f.Reader, err = f.r.newFileReader(f.in)
if err != nil {
return 0, err
}
}
return f.Reader.Read(p)
}
//ReadDirFromInode returns a fully populated Directory from a given Inode.
//If the given inode is not a directory it returns an error.
func (r *Reader) readDirFromInode(i *inode.Inode) (*directory.Directory, error) {
var offset uint32
var metaOffset uint16
var size uint32
switch i.Type {
case inode.BasicDirectoryType:
offset = i.Info.(inode.BasicDirectory).DirectoryIndex
metaOffset = i.Info.(inode.BasicDirectory).DirectoryOffset
size = uint32(i.Info.(inode.BasicDirectory).DirectorySize)
case inode.ExtDirType:
offset = i.Info.(inode.ExtendedDirectory).Init.DirectoryIndex
metaOffset = i.Info.(inode.ExtendedDirectory).Init.DirectoryOffset
size = i.Info.(inode.ExtendedDirectory).Init.DirectorySize
default:
return nil, errors.New("Not a directory inode")
}
br, err := r.newMetadataReader(int64(r.super.DirTableStart + uint64(offset)))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(metaOffset), io.SeekStart)
if err != nil {
return nil, err
}
dir, err := directory.NewDirectory(br, size)
if err != nil {
return dir, err
}
return dir, nil
}
//GetInodeFromEntry returns the inode associated with a given directory.Entry
func (r *Reader) getInodeFromEntry(en *directory.Entry) (*inode.Inode, error) {
br, err := r.newMetadataReader(int64(r.super.InodeTableStart + uint64(en.Header.InodeOffset)))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(en.Init.Offset), io.SeekStart)
if err != nil {
return nil, err
}
i, err := inode.ProcessInode(br, r.super.BlockSize)
if err != nil {
return nil, err
}
return i, nil
}
-94
View File
@@ -1,94 +0,0 @@
package squashfs
import (
"bytes"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
//FileReader provides a io.Reader interface for files within a squashfs archive
type fileReader struct {
r *Reader
data *dataReader
in *inode.Inode
fragmentData []byte
fragged bool
fragOnly bool
read int
FileSize int //FileSize is the total size of the given file
}
var (
//ErrPathIsNotFile returns when trying to read from a file, but the given path is NOT a file.
errPathIsNotFile = errors.New("The given path is not a file")
)
//ReadFile provides a squashfs.FileReader for the file at the given location.
func (r *Reader) newFileReader(in *inode.Inode) (*fileReader, error) {
var rdr fileReader
rdr.in = in
if in.Type != inode.BasicFileType && in.Type != inode.ExtFileType {
return nil, errPathIsNotFile
}
switch in.Type {
case inode.BasicFileType:
fil := in.Info.(inode.BasicFile)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.Init.BlockStart == 0
rdr.FileSize = int(fil.Init.Size)
case inode.ExtFileType:
fil := in.Info.(inode.ExtendedFile)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.Init.BlockStart == 0
rdr.FileSize = int(fil.Init.Size)
}
var err error
if rdr.fragged {
rdr.fragmentData, err = r.getFragmentDataFromInode(in)
if err != nil {
return nil, err
}
}
if !rdr.fragOnly {
rdr.data, err = r.newDataReaderFromInode(in)
}
return &rdr, nil
}
//Close runs Close on the data reader and frees the fragmentdata
func (f *fileReader) Close() error {
if f.data != nil {
f.data.Close()
}
f.fragmentData = nil
return nil
}
func (f *fileReader) Read(p []byte) (int, error) {
if f.fragOnly {
n, err := bytes.NewBuffer(f.fragmentData[f.read:]).Read(p)
f.read += n
if err != nil {
return n, err
}
return n, nil
}
var read int
n, err := f.data.Read(p)
read += n
if f.fragged && err == io.EOF {
if f.fragmentData == nil {
f.fragmentData, err = f.r.getFragmentDataFromInode(f.in)
}
n, err = bytes.NewBuffer(f.fragmentData).Read(p[read:])
read += n
if err != nil {
return read, err
}
} else if err != nil {
return read, err
}
return read, nil
}
-80
View File
@@ -1,80 +0,0 @@
package squashfs
import (
"encoding/binary"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
//FragmentEntry is an entry in the fragment table
type fragmentEntry struct {
Start uint64
Size uint32
Unused uint32
}
//GetFragmentDataFromInode returns the fragment data for a given inode.
//If the inode does not have a fragment, harmlessly returns an empty slice without an error.
func (r *Reader) getFragmentDataFromInode(in *inode.Inode) ([]byte, error) {
var size uint32
var fragIndex uint32
var fragOffset uint32
if in.Type == inode.BasicFileType {
bf := in.Info.(inode.BasicFile)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.Init.BlockStart == 0 {
size = bf.Init.Size
} else {
size = bf.BlockSizes[len(bf.BlockSizes)-1]
}
fragIndex = bf.Init.FragmentIndex
fragOffset = bf.Init.FragmentOffset
} else if in.Type == inode.ExtFileType {
bf := in.Info.(inode.ExtendedFile)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.Init.BlockStart == 0 {
size = bf.Init.Size
} else {
size = bf.BlockSizes[len(bf.BlockSizes)-1]
}
fragIndex = bf.Init.FragmentIndex
fragOffset = bf.Init.FragmentOffset
} else {
return nil, errors.New("Inode type not supported")
}
//reading the fragment entry first
fragEntryRdr, err := r.newMetadataReader(int64(r.fragOffsets[int(fragIndex/512)]))
if err != nil {
return nil, err
}
_, err = fragEntryRdr.Seek(int64(16*fragIndex), io.SeekStart)
if err != nil {
return nil, err
}
var entry fragmentEntry
err = binary.Read(fragEntryRdr, binary.LittleEndian, &entry)
if err != nil {
return nil, err
}
//now reading the actual fragment
dr, err := r.newDataReader(int64(entry.Start), []uint32{entry.Size})
if err != nil {
return nil, err
}
_, err = dr.Read(make([]byte, fragOffset))
if err != nil {
return nil, err
}
tmp := make([]byte, size)
err = binary.Read(dr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
return tmp, nil
}
+6 -13
View File
@@ -1,18 +1,11 @@
module github.com/CalebQ42/squashfs
go 1.15
go 1.19
require (
github.com/CalebQ42/GoAppImage v0.4.0
github.com/adrg/xdg v0.2.3 // indirect
github.com/google/go-cmp v0.5.4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/klauspost/compress v1.11.3
github.com/kr/text v0.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.1
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.8
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
github.com/klauspost/compress v1.15.9
github.com/pierrec/lz4/v4 v4.1.15
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e
github.com/therootcompany/xz v1.0.1
github.com/ulikunitz/xz v0.5.10
)
+10 -73
View File
@@ -1,73 +1,10 @@
github.com/CalebQ42/GoAppImage v0.4.0 h1:aF+Y/vyo/RGhoyZEW1CMY6WyRWrZZO4ydsRFAtIGnaY=
github.com/CalebQ42/GoAppImage v0.4.0/go.mod h1:qHudJKAn/dlkNWNnH4h1YKXp29EZ7Bppsn7sNP2HuvU=
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/adrg/xdg v0.2.3 h1:GxXngdYxNDkoUvZXjNJGwqZxWXi43MKbOOlA/00qZi4=
github.com/adrg/xdg v0.2.3/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pierrec/lz4/v4 v4.1.1 h1:cS6aGkNLJr4u+UwaA21yp+gbWN3WJWtKo1axmPDObMA=
github.com/pierrec/lz4/v4 v4.1.1/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M=
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.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-66
View File
@@ -1,66 +0,0 @@
package compression
import (
"bytes"
"compress/zlib"
"encoding/binary"
"io"
)
type gzipInit struct {
CompressionLevel int32
WindowSize int16
Strategies int16
}
//Gzip is a decompressor for gzip type compression. Uses zlib for compression and decompression
type Gzip struct {
CompressionLevel int32
HasCustomWindow bool
HasStrategies bool
}
//NewGzipCompressorWithOptions creates a new gzip compressor/decompressor with options read from the given reader.
func NewGzipCompressorWithOptions(r io.Reader) (*Gzip, error) {
var gzip Gzip
var init gzipInit
err := binary.Read(r, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
gzip.CompressionLevel = init.CompressionLevel
//TODO: proper support for window size and strategies
gzip.HasCustomWindow = init.WindowSize != 15
gzip.HasStrategies = init.Strategies != 0 && init.Strategies != 1
return &gzip, nil
}
//Decompress reads the entirety of the given reader and returns it uncompressed as a byte slice.
func (g *Gzip) Decompress(r io.Reader) ([]byte, error) {
rdr, err := zlib.NewReader(r)
if err != nil {
return nil, err
}
var data bytes.Buffer
_, err = io.Copy(&data, rdr)
if err != nil {
return nil, err
}
return data.Bytes(), nil
}
//Compress compresses the given data (as a byte array) and returns the compressed data.
func (g *Gzip) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
wrt := zlib.NewWriter(&buf)
defer wrt.Close()
_, err := wrt.Write(data)
if err != nil {
return nil, err
}
err = wrt.Flush()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-37
View File
@@ -1,37 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/pierrec/lz4/v4"
)
//Lz4 is a Lz4 Compressor/Decompressor
type Lz4 struct {
HC bool
}
//NewLz4CompressorWithOptions creates a new lz4 compressor/decompressor with options read from the given reader.
func NewLz4CompressorWithOptions(r io.Reader) (*Lz4, error) {
var lz4 Lz4
var init struct {
Version int32
Flags int32
}
err := binary.Read(r, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
lz4.HC = init.Flags == 1
return &lz4, nil
}
//Decompress decompresses all data from r and returns the uncompressed bytes
func (l *Lz4) Decompress(r io.Reader) ([]byte, error) {
rdr := lz4.NewReader(r)
var buf bytes.Buffer
_, err := io.Copy(&buf, rdr)
return buf.Bytes(), err
}
-25
View File
@@ -1,25 +0,0 @@
package compression
import (
"bytes"
"io"
"github.com/ulikunitz/xz/lzma"
)
//Lzma is a lzma decompressor
type Lzma struct{}
//Decompress decompresses all the data in the given reader and returns the uncompressed bytes.
func (l *Lzma) Decompress(rdr io.Reader) ([]byte, error) {
r, err := lzma.NewReader(rdr)
if err != nil {
return nil, err
}
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-13
View File
@@ -1,13 +0,0 @@
package compression
import "io"
//Compressor is a squashfs decompressor interface. Allows for easy compression.
type Compressor interface {
Compress([]byte) ([]byte, error)
}
//Decompressor is a squashfs decompressor interface. Allows for easy decompression no matter the type of compression.
type Decompressor interface {
Decompress(io.Reader) ([]byte, error)
}
-55
View File
@@ -1,55 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/ulikunitz/xz"
)
type xzInit struct {
DictionarySize int32
Filters int32
}
//Xz is a Xz decompressor.
type Xz struct {
DictionarySize int32
HasFilters bool
}
//NewXzCompressorWithOptions creates a new Xz compressor/decompressor that reads the compressor options from the given reader.
func NewXzCompressorWithOptions(rdr io.Reader) (*Xz, error) {
var x Xz
var init xzInit
err := binary.Read(rdr, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
x.DictionarySize = init.DictionarySize
//TODO: When I can do filters, parse the filters
if init.Filters != 0 {
x.HasFilters = true
}
return &x, nil
}
//Decompress decompresses all the data from the rdr and returns the uncompressed bytes.
func (x *Xz) Decompress(rdr io.Reader) ([]byte, error) {
r, err := xz.NewReader(rdr)
if err != nil {
return nil, err
}
r.DictCap = int(x.DictionarySize)
err = r.Verify()
if err != nil {
return nil, err
}
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-36
View File
@@ -1,36 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/klauspost/compress/zstd"
)
//Zstd is a zstd compressor/decompressor
type Zstd struct {
CompressionLevel int32
}
//NewZstdCompressorWithOptions creates a new Zstd with options read from the given reader
func NewZstdCompressorWithOptions(r io.Reader) (*Zstd, error) {
var zstd Zstd
err := binary.Read(r, binary.LittleEndian, &zstd)
if err != nil {
return nil, err
}
return &zstd, nil
}
//Decompress decompresses all data from the reader and returns the uncompressed data
func (z *Zstd) Decompress(r io.Reader) ([]byte, error) {
rdr, err := zstd.NewReader(r)
if err != nil {
return nil, err
}
defer rdr.Close()
var buf bytes.Buffer
_, err = io.Copy(&buf, rdr)
return buf.Bytes(), err
}
+145
View File
@@ -0,0 +1,145 @@
package data
import (
"io"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/toreader"
)
type FullReader struct {
r io.ReaderAt
d decompress.Decompressor
fragRdr func() (io.Reader, error)
sizes []uint32
blockSize uint32
start uint64
}
func NewFullReader(r io.ReaderAt, start uint64, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) *FullReader {
return &FullReader{
r: r,
start: start,
blockSize: blockSize,
sizes: blockSizes,
d: d,
}
}
func (r *FullReader) AddFragment(rdr func() (io.Reader, error)) {
r.fragRdr = rdr
r.sizes = append(r.sizes, 0)
}
type outDat struct {
err error
data []byte
i int
}
func (r FullReader) process(index int, offset int64, out chan outDat) {
var err error
var dat []byte
var rdr io.ReadCloser
size := realSize(r.sizes[index])
if size == 0 {
out <- outDat{
i: index,
err: nil,
data: make([]byte, r.blockSize),
}
return
}
// rdr := io.LimitReader(toreader.NewReader(r.r, offset), int64(size))
if size == r.sizes[index] {
//Special workaround for zstd for increased performancce.
if zstd, ok := r.d.(*decompress.Zstd); ok {
dat = make([]byte, size)
_, err = r.r.ReadAt(dat, offset)
if err == nil {
dat, err = zstd.Decode(dat)
}
} else {
rdr, err = r.d.Reader(io.LimitReader(toreader.NewReader(r.r, offset), int64(size)))
if err == nil {
dat, err = io.ReadAll(rdr)
}
}
} else {
dat = make([]byte, size)
_, err = r.r.ReadAt(dat, offset)
}
out <- outDat{
i: index,
err: err,
data: dat,
}
if clr, ok := rdr.(io.Closer); ok {
clr.Close()
}
}
func (r FullReader) WriteTo(w io.Writer) (n int64, err error) {
out := make(chan outDat, len(r.sizes))
offset := r.start
num := len(r.sizes)
for i := 0; i < num; i++ {
if i == num-1 && r.fragRdr != nil {
go func() {
rdr, e := r.fragRdr()
if err != nil {
out <- outDat{
i: num - 1,
err: e,
}
return
}
dat, e := io.ReadAll(rdr)
out <- outDat{
i: num - 1,
err: e,
data: dat,
}
if clr, ok := rdr.(io.Closer); ok {
clr.Close()
}
}()
continue
}
go r.process(i, int64(offset), out)
offset += uint64(realSize(r.sizes[i]))
}
cache := make(map[int]outDat)
var tmpN int
for cur := 0; cur < num; {
dat := <-out
if dat.err != nil {
err = dat.err
return
}
if dat.i != cur {
cache[dat.i] = dat
continue
}
tmpN, err = w.Write(dat.data)
n += int64(tmpN)
if err != nil {
return
}
cur++
var ok bool
for {
dat, ok = cache[cur]
if !ok {
break
}
tmpN, err = w.Write(dat.data)
n += int64(tmpN)
if err != nil {
return
}
cur++
}
}
return
}
+98
View File
@@ -0,0 +1,98 @@
package data
import (
"bytes"
"io"
"github.com/CalebQ42/squashfs/internal/decompress"
)
type Reader struct {
master io.Reader
cur io.Reader
fragRdr io.Reader
d decompress.Decompressor
comRdr io.Reader
blockSizes []uint32
blockSize uint32
resetable bool
}
func NewReader(r io.Reader, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) *Reader {
return &Reader{
d: d,
master: r,
blockSizes: blockSizes,
blockSize: blockSize,
resetable: true,
}
}
func (r *Reader) AddFragment(rdr io.Reader) {
r.fragRdr = rdr
r.blockSizes = append(r.blockSizes, 0)
}
func realSize(siz uint32) uint32 {
return siz &^ (1 << 24)
}
func (r *Reader) advance() (err error) {
if clr, ok := r.cur.(io.Closer); ok {
clr.Close()
}
if len(r.blockSizes) == 0 {
return io.EOF
}
if len(r.blockSizes) == 1 && r.fragRdr != nil {
r.cur = r.fragRdr
} else {
size := realSize(r.blockSizes[0])
if size == 0 {
r.cur = bytes.NewReader(make([]byte, r.blockSize))
} else {
r.cur = io.LimitReader(r.master, int64(size))
if size == r.blockSizes[0] {
if r.d.Resetable() {
if r.comRdr == nil {
r.cur, err = r.d.Reader(r.cur)
if err != nil {
return
}
} else {
err = r.d.Reset(r.comRdr, r.cur)
r.cur = r.comRdr
}
} else {
r.cur, err = r.d.Reader(r.cur)
}
}
}
}
r.blockSizes = r.blockSizes[1:]
return
}
func (r *Reader) Read(p []byte) (n int, err error) {
if r.cur == nil {
err = r.advance()
if err != nil {
return
}
}
n, err = r.cur.Read(p)
if err == io.EOF {
err = r.advance()
if err != nil {
return
}
var tmpN int
tmp := make([]byte, len(p)-n)
tmpN, err = r.Read(tmp)
for i := range tmp {
p[n+i] = tmp[i]
}
n += tmpN
}
return
}
+19
View File
@@ -0,0 +1,19 @@
package decompress
import (
"io"
"github.com/klauspost/compress/zlib"
)
type GZip struct{}
func (g GZip) Reader(src io.Reader) (io.ReadCloser, error) {
return zlib.NewReader(src)
}
func (g GZip) Resetable() bool { return true }
func (g GZip) Reset(old, src io.Reader) error {
return old.(zlib.Resetter).Reset(src, nil)
}
+19
View File
@@ -0,0 +1,19 @@
package decompress
import (
"errors"
"io"
)
var ErrNotResetable = errors.New("decompressor not resetable")
type Decompressor interface {
//Creates a new decompressor reading from src.
Reader(src io.Reader) (io.ReadCloser, error)
//Reports whether Reset will work or not.
Resetable() bool
//Reset attempts to re-use an old decompressor with new data.
//Will return ErrNotResetable if not Resetable().
//Must ALWAYS be provided with a reader created with Reader.
Reset(old, src io.Reader) error
}
+20
View File
@@ -0,0 +1,20 @@
package decompress
import (
"io"
"github.com/pierrec/lz4/v4"
)
type Lz4 struct{}
func (l Lz4) Reader(r io.Reader) (io.ReadCloser, error) {
return io.NopCloser(lz4.NewReader(r)), nil
}
func (l Lz4) Resetable() bool { return true }
func (l Lz4) Reset(old, src io.Reader) error {
old.(*lz4.Reader).Reset(src)
return nil
}
+18
View File
@@ -0,0 +1,18 @@
package decompress
import (
"io"
"github.com/ulikunitz/xz/lzma"
)
type Lzma struct{}
func (l Lzma) Reader(r io.Reader) (io.ReadCloser, error) {
rdr, err := lzma.NewReader(r)
return io.NopCloser(rdr), err
}
func (l Lzma) Resetable() bool { return false }
func (l Lzma) Reset(old, src io.Reader) error { return ErrNotResetable }
+22
View File
@@ -0,0 +1,22 @@
package decompress
import (
"bytes"
"io"
"github.com/rasky/go-lzo"
)
type Lzo struct{}
func (l Lzo) Reader(r io.Reader) (io.ReadCloser, error) {
cache, err := lzo.Decompress1X(r, 0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(bytes.NewReader(cache)), nil
}
func (l Lzo) Resetable() bool { return false }
func (l Lzo) Reset(old, src io.Reader) error { return ErrNotResetable }
+20
View File
@@ -0,0 +1,20 @@
package decompress
import (
"io"
"github.com/therootcompany/xz"
)
type Xz struct{}
func (x Xz) Reader(r io.Reader) (io.ReadCloser, error) {
rdr, err := xz.NewReader(r, 0)
return io.NopCloser(rdr), err
}
func (x Xz) Resetable() bool { return true }
func (x Xz) Reset(old, src io.Reader) error {
return old.(*xz.Reader).Reset(src)
}
+29
View File
@@ -0,0 +1,29 @@
package decompress
import (
"io"
"github.com/klauspost/compress/zstd"
)
type Zstd struct {
writeToReader *zstd.Decoder
}
func (z Zstd) Reader(src io.Reader) (io.ReadCloser, error) {
r, err := zstd.NewReader(src)
return r.IOReadCloser(), err
}
func (z Zstd) Resetable() bool { return true }
func (z Zstd) Reset(old, src io.Reader) error {
return old.(*zstd.Decoder).Reset(src)
}
func (z *Zstd) Decode(in []byte) (out []byte, err error) {
if z.writeToReader == nil {
z.writeToReader, _ = zstd.NewReader(nil)
}
return z.writeToReader.DecodeAll(in, nil)
}
+47 -63
View File
@@ -6,89 +6,73 @@ import (
"io"
)
//Header is the header for a directory in the directory table
type Header struct {
Count uint32
InodeOffset uint32
InodeNumber uint32
type header struct {
Entries uint32
InodeStart uint32
Num uint32
}
//EntryInit is the values that can be easily decoded
type EntryInit struct {
Offset uint16
InodeOffset int16
Type uint16
NameSize uint16
type entryInit struct {
Offset uint16
NumOffset int16
Type uint16
NameSize uint16
}
type entry struct {
entryInit
Name []byte
}
//Entry is an entry in a directory.
type Entry struct {
Init EntryInit
Name string
Header *Header
Name string
BlockStart uint32
Type uint16
Offset uint16
}
//NewEntry creates a new directory entry
func NewEntry(rdr io.Reader) (Entry, error) {
var entry Entry
err := binary.Read(rdr, binary.LittleEndian, &entry.Init)
func readEntry(r io.Reader) (e entry, err error) {
err = binary.Read(r, binary.LittleEndian, &e.entryInit)
if err != nil {
return Entry{}, err
return
}
tmp := make([]byte, entry.Init.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return Entry{}, err
}
entry.Name = string(tmp)
return entry, err
e.Name = make([]byte, e.NameSize+1)
err = binary.Read(r, binary.LittleEndian, &e.Name)
return
}
//Directory is an entry in the directory table of a squashfs.
//Will only have multiple headers if there are more then 256 entries
type Directory struct {
Headers []Header
Entries []Entry
}
//NewDirectory reads the directory from rdr
func NewDirectory(base io.Reader, size uint32) (*Directory, error) {
var dir Directory
var err error
tmp := make([]byte, size)
base.Read(tmp)
rdr := bytes.NewBuffer(tmp)
func ReadEntries(rdr io.Reader, size uint32) (e []Entry, err error) {
dat := make([]byte, size-3)
rdr.Read(dat)
r := bytes.NewReader(dat)
var h header
var en entry
for {
var hdr Header
err = binary.Read(rdr, binary.LittleEndian, &hdr)
if err == io.ErrUnexpectedEOF {
break
err = binary.Read(r, binary.LittleEndian, &h)
if err == io.EOF {
err = nil
return
} else if err != nil {
return nil, err
return
}
hdr.Count++
headers := hdr.Count / 256
if hdr.Count%256 > 0 {
headers++
}
dir.Headers = append(dir.Headers, hdr)
for i := uint32(0); i < hdr.Count; i++ {
h.Entries++
for i := 0; i < int(h.Entries); i++ {
if i != 0 && i%256 == 0 {
var newHdr Header
err = binary.Read(rdr, binary.LittleEndian, &newHdr)
err = binary.Read(r, binary.LittleEndian, &h)
if err != nil {
return nil, err
return
}
dir.Headers = append(dir.Headers, newHdr)
}
var ent Entry
ent, err = NewEntry(rdr)
en, err = readEntry(r)
if err != nil {
return nil, err
return
}
ent.Header = &dir.Headers[len(dir.Headers)-1]
dir.Entries = append(dir.Entries, ent)
e = append(e, Entry{
Name: string(en.Name),
BlockStart: h.InodeStart,
Type: en.Type,
Offset: en.Offset,
})
}
}
return &dir, nil
}
+65
View File
@@ -0,0 +1,65 @@
package 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
}
+62
View File
@@ -0,0 +1,62 @@
package 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
}
+79
View File
@@ -0,0 +1,79 @@
package inode
import (
"encoding/binary"
"errors"
"io"
"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
}
-255
View File
@@ -1,255 +0,0 @@
package inode
import (
"encoding/binary"
"io"
)
const (
BasicDirectoryType = iota + 1
BasicFileType
BasicSymlinkType
BasicBlockDeviceType
BasicCharDeviceType
BasicFifoType
BasicSocketType
ExtDirType
ExtFileType
ExtSymlinkType
ExtBlockDeviceType
ExtCharDeviceType
ExtFifoType
ExtSocketType
)
//Header is the common header for all inodes
type Header struct {
InodeType uint16
Permissions uint16
UID uint16
GID uint16
ModifiedTime uint32
Number uint32
}
//BasicDirectory is self explainatory
type BasicDirectory struct {
DirectoryIndex uint32
HardLinks uint32
DirectorySize uint16
DirectoryOffset uint16
ParentInodeNumber uint32
}
//ExtendedDirectoryInit is the information that can be directoy decoded
type ExtendedDirectoryInit struct {
HardLinks uint32
DirectorySize uint32
DirectoryIndex uint32
ParentInodeNumber uint32
IndexCount uint16 //one less then directory indexes following structure
DirectoryOffset uint16
XattrIndex uint32
}
//ExtendedDirectory is a directory with extra info
type ExtendedDirectory struct {
Init ExtendedDirectoryInit
Indexes []DirectoryIndex
}
//NewExtendedDirectory creates a new ExtendedDirectory
func NewExtendedDirectory(rdr io.Reader) (ExtendedDirectory, error) {
var inode ExtendedDirectory
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
for i := uint16(0); i < inode.Init.IndexCount; i++ {
tmp, err := NewDirectoryIndex(rdr)
if err != nil {
return inode, err
}
inode.Indexes = append(inode.Indexes, tmp)
}
return inode, err
}
//DirectoryIndexInit holds the values that can be easily decoded
type DirectoryIndexInit struct {
Offset uint32
DirTableOffset uint32
NameSize uint32
}
//DirectoryIndex is a quick lookup provided by an ExtendedDirectory
type DirectoryIndex struct {
Init DirectoryIndexInit
Name string
}
//NewDirectoryIndex return a new DirectoryIndex
func NewDirectoryIndex(rdr io.Reader) (DirectoryIndex, error) {
var index DirectoryIndex
err := binary.Read(rdr, binary.LittleEndian, &index.Init)
if err != nil {
return index, err
}
tmp := make([]byte, index.Init.NameSize+1, index.Init.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return index, err
}
index.Name = string(tmp)
return index, nil
}
//BasicFileInit is the information that can be directoy decoded
type BasicFileInit struct {
BlockStart uint32
FragmentIndex uint32
FragmentOffset uint32
Size uint32
}
//BasicFile is self explainatory
type BasicFile struct {
Init BasicFileInit
BlockSizes []uint32
Fragmented bool
}
//NewBasicFile creates a new BasicFile
func NewBasicFile(rdr io.Reader, blockSize uint32) (BasicFile, error) {
var inode BasicFile
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.Fragmented = inode.Init.FragmentIndex != 0xFFFFFFFF
blocks := inode.Init.Size / blockSize
if inode.Init.Size%blockSize > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//ExtendedFileInit is the information that can be directly decoded
type ExtendedFileInit struct {
BlockStart uint32
Size uint32
Sparse uint64
HardLinks uint32
FragmentIndex uint32
FragmentOffset uint32
XattrIndex uint32
}
//ExtendedFile is a file with more information
type ExtendedFile struct {
Init ExtendedFileInit
BlockSizes []uint32
Fragmented bool
}
//NewExtendedFile creates a new ExtendedFile
func NewExtendedFile(rdr io.Reader, blockSize uint32) (ExtendedFile, error) {
var inode ExtendedFile
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.Fragmented = inode.Init.FragmentIndex != 0xFFFFFFFF
blocks := inode.Init.Size / blockSize
if inode.Init.Size%blockSize > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//BasicSymlinkInit is all the values that can be directly decoded
type BasicSymlinkInit struct {
HardLinks uint32
TargetPathSize uint32
}
//BasicSymlink is a symlink
type BasicSymlink struct {
Init BasicSymlinkInit
targetPath []byte //len is TargetPathSize
Path string
}
//NewBasicSymlink creates a new BasicSymlink
func NewBasicSymlink(rdr io.Reader) (BasicSymlink, error) {
var inode BasicSymlink
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.targetPath = make([]byte, inode.Init.TargetPathSize, inode.Init.TargetPathSize)
err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath)
if err != nil {
return inode, err
}
inode.Path = string(inode.targetPath)
return inode, err
}
//ExtendedSymlinkInit is all the values that can be directly decoded
type ExtendedSymlinkInit struct {
HardLinks uint32
TargetPathSize uint32
}
//ExtendedSymlink is a symlink with extra information
type ExtendedSymlink struct {
Init ExtendedSymlinkInit
targetPath []uint8
Path string
XattrIndex uint32
}
//NewExtendedSymlink creates a new ExtendedSymlink
func NewExtendedSymlink(rdr io.Reader) (ExtendedSymlink, error) {
var inode ExtendedSymlink
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.targetPath = make([]uint8, inode.Init.TargetPathSize, inode.Init.TargetPathSize)
err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath)
if err != nil {
return inode, err
}
inode.Path = string(inode.targetPath)
err = binary.Read(rdr, binary.LittleEndian, &inode.XattrIndex)
return inode, err
}
//BasicDevice is a device
type BasicDevice struct {
HardLinks uint32
Device uint32
}
//ExtendedDevice is a device with more info
type ExtendedDevice struct {
BasicDevice
XattrIndex uint32
}
//BasicIPC is a Fifo or Socket device
type BasicIPC struct {
HardLink uint32
}
//ExtendedIPC is a IPC device with extra info
type ExtendedIPC struct {
BasicIPC
XattrIndex uint32
}
+45
View File
@@ -0,0 +1,45 @@
package 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
}
-125
View File
@@ -1,125 +0,0 @@
package inode
import (
"encoding/binary"
"io"
)
//Inode holds an inode. Header is the header that's common for all inodes.
//
//Info holds the actual Inode. Due to each inode type being a different type, it's store as an interface{}
type Inode struct {
Header Header
Type int //Type the inode type defined in the header. Here so it's easy to access
Info interface{} //Info is the parsed specific data. It's type is defined by Type.
}
//ProcessInode tries to read an inode from the BlockReader
func ProcessInode(br io.Reader, blockSize uint32) (*Inode, error) {
var head Header
err := binary.Read(br, binary.LittleEndian, &head)
if err != nil {
return nil, err
}
var info interface{}
switch head.InodeType {
case BasicDirectoryType:
var inode BasicDirectory
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicFileType:
inode, err := NewBasicFile(br, blockSize)
if err != nil {
return nil, err
}
info = inode
case BasicSymlinkType:
inode, err := NewBasicSymlink(br)
if err != nil {
return nil, err
}
info = inode
case BasicBlockDeviceType:
var inode BasicDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicCharDeviceType:
var inode BasicDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicFifoType:
var inode BasicIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicSocketType:
var inode BasicIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtDirType:
inode, err := NewExtendedDirectory(br)
if err != nil {
return nil, err
}
info = inode
case ExtFileType:
inode, err := NewExtendedFile(br, blockSize)
if err != nil {
return nil, err
}
info = inode
case ExtSymlinkType:
inode, err := NewExtendedSymlink(br)
if err != nil {
return nil, err
}
info = inode
case ExtBlockDeviceType:
var inode ExtendedDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtCharDeviceType:
var inode ExtendedDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtFifoType:
var inode ExtendedIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtSocketType:
var inode ExtendedIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
}
return &Inode{
Type: int(head.InodeType),
Header: head,
Info: info,
}, nil
}
+46
View File
@@ -0,0 +1,46 @@
package 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
}
+81
View File
@@ -0,0 +1,81 @@
package metadata
import (
"encoding/binary"
"io"
"github.com/CalebQ42/squashfs/internal/decompress"
)
type Reader struct {
master io.Reader
cur io.Reader
d decompress.Decompressor
comRdr io.Reader
}
func NewReader(master io.Reader, d decompress.Decompressor) *Reader {
return &Reader{
master: master,
d: d,
}
}
func realSize(siz uint16) uint16 {
return siz &^ 0x8000
}
func (r *Reader) advance() (err error) {
if !r.d.Resetable() {
if clr, ok := r.cur.(io.Closer); ok {
clr.Close()
}
}
var raw uint16
err = binary.Read(r.master, binary.LittleEndian, &raw)
if err != nil {
return
}
size := realSize(raw)
r.cur = io.LimitReader(r.master, int64(size))
if size == raw {
if r.d.Resetable() {
if r.comRdr == nil {
r.cur, err = r.d.Reader(r.cur)
if err != nil {
return
}
} else {
err = r.d.Reset(r.comRdr, r.cur)
r.cur = r.comRdr
}
} else {
r.cur, err = r.d.Reader(r.cur)
}
}
return
}
func (r *Reader) Read(p []byte) (n int, err error) {
if r.cur == nil {
err = r.advance()
if err != nil {
return
}
}
n, err = r.cur.Read(p)
if err == io.EOF {
err = r.advance()
if err != nil {
return
}
var tmpN int
tmp := make([]byte, len(p)-n)
tmpN, err = r.Read(tmp)
for i := 0; i < tmpN; i++ {
p[n+i] = tmp[i]
}
n += tmpN
}
return
}
+25
View File
@@ -0,0 +1,25 @@
package toreader
import "io"
type Reader struct {
r io.ReaderAt
off int64
}
func NewReader(r io.ReaderAt, start int64) *Reader {
return &Reader{
r: r,
off: start,
}
}
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.r.ReadAt(p, r.off)
r.off += int64(n)
return
}
func (r Reader) Offset() int64 {
return r.off
}
+23
View File
@@ -0,0 +1,23 @@
package toreader
import "io"
type ReaderAt struct {
d []byte
}
func NewReaderAt(r io.Reader) (ra ReaderAt, err error) {
ra.d, err = io.ReadAll(r)
return
}
func (r ReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
if int(off) >= len(r.d) {
return 0, io.EOF
}
n = copy(p, r.d[off:])
if n != len(p) {
err = io.EOF
}
return
}
-188
View File
@@ -1,188 +0,0 @@
package squashfs
import (
"bytes"
"encoding/binary"
"errors"
"io"
)
type metadata struct {
raw uint16
size uint16
compressed bool
}
//MetadataReader is a block reader for metadata. It will automatically read the next block, when it reaches the end of a block.
type metadataReader struct {
s *Reader
offset int64
headers []*metadata
data []byte
readOffset int
}
//NewMetadataReader creates a new MetadataReader, beginning to read at the given offset. It will automatically cache the first block at the location.
func (s *Reader) newMetadataReader(offset int64) (*metadataReader, error) {
var br metadataReader
br.s = s
br.offset = offset
err := br.parseMetadata()
if err != nil {
return nil, err
}
err = br.readNextDataBlock()
if err != nil {
return nil, err
}
return &br, nil
}
//NewMetadataReaderFromInodeRef creates a new MetadataReader with the offsets set by the given inode reference.
func (s *Reader) newMetadataReaderFromInodeRef(ref uint64) (*metadataReader, error) {
offset, metaOffset := processInodeRef(ref)
br, err := s.newMetadataReader(int64(s.super.InodeTableStart + offset))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(metaOffset), io.SeekStart)
if err != nil {
return nil, err
}
return br, nil
}
//ProcessInodeRef processes an inode reference and returns two values
//
//The first value is the inode table offset. AKA, it's where the metadata block of the inode STARTS relative to the inode table.
//
//The second value is the offset of the inode, INSIDE of the metadata.
func processInodeRef(inodeRef uint64) (tableOffset uint64, metaOffset uint64) {
tableOffset = inodeRef >> 16
metaOffset = inodeRef &^ 0xFFFFFFFF0000
return
}
func (br *metadataReader) parseMetadata() error {
var raw uint16
err := binary.Read(io.NewSectionReader(br.s.r, br.offset, 2), binary.LittleEndian, &raw)
if err != nil {
return err
}
br.offset += 2
compressed := raw&0x8000 != 0x8000
size := raw &^ 0x8000
br.headers = append(br.headers, &metadata{
raw: raw,
size: size,
compressed: compressed,
})
return nil
}
func (br *metadataReader) readNextDataBlock() error {
meta := br.headers[len(br.headers)-1]
r := io.NewSectionReader(br.s.r, br.offset, int64(meta.size))
if meta.compressed {
byts, err := br.s.decompressor.Decompress(r)
if err != nil {
return err
}
br.offset += int64(meta.size)
br.data = append(br.data, byts...)
return nil
}
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return err
}
br.offset += int64(meta.size)
br.data = append(br.data, buf.Bytes()...)
return nil
}
//Read reads bytes into the given byte slice. Returns the amount of data read.
func (br *metadataReader) Read(p []byte) (int, error) {
if br.readOffset+len(p) <= len(br.data) {
for i := 0; i < len(p); i++ {
p[i] = br.data[br.readOffset+i]
}
br.readOffset += len(p)
return len(p), nil
}
read := 0
for read < len(p) {
err := br.parseMetadata()
if err != nil {
br.readOffset += read
return read, err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset += read
return read, err
}
for ; read < len(p); read++ {
if br.readOffset+read < len(br.data) {
p[read] = br.data[br.readOffset+read]
} else {
break
}
}
}
br.readOffset += read
if read != len(p) {
return read, errors.New("Didn't read enough data")
}
return read, nil
}
//Seek will seek to the specified location (if possible). Seeking is relative to the uncompressed data.
//When io.SeekCurrent or io.SeekStart is set, if seeking would put the offset beyond the current cached data, it will try to read the next data blocks to accomodate. On a failure it will seek to the end of the data.
//When io.SeekEnd is set, it wil seek reletive to the currently cached data.
func (br *metadataReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekCurrent:
br.readOffset += int(offset)
for {
if br.readOffset < len(br.data) {
break
}
err := br.parseMetadata()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
}
case io.SeekStart:
br.readOffset = int(offset)
for {
if br.readOffset < len(br.data) {
break
}
err := br.parseMetadata()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
}
case io.SeekEnd:
br.readOffset = len(br.data) - int(offset)
if br.readOffset < 0 {
br.readOffset = 0
return int64(br.readOffset), errors.New("Trying to seek to a negative value")
}
}
return int64(br.readOffset), nil
}
+183 -201
View File
@@ -5,238 +5,220 @@ import (
"errors"
"io"
"math"
"time"
"github.com/CalebQ42/squashfs/internal/compression"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
)
const (
magic = 0x73717368
)
type Reader struct {
*FS
d decompress.Decompressor
r io.ReaderAt
fragEntries []fragEntry
ids []uint32
// exportTable []uint64
s superblock
}
var (
//ErrNoMagic is returned if the magic number in the superblock isn't correct.
errNoMagic = errors.New("Magic number doesn't match. Either isn't a squashfs or corrupted")
//ErrIncompatibleCompression is returned if the compression type in the superblock doesn't work.
errIncompatibleCompression = errors.New("Compression type unsupported")
//ErrCompressorOptions is returned if compressor options is present. It's not currently supported.
errCompressorOptions = errors.New("Compressor options is not currently supported")
//ErrOptions is returned when compression options that I haven't tested is set. When this is returned, the Reader is also returned.
ErrOptions = errors.New("Possibly incompatible compressor options")
ErrorMagic = errors.New("magic incorrect. probably not reading squashfs archive")
ErrorLog = errors.New("block log is incorrect. possible corrupted archive")
ErrorVersion = errors.New("squashfs version of archive is not 4.0")
)
//Reader processes and reads a squashfs archive.
type Reader struct {
r io.ReaderAt
super superblock
flags superblockFlags
decompressor compression.Decompressor
fragOffsets []uint64
idTable []uint32
}
// The types of compression supported by squashfs
const (
GZipCompression = uint16(iota + 1)
LZMACompression
LZOCompression
XZCompression
LZ4Compression
ZSTDCompression
)
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
hasUnsupportedOptions := false
var rdr Reader
rdr.r = r
err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super)
// Creates a new squashfs.Reader from the given io.Reader. NOTE: All data from the io.Reader will be read and stored in memory.
func NewReaderFromReader(r io.Reader) (*Reader, error) {
rdr, err := toreader.NewReaderAt(r)
if err != nil {
return nil, err
}
if rdr.super.Magic != magic {
return nil, errNoMagic
return NewReader(rdr)
}
// Creates a new squashfs.Reader from the given io.ReaderAt.
func NewReader(r io.ReaderAt) (*Reader, error) {
var squash Reader
squash.r = r
err := binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &squash.s)
if err != nil {
return nil, err
}
// if rdr.super.BlockLog == uint16(math.Log2(float64(rdr.super.BlockSize))) {
// return nil, errors.New("BlockSize and BlockLog doesn't match. The archive is probably corrupt")
// }
rdr.flags = rdr.super.GetFlags()
if rdr.flags.CompressorOptions {
switch rdr.super.CompressionType {
case gzipCompression:
gzip, err := compression.NewGzipCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if gzip.HasCustomWindow || gzip.HasStrategies {
hasUnsupportedOptions = true
}
rdr.decompressor = gzip
case xzCompression:
xz, err := compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if xz.HasFilters {
return nil, errors.New("XZ compression options has filters. These are not yet supported")
}
rdr.decompressor = xz
case lz4Compression:
lz4, err := compression.NewLz4CompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if lz4.HC {
hasUnsupportedOptions = true
}
rdr.decompressor = lz4
case zstdCompression:
zstd, err := compression.NewZstdCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 4))
if err != nil {
return nil, err
}
rdr.decompressor = zstd
default:
return nil, errCompressorOptions
}
} else {
switch rdr.super.CompressionType {
case gzipCompression:
rdr.decompressor = &compression.Gzip{}
case lzmaCompression:
rdr.decompressor = &compression.Lzma{}
case xzCompression:
rdr.decompressor = &compression.Xz{}
case lz4Compression:
rdr.decompressor = &compression.Lz4{}
case zstdCompression:
rdr.decompressor = &compression.Zstd{}
default:
//TODO: all compression types.
return nil, errIncompatibleCompression
}
if !squash.s.checkMagic() {
return nil, ErrorMagic
}
fragBlocks := int(math.Ceil(float64(rdr.super.FragCount) / 512))
if fragBlocks > 0 {
offset := int64(rdr.super.FragTableStart)
for i := 0; i < fragBlocks; i++ {
tmp := make([]byte, 8)
_, err = r.ReadAt(tmp, offset)
if err != nil {
return nil, err
}
rdr.fragOffsets = append(rdr.fragOffsets, binary.LittleEndian.Uint64(tmp))
offset += 8
}
if !squash.s.checkBlockLog() {
return nil, ErrorLog
}
unread := rdr.super.IDCount
blockOffsets := make([]uint64, int(math.Ceil(float64(rdr.super.IDCount)/2048)))
for i := range blockOffsets {
secRdr := io.NewSectionReader(r, int64(rdr.super.IDTableStart)+(8*int64(i)), 8)
err = binary.Read(secRdr, binary.LittleEndian, &blockOffsets[i])
if !squash.s.checkVersion() {
return nil, ErrorVersion
}
switch squash.s.CompType {
case GZipCompression:
squash.d = decompress.GZip{}
case LZMACompression:
squash.d = decompress.Lzma{}
case LZOCompression:
squash.d = decompress.Lzo{}
case XZCompression:
squash.d = decompress.Xz{}
case LZ4Compression:
squash.d = decompress.Lz4{}
case ZSTDCompression:
squash.d = &decompress.Zstd{}
default:
return nil, errors.New("uh, I need to do this, OR something if very wrong")
}
if !squash.s.noFragments() && squash.s.FragCount > 0 {
fragOffsets := make([]uint64, int(math.Ceil(float64(squash.s.FragCount)/512)))
err = binary.Read(toreader.NewReader(r, int64(squash.s.FragTableStart)), binary.LittleEndian, &fragOffsets)
if err != nil {
return nil, err
}
idRdr, err := rdr.newMetadataReader(int64(blockOffsets[i]))
if err != nil {
return nil, err
}
read := uint16(math.Min(float64(unread), 2048))
for i := uint16(0); i < read; i++ {
var tmp uint32
err = binary.Read(idRdr, binary.LittleEndian, &tmp)
squash.fragEntries = make([]fragEntry, squash.s.FragCount)
if len(fragOffsets) == 1 {
rdr := metadata.NewReader(toreader.NewReader(r, int64(fragOffsets[0])), squash.d)
err = binary.Read(rdr, binary.LittleEndian, &squash.fragEntries)
if err != nil {
return nil, err
}
rdr.idTable = append(rdr.idTable, tmp)
}
unread -= read
}
if hasUnsupportedOptions {
return &rdr, ErrOptions
}
return &rdr, nil
}
//GetRootFolder returns a squashfs.File that references the root directory of the squashfs archive.
func (r *Reader) GetRootFolder() (root *File, err error) {
root = new(File)
mr, err := r.newMetadataReaderFromInodeRef(r.super.RootInodeRef)
if err != nil {
return nil, err
}
root.in, err = inode.ProcessInode(mr, r.super.BlockSize)
if err != nil {
return nil, err
}
root.path = "/"
root.filType = root.in.Type
root.r = r
return root, nil
}
//GetAllFiles returns a slice of ALL files and folders contained in the squashfs.
func (r *Reader) GetAllFiles() (fils []*File, err error) {
root, err := r.GetRootFolder()
if err != nil {
return nil, err
}
return root.GetChildrenRecursively()
}
//FindFile returns the first file (in the same order as Reader.GetAllFiles) that the given function returns true for. Returns nil if nothing is found.
func (r *Reader) FindFile(query func(*File) bool) *File {
root, err := r.GetRootFolder()
if err != nil {
return nil
}
fils, err := root.GetChildren()
if err != nil {
return nil
}
var childrenDirs []*File
for _, fil := range fils {
if query(fil) {
return fil
}
if fil.IsDir() {
childrenDirs = append(childrenDirs, fil)
} else {
toRead := squash.s.FragCount
var curRead uint32
var tmp []fragEntry
var rdr *metadata.Reader
var offset int
for i := range fragOffsets {
curRead = uint32(math.Min(512, float64(toRead)))
tmp = make([]fragEntry, curRead)
rdr = metadata.NewReader(toreader.NewReader(r, int64(fragOffsets[i])), squash.d)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
offset = int(squash.s.FragCount - toRead)
for i := range tmp {
squash.fragEntries[offset+i] = tmp[i]
}
toRead -= curRead
}
}
}
for len(childrenDirs) != 0 {
var tmp []*File
for _, dirs := range childrenDirs {
chil, err := dirs.GetChildren()
if squash.s.IdCount > 0 {
idOffsets := make([]uint64, int(math.Ceil(float64(squash.s.IdCount)/2048)))
err = binary.Read(toreader.NewReader(r, int64(squash.s.IdTableStart)), binary.LittleEndian, &idOffsets)
if err != nil {
return nil, err
}
squash.ids = make([]uint32, squash.s.IdCount)
if len(idOffsets) == 1 {
rdr := metadata.NewReader(toreader.NewReader(r, int64(idOffsets[0])), squash.d)
err = binary.Read(rdr, binary.LittleEndian, &squash.ids)
if err != nil {
return nil
return nil, err
}
for _, child := range chil {
if query(child) {
return child
} else {
toRead := squash.s.IdCount
var curRead uint16
var tmp []uint32
var rdr *metadata.Reader
var offset int
for i := range idOffsets {
curRead = uint16(math.Min(2048, float64(toRead)))
tmp = make([]uint32, curRead)
rdr = metadata.NewReader(toreader.NewReader(r, int64(idOffsets[i])), squash.d)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
if child.IsDir() {
tmp = append(tmp, child)
offset = int(squash.s.IdCount - toRead)
for i := range tmp {
squash.ids[offset+i] = tmp[i]
}
toRead -= curRead
}
}
childrenDirs = tmp
}
return nil
root, err := squash.inodeFromRef(squash.s.RootInodeRef)
if err != nil {
return nil, err
}
rootEnts, err := squash.readDirectory(root)
if err != nil {
return nil, err
}
enType := root.Type
if enType == inode.EDir {
enType = inode.Dir
}
squash.FS = &FS{
e: rootEnts,
File: &File{
rdr: &squash,
i: root,
e: directory.Entry{
Name: "",
Type: enType,
},
r: &squash,
},
}
return &squash, nil
}
//FindAll returns all files where the given function returns true.
func (r *Reader) FindAll(query func(*File) bool) (all []*File) {
root, err := r.GetRootFolder()
if err != nil {
return nil
}
fils, err := root.GetChildrenRecursively()
if err != nil {
return nil
}
for _, fil := range fils {
if query(fil) {
all = append(all, fil)
}
}
return
}
// func (r *Reader) initExport() (err error) {
// num := int(math.Ceil(float64(r.s.InodeCount) / 1024))
// offsets := make([]uint64, num)
// err = binary.Read(toreader.NewReader(r.r, int64(r.s.ExportTableStart)), binary.LittleEndian, &offsets)
// if err != nil {
// return
// }
// left := r.s.InodeCount
// var toRead uint32
// var new []uint64
// var rdr *metadata.Reader
// for i := range offsets {
// rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offsets[i])), r.d)
// toRead = uint32(math.Min(1024, float64(left)))
// new = make([]uint64, toRead)
// err = binary.Read(rdr, binary.LittleEndian, &new)
// if err != nil {
// return
// }
// left -= toRead
// r.exportTable = append(r.exportTable, new...)
// }
// return nil
// }
//GetFileAtPath will return the file at the given path. If the file cannot be found, will return nil.
func (r *Reader) GetFileAtPath(path string) *File {
dir, err := r.GetRootFolder()
if err != nil {
return nil
}
return dir.GetFileAtPath(path)
// func (r *Reader) inode(index uint32) (i inode.Inode, err error) {
// if r.s.exportable() {
// if r.exportTable == nil {
// err = r.initExport()
// if err != nil {
// return
// }
// }
// return r.inodeFromRef(r.exportTable[index-1])
// }
// err = errors.New("archive is not exportable")
// return
// }
// Returns the last time the archive was modified.
func (r Reader) ModTime() time.Time {
return time.Unix(int64(r.s.ModTime), 0)
}
+351
View File
@@ -0,0 +1,351 @@
package squashfs
import (
"errors"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
//File represents a file inside a squashfs archive.
type File struct {
i inode.Inode
rdr io.Reader
fullRdr io.WriterTo
r *Reader
parent *FS
e directory.Entry
dirsRead int
}
var (
ErrReadNotFile = errors.New("read called on non-file")
)
func (r Reader) newFile(en directory.Entry, parent *FS) (*File, error) {
i, err := r.inodeFromDir(en)
if err != nil {
return nil, err
}
var rdr io.Reader
var full io.WriterTo
if i.Type == inode.Fil || i.Type == inode.EFil {
full, rdr, err = r.getReaders(i)
if err != nil {
return nil, err
}
}
return &File{
e: en,
i: i,
rdr: rdr,
fullRdr: full,
r: &r,
parent: parent,
}, nil
}
//Stat returns the File's fs.FileInfo
func (f File) Stat() (fs.FileInfo, error) {
return newFileInfo(f.e, f.i), nil
}
//Read reads the data from the file. Only works if file is a normal file.
func (f File) Read(p []byte) (int, error) {
if f.i.Type != inode.Fil && f.i.Type != inode.EFil {
return 0, ErrReadNotFile
}
if f.rdr == nil {
return 0, fs.ErrClosed
}
return f.rdr.Read(p)
}
//WriteTo writes all data from the file to the writer. This is multi-threaded.
//The underlying reader is seperate from the one used with Read and can be reused.
func (f File) WriteTo(w io.Writer) (int64, error) {
return f.fullRdr.WriteTo(w)
}
//Close simply nils the underlying reader. Here mostly to satisfy fs.File
func (f *File) Close() error {
f.rdr = nil
return nil
}
//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) (out []fs.DirEntry, err error) {
if !f.IsDir() {
return nil, errors.New("File is not a directory")
}
ents, err := f.r.readDirectory(f.i)
if err != nil {
return nil, err
}
start, end := 0, len(ents)
if n > 0 {
start, end = f.dirsRead, f.dirsRead+n
if end > len(f.r.e) {
end = len(f.r.e)
err = io.EOF
}
}
var fi fileInfo
for _, e := range ents[start:end] {
fi, err = f.r.newFileInfo(e)
if err != nil {
f.dirsRead += len(out)
return
}
out = append(out, fs.FileInfoToDirEntry(fi))
}
f.dirsRead += len(out)
return
}
//FS returns the File as a FS.
func (f *File) FS() (*FS, error) {
if !f.IsDir() {
return nil, errors.New("File is not a directory")
}
ents, err := f.r.readDirectory(f.i)
if err != nil {
return nil, err
}
return &FS{
File: f,
e: ents,
}, nil
}
//IsDir Yep.
func (f File) IsDir() bool {
return f.i.Type == inode.Dir || f.i.Type == inode.EDir
}
//IsRegular yep.
func (f File) IsRegular() bool {
return f.i.Type == inode.Fil || f.i.Type == inode.EFil
}
//IsSymlink yep.
func (f File) IsSymlink() bool {
return f.i.Type == inode.Sym || f.i.Type == inode.ESym
}
//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.i.Type {
case inode.Sym:
return string(f.i.Data.(inode.Symlink).Target)
case inode.ESym:
return string(f.i.Data.(inode.ESymlink).Target)
}
return ""
}
func (f File) path() string {
if f.parent == nil {
return f.e.Name
}
return f.parent.path() + "/" + f.e.Name
}
//GetSymlinkFile returns the File the symlink is pointing to.
//If not a symlink, or the target is unobtainable (such as it being outside the archive or it's absolute) returns nil
func (f File) GetSymlinkFile() *File {
if !f.IsSymlink() {
return nil
}
if strings.HasPrefix(f.SymlinkPath(), "/") {
return nil
}
sym, err := f.parent.Open(f.SymlinkPath())
if err != nil {
return nil
}
return sym.(*File)
}
//ExtractionOptions are available options on how to extract.
type ExtractionOptions struct {
LogOutput io.Writer //Where error log should write. If nil, uses os.Stdout. Has no effect if verbose is false.
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
FolderPerm fs.FileMode //The permissions used when creating the extraction folder
}
//DefaultOptions is the default ExtractionOptions.
func DefaultOptions() ExtractionOptions {
return ExtractionOptions{
FolderPerm: 0755,
}
}
//ExtractTo extracts the File to the given folder with the default options.
//If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractTo(folder string) error {
return f.ExtractWithOptions(folder, DefaultOptions())
}
//ExtractSymlink extracts the File to the folder with the DereferenceSymlink option.
//If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractSymlink(folder string) error {
return f.ExtractWithOptions(folder, ExtractionOptions{
DereferenceSymlink: true,
FolderPerm: 0755,
})
}
//ExtractWithOptions extracts the File to the given folder with the given ExtrationOptions.
//If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error {
if op.Verbose {
if op.LogOutput == nil {
op.LogOutput = os.Stdout
}
log.SetOutput(op.LogOutput)
}
return f.realExtract(folder, op)
}
func (f File) realExtract(folder string, op ExtractionOptions) error {
err := os.MkdirAll(folder, op.FolderPerm)
if err != nil && !os.IsExist(err) {
if op.Verbose {
log.Println("Error while creating extraction folder")
}
return err
}
folder = filepath.Clean(folder)
if f.IsDir() {
filFS, _ := f.FS()
var ents []directory.Entry
ents, err = f.r.readDirectory(f.i)
if err != nil {
if op.Verbose {
log.Println("Error while reading children of", f.path())
}
return err
}
errChan := make(chan error)
for i := 0; i < len(ents); i++ {
go func(ent directory.Entry) {
fil, goErr := f.r.newFile(ent, filFS)
if goErr != nil {
if op.Verbose {
log.Println("Error while reading info for", filepath.Join(f.path(), ent.Name))
}
errChan <- goErr
return
}
if fil.IsDir() {
info, _ := fil.Stat()
err = os.Mkdir(filepath.Join(folder, fil.e.Name), info.Mode())
if err != nil {
if op.Verbose {
log.Println("Error while creating", filepath.Join(folder, fil.e.Name))
}
errChan <- err
return
}
errChan <- fil.realExtract(filepath.Join(folder, fil.e.Name), op)
} else {
errChan <- fil.realExtract(folder, op)
}
fil.Close()
}(ents[i])
}
for i := 0; i < len(ents); i++ {
err = <-errChan
if err != nil {
return err
}
}
return nil
} else if f.IsRegular() {
var fil *os.File
fil, err = os.Create(folder + "/" + f.e.Name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.e.Name)
fil, err = os.Create(folder + "/" + f.e.Name)
if err != nil {
if op.Verbose {
log.Println("Error while creating", folder+"/"+f.e.Name)
}
return err
}
} else if err != nil {
if op.Verbose {
log.Println("Error while creating", folder+"/"+f.e.Name)
}
return err
}
_, err = io.Copy(fil, f)
if err != nil {
if op.Verbose {
log.Println("Error while copying data to", folder+"/"+f.e.Name)
}
return err
}
return nil
} else if f.IsSymlink() {
symPath := f.SymlinkPath()
if op.DereferenceSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name)
}
return errors.New("cannot get symlink target")
}
fil.e.Name = f.e.Name
err = fil.realExtract(folder, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting the symlink's file:", folder+"/"+f.e.Name)
}
return err
}
return nil
} else if op.UnbreakSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name)
}
return errors.New("cannot get symlink target")
}
extractLoc := filepath.Clean(folder + "/" + filepath.Dir(symPath))
err = fil.realExtract(extractLoc, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting ", folder+"/"+f.e.Name)
}
return err
}
}
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.e.Name)
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name)
}
if err != nil {
if op.Verbose {
log.Println("Error while making symlink:", folder+"/"+f.e.Name)
}
return err
}
return nil
}
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.i.Type)))
}
+66
View File
@@ -0,0 +1,66 @@
package squashfs
import (
"io/fs"
"time"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
type fileInfo struct {
e directory.Entry
size int64
perm uint32
modTime uint32
}
func (r Reader) newFileInfo(e directory.Entry) (fileInfo, error) {
i, err := r.inodeFromDir(e)
if err != nil {
return fileInfo{}, err
}
return newFileInfo(e, i), nil
}
func newFileInfo(e directory.Entry, i inode.Inode) fileInfo {
var size int64
if i.Type == inode.Fil {
size = int64(i.Data.(inode.File).Size)
} else if i.Type == inode.EFil {
size = int64(i.Data.(inode.EFile).Size)
}
return fileInfo{
e: e,
size: size,
perm: uint32(i.Perm),
modTime: i.ModTime,
}
}
func (f fileInfo) Name() string {
return f.e.Name
}
func (f fileInfo) Size() int64 {
return f.size
}
func (f fileInfo) Mode() fs.FileMode {
if f.IsDir() {
return fs.FileMode(f.perm | uint32(fs.ModeDir))
}
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.e.Type == inode.Dir
}
func (f fileInfo) Sys() any {
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package squashfs
import (
"io"
"github.com/CalebQ42/squashfs/internal/toreader"
)
type fragEntry struct {
Start uint64
Size uint32
_ uint32
}
func (r Reader) fragReader(index uint32) (io.Reader, error) {
realSize := r.fragEntries[index].Size &^ (1 << 24)
rdr := io.LimitReader(toreader.NewReader(r.r, int64(r.fragEntries[index].Start)), int64(realSize))
if realSize != r.fragEntries[index].Size {
return rdr, nil
}
return r.d.Reader(rdr)
}
+375
View File
@@ -0,0 +1,375 @@
package squashfs
import (
"bytes"
"io"
"io/fs"
"path"
"path/filepath"
"strings"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
//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 {
*File
e []directory.Entry
}
func (r Reader) newFS(e directory.Entry, parent *FS) (*FS, error) {
i, err := r.inodeFromDir(e)
if err != nil {
return nil, err
}
ents, err := r.readDirectory(i)
if err != nil {
return nil, err
}
return &FS{
File: &File{
i: i,
r: &r,
parent: parent,
e: e,
},
e: ents,
}, nil
}
//Open opens the file at name. Returns a squashfs.File.
func (f FS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(name)
if name == "." || name == "" {
return f.File, nil
}
split := strings.Split(name, "/")
for i := range f.e {
if f.e[i].Name != split[0] {
continue
}
if len(split) > 1 && f.e[i].Type != inode.Dir {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
if len(split) > 1 {
newFS, err := f.r.newFS(f.e[i], &f)
if err != nil {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: err,
}
}
out, err := newFS.Open(strings.Join(split[1:], "/"))
if err != nil {
err.(*fs.PathError).Path = name
}
return out, err
}
out, err := f.r.newFile(f.e[i], &f)
if err != nil {
err = &fs.PathError{
Op: "open",
Path: name,
Err: err,
}
}
return out, err
}
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
//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) {
if !fs.ValidPath(pattern) {
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: fs.ErrInvalid,
}
}
pattern = filepath.Clean(pattern)
split := strings.Split(pattern, "/")
for i := 0; i < len(f.e); i++ {
if match, _ := path.Match(split[0], f.e[i].Name); match {
if len(split) == 1 {
out = append(out, f.e[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 := 0; i < len(subGlob); i++ {
subGlob[i] = f.File.e.Name + "/" + subGlob[i]
}
out = append(out, subGlob...)
}
}
return
}
//ReadDir returns all the DirEntry 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) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(name)
if name == "." || name == "" {
return f.File.ReadDir(-1)
}
split := strings.Split(name, "/")
for i := 0; i < len(f.e); i++ {
if split[0] == f.e[i].Name {
if len(split) == 1 {
fi, err := f.r.newFile(f.e[i], &f)
if err != nil {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
out, err := fi.ReadDir(-1)
if err != nil {
err = &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
return out, err
}
sub, err := f.Sub(split[0])
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "readir"
pathErr.Path = name
return nil, pathErr
}
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
redDir, err := sub.(fs.ReadDirFS).ReadDir(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "readdir"
pathErr.Path = name
return nil, pathErr
}
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
return redDir, nil
}
}
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrNotExist,
}
}
//ReadFile returns the data (in []byte) for the file at name.
func (f FS) ReadFile(name string) ([]byte, error) {
fil, err := f.Open(name)
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
pathErr.Op = "readfile"
pathErr.Path = name
return nil, pathErr
}
}
var buf bytes.Buffer
_, err = io.Copy(&buf, fil)
if err != nil {
return nil, &fs.PathError{
Op: "readfile",
Path: name,
Err: err,
}
}
return buf.Bytes(), nil
}
//Stat returns the fs.FileInfo for the file at name.
func (f FS) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(strings.TrimPrefix(name, "/"))
if name == "." || name == "" {
return f.File.Stat()
}
split := strings.Split(name, "/")
for i := 0; i < len(f.e); i++ {
if split[0] == f.e[i].Name {
if len(split) == 1 {
in, err := f.r.newFileInfo(f.e[i])
if err != nil {
err = &fs.PathError{
Op: "stat",
Path: name,
Err: err,
}
}
return in, err
}
sub, err := f.Sub(split[0])
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "stat"
pathErr.Path = name
return nil, pathErr
}
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: err,
}
}
stat, err := sub.(fs.StatFS).Stat(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "stat"
pathErr.Path = name
return nil, pathErr
}
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: err,
}
}
return stat, nil
}
}
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: fs.ErrNotExist,
}
}
//Sub returns the FS at dir
func (f FS) Sub(dir string) (fs.FS, error) {
if !fs.ValidPath(dir) {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: fs.ErrInvalid,
}
}
dir = filepath.Clean(dir)
if dir == "." || dir == "" {
return f, nil
}
split := strings.Split(dir, "/")
for i := range f.e {
if f.e[i].Name != split[0] {
continue
}
if f.e[i].Type != inode.Dir {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: fs.ErrNotExist,
}
}
newFS, err := f.r.newFS(f.e[i], &f)
if err != nil {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: err,
}
}
if len(split) > 1 {
ret, err := newFS.Sub(strings.Join(split[1:], "/"))
if err != nil {
err.(*fs.PathError).Path = dir
}
return ret, err
}
return newFS, nil
}
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: fs.ErrNotExist,
}
}
+114
View File
@@ -0,0 +1,114 @@
package squashfs
import (
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/data"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
)
func (r Reader) inodeFromRef(ref uint64) (i inode.Inode, err error) {
offset, meta := (ref>>16)+r.s.InodeTableStart, ref&0xFFFF
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
_, err = rdr.Read(make([]byte, meta))
if err != nil {
return
}
return inode.Read(rdr, r.s.BlockSize)
}
func (r Reader) inodeFromDir(e directory.Entry) (i inode.Inode, err error) {
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(uint64(e.BlockStart)+r.s.InodeTableStart)), r.d)
_, err = rdr.Read(make([]byte, e.Offset))
if err != nil {
return
}
return inode.Read(rdr, r.s.BlockSize)
}
func (r Reader) getReaders(i inode.Inode) (full *data.FullReader, rdr *data.Reader, err error) {
var fragOffset uint64
var blockOffset uint64
var blockSizes []uint32
var fragInd uint32
var fragSize uint32
if i.Type == inode.Fil {
fragOffset = uint64(i.Data.(inode.File).FragOffset)
blockOffset = uint64(i.Data.(inode.File).BlockStart)
blockSizes = i.Data.(inode.File).BlockSizes
fragInd = i.Data.(inode.File).FragInd
fragSize = i.Data.(inode.File).Size % r.s.BlockSize
} else if i.Type == inode.EFil {
fragOffset = uint64(i.Data.(inode.EFile).FragOffset)
blockOffset = i.Data.(inode.EFile).BlockStart
blockSizes = i.Data.(inode.EFile).BlockSizes
fragInd = i.Data.(inode.EFile).FragInd
fragSize = uint32(i.Data.(inode.EFile).Size % uint64(r.s.BlockSize))
} else {
return nil, nil, errors.New("getReaders called on non-file type")
}
rdr = data.NewReader(toreader.NewReader(r.r, int64(blockOffset)), r.d, blockSizes, r.s.BlockSize)
full = data.NewFullReader(r.r, uint64(blockOffset), r.d, blockSizes, r.s.BlockSize)
if fragInd != 0xFFFFFFFF {
full.AddFragment(func() (io.Reader, error) {
var fragRdr io.Reader
fragRdr, err = r.fragReader(fragInd)
if err != nil {
return nil, err
}
var n, tmpN int
for n != int(fragOffset) {
tmpN, err = fragRdr.Read(make([]byte, int(fragOffset)-n))
if err != nil {
return nil, err
}
n += tmpN
}
fragRdr = io.LimitReader(fragRdr, int64(fragSize))
return fragRdr, nil
})
var fragRdr io.Reader
fragRdr, err = r.fragReader(fragInd)
if err != nil {
return nil, nil, err
}
var n, tmpN int
for n != int(fragOffset) {
tmpN, err = fragRdr.Read(make([]byte, int(fragOffset)-n))
if err != nil {
return nil, nil, err
}
n += tmpN
}
fragRdr = io.LimitReader(fragRdr, int64(fragSize))
rdr.AddFragment(fragRdr)
}
return
}
func (r Reader) readDirectory(i inode.Inode) ([]directory.Entry, error) {
var offset uint64
var blockOffset uint16
var size uint32
if i.Type == inode.Dir {
offset = uint64(i.Data.(inode.Directory).BlockStart)
blockOffset = i.Data.(inode.Directory).Offset
size = uint32(i.Data.(inode.Directory).Size)
} else if i.Type == inode.EDir {
offset = uint64(i.Data.(inode.EDirectory).BlockStart)
blockOffset = i.Data.(inode.EDirectory).Offset
size = i.Data.(inode.EDirectory).Size
} else {
return nil, errors.New("readDirectory called on non-directory type")
}
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset+r.s.DirTableStart)), r.d)
_, err := rdr.Read(make([]byte, blockOffset))
if err != nil {
return nil, err
}
return directory.ReadEntries(rdr, size)
}
-136
View File
@@ -1,136 +0,0 @@
package squashfs
import (
"io"
"net/http"
"os"
"testing"
goappimage "github.com/CalebQ42/GoAppImage"
)
const (
downloadURL = "https://github.com/Swordfish90/cool-retro-term/releases/download/1.1.1/Cool-Retro-Term-1.1.1-x86_64.AppImage"
appImageName = "Cool-Retro-Term.AppImage"
squashfsName = "airootfs.sfs" //testing with a ArchLinux root fs from the live img
)
//Right now, don't use. Arch linux sfs uses XZ compression and when tested, most files just completely fail to extract.
func TestSquashfs(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
squashFil, err := os.Open(wd + "/testing/" + squashfsName)
if err != nil {
t.Fatal(err)
}
rdr, err := NewSquashfsReader(squashFil)
if err != nil {
t.Fatal(err)
}
os.RemoveAll(wd + "/testing/" + squashfsName + ".d")
root, _ := rdr.GetRootFolder()
errs := root.ExtractWithOptions(wd+"/testing/"+squashfsName+".d", false, false, os.ModePerm, true)
t.Fatal(errs)
}
func TestAppImage(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
aiFil, err := os.Open(wd + "/testing/" + appImageName)
if os.IsNotExist(err) {
downloadTestAppImage(t, wd+"/testing")
aiFil, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
defer aiFil.Close()
stat, _ := aiFil.Stat()
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset))
if err != nil {
t.Fatal(err)
}
fil := rdr.GetFileAtPath(".DirIcon")
if fil == nil {
t.Fatal("Can't find desktop file")
}
errs := fil.ExtractSymlink(wd + "/testing/")
if len(errs) > 0 {
t.Fatal(errs)
}
// os.RemoveAll(wd + "/testing/" + appImageName + ".d")
// root, _ := rdr.GetRootFolder()
// errs := root.ExtractWithOptions(wd+"/testing/"+appImageName+".d", true, os.ModePerm, true)
// t.Fatal(errs)
t.Fatal("No problemo!")
}
func downloadTestAppImage(t *testing.T, dir string) {
//seems to time out on slow connections. Might fix that at some point... or not
os.Mkdir(dir, 0777)
appImage, err := os.Create(dir + "/" + appImageName)
if err != nil {
t.Fatal(err)
}
defer appImage.Close()
check := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
resp, err := check.Get(downloadURL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
_, err = io.Copy(appImage, resp.Body)
if err != nil {
t.Fatal(err)
}
}
func TestCreateSquashFromAppImage(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
err = os.Mkdir(wd+"/testing", 0777)
if err != nil && !os.IsExist(err) {
t.Fatal(err)
}
_, err = os.Open(wd + "/testing/" + appImageName)
if os.IsNotExist(err) {
downloadTestAppImage(t, wd+"/testing")
_, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
aiFil, err := os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
defer aiFil.Close()
aiFil.Seek(ai.Offset, 0)
os.Remove(wd + "/testing/" + appImageName + ".squashfs")
aiSquash, err := os.Create(wd + "/testing/" + appImageName + ".squashfs")
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(aiSquash, aiFil)
if err != nil {
t.Fatal(err)
}
}
+172
View File
@@ -0,0 +1,172 @@
package squashfs_test
//Actually proper tests go here.
import (
"errors"
"io"
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/CalebQ42/squashfs"
)
const (
squashfsURL = "https://darkstorm.tech/LinuxPATest.sfs"
squashfsName = "LinuxPATest.sfs"
filePath = "PortableApps/Notepad++Portable/App/DefaultData/Config/contextMenu.xml"
)
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 BenchmarkRace(b *testing.B) {
// tmpDir := b.TempDir()
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
start := time.Now()
rdr, err := squashfs.NewReader(fil)
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractTo(libPath)
if err != nil {
b.Fatal(err)
}
libTime = time.Since(start)
cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name())
start = time.Now()
err = cmd.Run()
if err != nil {
b.Fatal(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 := b.TempDir()
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 := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
op := squashfs.DefaultOptions()
op.Verbose = true
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, d fs.DirEntry, _ error) error {
libFil, e := os.Open(filepath.Join(libPath, path))
if e != nil {
return e
}
stat, _ := d.Info()
libStat, _ := libFil.Stat()
if stat.Size() != libStat.Size() {
t.Log(path, "not the same size between library and unsquashfs")
t.Log("File is", libStat.Size())
t.Log("Should be", stat.Size())
return errors.New("file not the correct size")
}
return nil
})
if err != nil {
t.Fatal(err)
}
t.Fatal("end")
}
func TestSingleFile(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
os.Remove(filepath.Base(filePath))
rdr, err := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
f, err := rdr.Open(filePath)
if err != nil {
t.Fatal(err)
}
op := squashfs.DefaultOptions()
op.Verbose = true
err = f.(*squashfs.File).ExtractWithOptions("testing", op)
if err != nil {
t.Fatal(err)
}
t.Fatal("HI")
}
+59 -46
View File
@@ -1,30 +1,22 @@
package squashfs
const (
gzipCompression = 1 + iota
lzmaCompression
lzoCompression
xzCompression
lz4Compression
zstdCompression
)
import "math"
//Superblock contains important information about a squashfs file. Located at the very front of the archive.
type superblock struct {
Magic uint32
InodeCount uint32
CreationTime uint32
ModTime uint32
BlockSize uint32
FragCount uint32
CompressionType uint16
CompType uint16
BlockLog uint16
Flags uint16
IDCount uint16
MajorVersion uint16
MinorVersion uint16
IdCount uint16
VerMaj uint16
VerMin uint16
RootInodeRef uint64
BytesUsed uint64
IDTableStart uint64
Size uint64
IdTableStart uint64
XattrTableStart uint64
InodeTableStart uint64
DirTableStart uint64
@@ -32,36 +24,57 @@ type superblock struct {
ExportTableStart uint64
}
//SuperblockFlags is the parsed version of Superblock.Flags
type superblockFlags struct {
UncompressedInodes bool
UncompressedData bool
Check bool
UncompressedFragments bool
NoFragments bool
AlwaysFragments bool
Duplicates bool
Exportable bool
UncompressedXattr bool
NoXattr bool
CompressorOptions bool
UncompressedIDs bool
func (s superblock) checkMagic() bool {
return s.Magic == 0x73717368
}
//GetFlags returns a SuperblockFlags for a given superblock.
func (s *superblock) GetFlags() superblockFlags {
return superblockFlags{
UncompressedInodes: s.Flags&0x1 == 0x1,
UncompressedData: s.Flags&0x2 == 0x2,
Check: s.Flags&0x4 == 0x4,
UncompressedFragments: s.Flags&0x8 == 0x8,
NoFragments: s.Flags&0x10 == 0x10,
AlwaysFragments: s.Flags&0x20 == 0x20,
Duplicates: s.Flags&0x40 == 0x40,
Exportable: s.Flags&0x80 == 0x80,
UncompressedXattr: s.Flags&0x100 == 0x100,
NoXattr: s.Flags&0x200 == 0x200,
CompressorOptions: s.Flags&0x400 == 0x400,
UncompressedIDs: s.Flags&0x800 == 0x800,
}
func (s superblock) checkBlockLog() bool {
return s.BlockLog == uint16(math.Log2(float64(s.BlockSize)))
}
func (s superblock) checkVersion() 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
}