Compare commits

...

70 Commits

Author SHA1 Message Date
Caleb Gardner 87f7533a17 Fix Error decompressing files with lots of NULLs #24 2023-08-11 15:32:52 -05:00
Caleb Gardner 7e1a584e8f Fixed an error not being reported in full reader 2023-04-17 11:40:15 -05:00
Caleb Gardner 942e0f770f Set main folder permission 2023-04-17 10:38:44 -05:00
Caleb Gardner 7d16990277 Updated README 2023-04-17 10:33:27 -05:00
Caleb Gardner d2c72f9464 Limit number of simultaneous file extractions to prevent hardlock
Added helper extraction functions
chmod & chown is now set after a folder's extraction to prevent permission issues
2023-04-17 10:22:10 -05:00
Caleb Gardner 2ba4551fb9 Fixed stupid errors 2023-04-17 08:01:20 -05:00
Caleb Gardner 6931075e7e Testing better large file support 2023-04-17 07:51:08 -05:00
Caleb Gardner 55a25c9d45 Updated README 2023-04-12 08:44:48 -05:00
Caleb Gardner 94b45c8402 Added IgnorePerm to ExtractionOptions 2023-04-12 07:57:57 -05:00
Caleb Gardner 01de43a5ae Added ErrReadNotFile to ReatAt, WriteTo 2023-04-11 00:34:43 -05:00
Caleb Gardner 5b29f4d029 Updated README 2023-04-09 21:09:53 -05:00
Caleb Gardner 6c7e926649 Updated deps 2023-04-09 20:41:45 -05:00
Caleb Gardner 72d85d7810 Added (expiremental) support for device files and Fifo 2023-04-09 20:41:00 -05:00
Caleb Gardner 67df5f40c6 Fix #21 and update deps 2023-03-29 12:45:21 -05:00
Caleb Gardner 1ae5593e6c Merge pull request #20 from x1unix/main
fix: use correct ENODATA value on different platforms
2023-03-18 06:57:45 -05:00
ds.hiveon 653c4a167b fix: remove redundant build tag 2023-03-18 07:46:29 +01:00
ds.hiveon 9fe17650b8 fix: use correct ENODATA value on different platforms 2023-03-18 07:44:18 +01:00
Caleb Gardner e9e967f085 Fuse2 fix 2023-01-18 02:31:08 -06:00
Caleb Gardner 187da99dd6 Added fuse2 mounting 2023-01-17 10:20:11 -06:00
Caleb Gardner 75d2a29319 Export SquashFuse 2023-01-17 09:56:34 -06:00
Caleb Gardner ce2e45ceec Fixed issues with decompress.Decoder 2023-01-05 01:29:23 -06:00
Caleb Gardner 089ef53c8c Revert changes to fullreader 2023-01-04 06:40:57 -06:00
Caleb Gardner 658e5c9e0b Mount is non-blocking again 2023-01-04 06:01:12 -06:00
Caleb Gardner f2d86aff96 Fixed a race condition with mounts that caused them to fail 2023-01-04 05:41:43 -06:00
Caleb Gardner f61237a1f0 Added ReaderAtOffset 2022-12-22 02:00:42 -06:00
Caleb Gardner 820e06e792 fuse Serve in goroutine 2022-12-17 17:06:28 -06:00
Caleb Gardner 4f8f5f6928 Tweaks to decode interface 2022-12-17 16:47:33 -06:00
Caleb Gardner 1b5078c7bd Messing around with optimizations. 2022-12-14 13:48:22 -06:00
Caleb Gardner 56fdba2f28 Merge pull request #17 from CalebQ42/fuseBraz
Fuse SUCCESS
2022-12-03 02:50:17 -06:00
Caleb Gardner ffbf4ebc64 Fuse SUCCESS 2022-12-03 02:45:58 -06:00
Belac Darkstorm a015b16293 Clean path before checking if valid. 2022-10-24 03:17:55 -05:00
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
62 changed files with 3017 additions and 3560 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: CalebQ42
+2 -1
View File
@@ -1 +1,2 @@
testing
testing
/go-unsquashfs
+22 -9
View File
@@ -1,18 +1,31 @@
# squashfs (WIP)
# squashfs
[![PkgGoDev](https://pkg.go.dev/badge/github.com/CalebQ42/squashfs)](https://pkg.go.dev/github.com/CalebQ42/squashfs) [![Go Report Card](https://goreportcard.com/badge/github.com/CalebQ42/squashfs)](https://goreportcard.com/report/github.com/CalebQ42/squashfs)
A PURE Go library to read and write squashfs.
A PURE Go library to read squashfs. There is currently no plans to add archive creation support as it will almost always be better to just call `mksquashfs`. I could see some possible use cases, but probably won't spend time on it unless it's requested (open a discussion fi you want this feature).
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.
The only major thing missing from squashfs reading is Xattr parsing.
Currently has support for reading squashfs files and extracting files and folders.
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).
## Performane
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`)
## [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
## Limitations
* 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.
* Socket files are not extracted.
* From my research, it seems like a socket file would be useless if it could be created.
* Fifo files are ignored on `darwin`
## Issues
* Significantly slower then `unsquashfs` when extracting folders (about 5 ~ 7 times slower on a ~100MB archive using zstd compression)
* This seems to be related to above along with the general optimization of `unsquashfs` and it's compression libraries.
* The larger the file's tree, the slower the extraction will be. Arch Linux's Live USB's airootfs.sfs takes ~35x longer for a full extraction.
## Recommendations on Usage
Due to the above performance consideration, this library should only be used to access files within the archive without extraction, or to mount it via Fuse.
* Neither of these use cases are largely effected by the issue above.
-63
View File
@@ -1,63 +0,0 @@
package squashfs
import (
"io"
)
type bufferedBytes struct {
data []byte
r offsetRange
}
type offsetRange struct {
beg int
end int
}
func (o *offsetRange) offset(off int) {
o.beg += off
o.end += off
}
func (o offsetRange) within(check int) bool {
return check >= o.beg || check <= o.end
}
type bufferedWriter struct {
w io.Writer
buffer []bufferedBytes
mainOffset int
}
func newBufferedWriter(w io.Writer) *bufferedWriter {
var out bufferedWriter
out.w = w
return &out
}
func (b *bufferedWriter) WriteTo(data []byte, offset int64) (n int, err error) {
if int(offset) == b.mainOffset {
n, err = b.Write(data)
if err != nil {
return
}
}
newBuff := bufferedBytes{
data: data,
r: offsetRange{
beg: int(offset),
end: int(offset) + len(data),
},
}
b.buffer = append(b.buffer, newBuff)
return 0, nil
}
func (b *bufferedWriter) Write(data []byte) (int, error) {
n, err := b.w.Write(data)
b.mainOffset += n
if err != nil {
return n, err
}
return n, err
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"flag"
"fmt"
"os"
"time"
"github.com/CalebQ42/squashfs"
)
func main() {
verbose := flag.Bool("v", false, "Verbose")
ignore := flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755")
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Println("Please provide a file name and extraction path")
os.Exit(0)
}
f, err := os.Open(flag.Arg(0))
if err != nil {
panic(err)
}
r, err := squashfs.NewReader(f)
if err != nil {
panic(err)
}
op := squashfs.DefaultOptions()
op.Verbose = *verbose
op.IgnorePerm = *ignore
n := time.Now()
err = r.ExtractWithOptions(flag.Arg(1), op)
if err != nil {
panic(err)
}
fmt.Println("Took:", time.Since(n))
}
-247
View File
@@ -1,247 +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
curData []byte
sizes []uint32
offset int64 //offset relative to the beginning of the squash file
curBlock int //Which block in sizes is currently cached
curReadOffset int //offset relative to the currently cached data
}
//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
dr.sizes = sizes
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.FileType:
fil := i.Info.(inode.File)
if fil.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.sizes = append(rdr.sizes, sizes)
}
if fil.Fragmented {
rdr.sizes = rdr.sizes[:len(rdr.sizes)-1]
}
case inode.ExtFileType:
fil := i.Info.(inode.ExtFile)
if fil.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.sizes = append(rdr.sizes, sizes)
}
if fil.Fragmented {
rdr.sizes = rdr.sizes[:len(rdr.sizes)-1]
}
default:
return nil, errInodeNotFile
}
err := rdr.readCurBlock()
if err != nil {
return nil, err
}
return &rdr, nil
}
//removed the compression bit from a data block size
func actualDataSize(size uint32) uint32 {
return size &^ (1 << 24)
}
func (d *dataReader) readNextBlock() error {
d.curBlock++
if d.curBlock >= len(d.sizes) {
d.curBlock--
return io.EOF
}
err := d.readCurBlock()
if err != nil {
d.curBlock--
d.readCurBlock()
return err
}
return nil
}
func (d *dataReader) readBlockAt(offset int64, size uint32) ([]byte, error) {
compressed := size&(1<<24) != (1 << 24)
size = size &^ (1 << 24)
if d.sizes[d.curBlock] == 0 {
return make([]byte, d.r.super.BlockSize), nil
}
sec := io.NewSectionReader(d.r.r, offset, int64(size))
if compressed {
btys, err := d.r.decompressor.Decompress(sec)
if err != nil {
return nil, err
}
return btys, nil
}
var buf bytes.Buffer
_, err := io.Copy(&buf, sec)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (d *dataReader) offsetForBlock(index int) int64 {
out := d.offset
for i := 0; i < index; i++ {
out += int64(actualDataSize(d.sizes[i]))
}
return out
}
func (d *dataReader) readCurBlock() error {
if d.curBlock >= len(d.sizes) {
return io.EOF
}
offset := d.offsetForBlock(d.curBlock)
data, err := d.readBlockAt(offset, d.sizes[d.curBlock])
if err != nil {
return err
}
d.curData = data
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++ {
if d.curReadOffset < len(d.curData) {
p[read] = d.curData[d.curReadOffset]
} else {
break
}
d.curReadOffset++
}
}
if read != len(p) {
return read, errors.New("Didn't read enough data")
}
return read, nil
}
// WriteTo writes all the data in the datablock to the writer. MUST BE USED ON A FRESH DATA READER.
func (d *dataReader) WriteTo(w io.Writer) (int64, error) {
type dataCache struct {
err error
data []byte
index int
}
dataChan := make(chan *dataCache)
for i := range d.sizes {
go func(index int, c chan *dataCache) {
var cache dataCache
cache.index = index
defer func() {
c <- &cache
}()
data, err := d.readBlockAt(d.offsetForBlock(index), d.sizes[index])
if err != nil {
cache.err = err
return
}
cache.data = data
return
}(i, dataChan)
}
curIndex := 0
totalWrite := int64(0)
var backlog []*dataCache
mainLoop:
for {
if curIndex == len(d.sizes) {
return totalWrite, nil
}
if len(backlog) > 0 {
for i, cache := range backlog {
if cache.index == curIndex {
writen, err := w.Write(cache.data)
totalWrite += int64(writen)
if err != nil {
return totalWrite, err
}
if len(backlog) > 0 {
backlog[i] = backlog[len(backlog)-1]
backlog = backlog[:len(backlog)-1]
} else {
backlog = nil
}
curIndex++
continue mainLoop
}
}
}
cache := <-dataChan
if cache.err != nil {
return totalWrite, cache.err
}
if cache.index == curIndex {
writen, err := w.Write(cache.data)
totalWrite += int64(writen)
if err != nil {
return totalWrite, err
}
curIndex++
} else {
backlog = append(backlog, cache)
}
}
}
-137
View File
@@ -1,137 +0,0 @@
package squashfs
import (
"io"
"io/fs"
"time"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
//DirEntry is a child of a directory.
type DirEntry struct {
en *directory.Entry
parent *FS
r *Reader
}
func (r *Reader) newDirEntry(en *directory.Entry, parent *FS) *DirEntry {
return &DirEntry{
en: en,
parent: parent,
r: r,
}
}
//Name returns the DirEntry's name
func (d DirEntry) Name() string {
return d.en.Name
}
//IsDir Yep.
func (d DirEntry) IsDir() bool {
return d.en.Type == inode.DirType
}
//Type returns the type bits of fs.FileMode of the DirEntry.
func (d DirEntry) Type() fs.FileMode {
switch d.en.Type {
case inode.DirType:
return fs.ModeDir
case inode.SymType:
return fs.ModeSymlink
default:
return 0
}
}
//Info returns the fs.FileInfo for the given DirEntry.
func (d DirEntry) Info() (fs.FileInfo, error) {
in, err := d.r.getInodeFromEntry(d.en)
if err != nil {
return nil, err
}
return &FileInfo{
name: d.en.Name,
i: in,
parent: d.parent,
r: d.r,
}, 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.InodeOffset)))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(en.InodeBlockOffset), 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
}
//FileInfo is a fs.FileInfo for a file.
type FileInfo struct {
i *inode.Inode
parent *FS
r *Reader
name string
}
//Name is the file's name.
func (f FileInfo) Name() string {
return f.name
}
//Size is the file's size if it's a regular file. Otherwise, returns 0.
func (f FileInfo) Size() int64 {
switch f.i.Type {
case inode.FileType:
return int64(f.i.Info.(inode.File).Size)
case inode.ExtFileType:
return int64(f.i.Info.(inode.ExtFile).Size)
}
return 0
}
//Mode returns the fs.FileMode bits of the file.
func (f FileInfo) Mode() fs.FileMode {
mode := fs.FileMode(f.i.Permissions)
switch f.i.Type {
case inode.DirType | inode.ExtDirType:
return mode | fs.ModeDir
case inode.ExtDirType:
return mode | fs.ModeDir
case inode.SymType:
return mode | fs.ModeSymlink
case inode.ExtSymType:
return mode | fs.ModeSymlink
}
return mode
}
//ModTime is the last time the file was modified.
func (f FileInfo) ModTime() time.Time {
return time.Unix(int64(f.i.ModifiedTime), 0)
}
//IsDir yep.
func (f FileInfo) IsDir() bool {
return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType
}
//Sys returns the File for the FileInfo. If something goes wrong, nil is returned.
func (f FileInfo) Sys() interface{} {
fil, err := f.File()
if err != nil {
return nil
}
return fil
}
-379
View File
@@ -1,379 +0,0 @@
package squashfs
import (
"errors"
"io"
"io/fs"
"log"
"os"
"path"
"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
parent *FS
r *Reader
reader *fileReader
name string
dirsRead int
}
//File creates a File from the FileInfo.
//*File satisfies fs.File and fs.ReadDirFile.
func (f FileInfo) File() (file *File, err error) {
file = &File{
name: f.name,
r: f.r,
parent: f.parent,
i: f.i,
}
if file.IsRegular() {
file.reader, err = f.r.newFileReader(f.i)
}
return
}
//File creates a File from the DirEntry.
func (d DirEntry) File() (file *File, err error) {
return d.r.newFileFromDirEntry(d.en, d.parent)
}
func (r Reader) newFileFromDirEntry(en *directory.Entry, parent *FS) (file *File, err error) {
file = &File{
name: en.Name,
r: &r,
parent: parent,
}
file.i, err = r.getInodeFromEntry(en)
if err != nil {
return nil, err
}
if file.IsRegular() {
file.reader, err = r.newFileReader(file.i)
}
return
}
//Stat returns the File's fs.FileInfo
func (f File) Stat() (fs.FileInfo, error) {
return &FileInfo{
i: f.i,
name: f.name,
parent: f.parent,
r: f.r,
}, 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.FileType || f.i.Type == inode.ExtFileType {
if f.reader == nil {
return 0, fs.ErrClosed
}
return f.reader.Read(p)
}
return 0, errors.New("Can only read files")
}
//WriteTo writes all data from the file to the writer. This is multi-threaded.
func (f File) WriteTo(w io.Writer) (int64, error) {
if f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType {
if f.reader == nil {
return 0, fs.ErrClosed
}
return f.reader.WriteTo(w)
}
return 0, errors.New("Can only read files")
}
//Close simply nils the underlying reader. Here mostly to satisfy fs.File
func (f *File) Close() error {
f.reader = 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) ([]fs.DirEntry, error) {
if !f.IsDir() {
return nil, errors.New("File is not a directory")
}
ffs, err := f.FS()
if err != nil {
return nil, err
}
var beg, end int
if n <= 0 {
beg, end = 0, len(ffs.entries)
} else {
beg, end = f.dirsRead, f.dirsRead+n
if end > len(ffs.entries) {
end = len(ffs.entries)
err = io.EOF
}
}
out := make([]fs.DirEntry, end-beg)
for i, ent := range ffs.entries[beg:end] {
out[i] = f.r.newDirEntry(ent, ffs)
}
return out, err
}
//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.readDirFromInode(f.i)
if err != nil {
return nil, err
}
return &FS{
entries: ents,
parent: f.parent,
r: f.r,
name: f.name,
}, nil
}
//IsDir Yep.
func (f File) IsDir() bool {
return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType
}
func (f File) path() string {
if f.name == "/" {
return f.name
}
return f.parent.path() + "/" + f.name
}
//IsRegular yep.
func (f File) IsRegular() bool {
return f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType
}
//IsSymlink yep.
func (f File) IsSymlink() bool {
return f.i.Type == inode.SymType || f.i.Type == inode.ExtSymType
}
//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.SymType:
return f.i.Info.(inode.Sym).Path
case inode.ExtSymType:
return f.i.Info.(inode.ExtSym).Path
}
return ""
}
//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 {
notBase bool
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{
DereferenceSymlink: false,
UnbreakSymlink: false,
Verbose: false,
FolderPerm: fs.ModePerm,
}
}
//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: fs.ModePerm,
})
}
//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 {
folder = path.Clean(folder)
if !op.notBase {
err := os.MkdirAll(folder, op.FolderPerm)
if err != nil {
return err
}
}
stat, err := f.Stat()
if f.IsDir() {
if op.notBase {
err = os.Mkdir(folder+"/"+f.name, stat.Mode())
if err != nil && !os.IsExist(err) {
return err
}
} else {
op.notBase = true
}
var ents []fs.DirEntry
ents, err = f.ReadDir(0)
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 *DirEntry) {
fil, goErr := ent.File()
if goErr != nil {
errChan <- goErr
fil.Close()
return
}
errChan <- fil.ExtractWithOptions(folder+"/"+f.name, op)
fil.Close()
return
}(ents[i].(*DirEntry))
}
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.name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.name)
fil, err = os.Create(folder + "/" + f.name)
if err != nil {
log.Println("Error while creating", folder+"/"+f.name)
return err
}
} else if err != nil {
return err
}
_, err = io.Copy(fil, f)
if err != nil {
log.Println("Error while copying data to", folder+"/"+f.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.name)
}
return errors.New("Cannot get symlink target")
}
fil.name = f.name
err = fil.ExtractWithOptions(folder, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting the symlink's file:", folder+"/"+f.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.name)
}
return errors.New("Cannot get symlink target")
}
extractLoc := path.Clean(folder + "/" + path.Dir(symPath))
err = fil.ExtractWithOptions(extractLoc, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting ", folder+"/"+f.name)
}
return err
}
}
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.name)
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name)
}
if err != nil {
if op.Verbose {
log.Println("Error while making symlink:", folder+"/"+f.name)
}
return err
}
return nil
}
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.i.Type)))
}
//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.Entry, error) {
var offset uint32
var metaOffset uint16
var size uint32
switch i.Type {
case inode.DirType:
offset = i.Info.(inode.Dir).DirectoryIndex
metaOffset = i.Info.(inode.Dir).DirectoryOffset
size = uint32(i.Info.(inode.Dir).DirectorySize)
case inode.ExtDirType:
offset = i.Info.(inode.ExtDir).DirectoryIndex
metaOffset = i.Info.(inode.ExtDir).DirectoryOffset
size = i.Info.(inode.ExtDir).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
}
ents, err := directory.NewDirectory(br, size)
if err != nil {
return nil, err
}
return ents, nil
}
-101
View File
@@ -1,101 +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.FileType && in.Type != inode.ExtFileType {
return nil, errPathIsNotFile
}
switch in.Type {
case inode.FileType:
fil := in.Info.(inode.File)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.BlockStart == 0
rdr.FileSize = int(fil.Size)
case inode.ExtFileType:
fil := in.Info.(inode.ExtFile)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.BlockStart == 0
rdr.FileSize = int(fil.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
}
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
}
func (f *fileReader) WriteTo(w io.Writer) (int64, error) {
if f.fragOnly {
n, err := w.Write(f.fragmentData)
return int64(n), err
}
if !f.fragged {
return f.data.WriteTo(w)
}
n, err := f.data.WriteTo(w)
if err != nil {
return int64(n), err
}
nn, err := w.Write(f.fragmentData)
return int64(nn) + n, err
}
-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 uint64
var fragIndex uint32
var fragOffset uint32
if in.Type == inode.FileType {
bf := in.Info.(inode.File)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.BlockStart == 0 {
size = uint64(bf.Size)
} else {
size = uint64(bf.BlockSizes[len(bf.BlockSizes)-1])
}
fragIndex = bf.FragmentIndex
fragOffset = bf.FragmentOffset
} else if in.Type == inode.ExtFileType {
bf := in.Info.(inode.ExtFile)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.BlockStart == 0 {
size = bf.Size
} else {
size = uint64(bf.BlockSizes[len(bf.BlockSizes)-1])
}
fragIndex = bf.FragmentIndex
fragOffset = bf.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
}
-489
View File
@@ -1,489 +0,0 @@
package squashfs
import (
"bytes"
"errors"
"io"
"io/fs"
"os"
"path"
"strings"
"github.com/CalebQ42/squashfs/internal/directory"
)
//FS is a fs.FS representation of a squashfs directory.
//Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS
type FS struct {
r *Reader
parent *FS
name string
entries []*directory.Entry
}
//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 = path.Clean(strings.TrimPrefix(name, "/"))
split := strings.Split(name, "/")
if split[0] == ".." {
if f.parent == nil {
//This should only happen on the root FS
return nil, &fs.PathError{
Op: "open",
Path: name,
//TODO: make error clearer
Err: errors.New("Trying to get file outside of squashfs"),
}
}
return f.parent.Open(strings.Join(split[1:], "/"))
}
for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match {
if len(split) == 1 {
return f.r.newFileFromDirEntry(f.entries[i], &f)
}
sub, err := f.Sub(split[0])
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
pathErr.Op = "open"
pathErr.Path = name
return nil, err
}
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: err,
}
}
fil, err := sub.Open(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "open"
pathErr.Path = name
return nil, err
}
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: err,
}
}
return fil, nil
}
}
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.
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 = path.Clean(strings.TrimPrefix(pattern, "/"))
split := strings.Split(pattern, "/")
if split[0] == ".." {
if f.parent == nil {
//This should only happen on the root FS
return nil, &fs.PathError{
Op: "readdir",
Path: pattern,
//TODO: make error clearer
Err: errors.New("Trying to get file outside of squashfs"),
}
}
return f.parent.Glob(strings.Join(split[1:], "/"))
}
for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match {
if len(split) == 1 {
out = append(out, f.entries[i].Name)
continue
}
sub, err := f.Sub(split[0])
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "glob"
pathErr.Path = pattern
return nil, pathErr
}
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: err,
}
}
subGlob, err := sub.(FS).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.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 = path.Clean(strings.TrimPrefix(name, "/"))
split := strings.Split(name, "/")
if split[0] == ".." {
if f.parent == nil {
//This should only happen on the root FS
return nil, &fs.PathError{
Op: "readdir",
Path: name,
//TODO: make error clearer
Err: errors.New("Trying to get file outside of squashfs"),
}
}
return f.parent.ReadDir(strings.Join(split[1:], "/"))
}
for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match {
if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
ents, err := f.r.readDirFromInode(in)
if err != nil {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: err,
}
}
out := make([]fs.DirEntry, len(f.entries))
for i, ent := range ents {
out[i] = &DirEntry{
en: ent,
parent: &f,
r: f.r,
}
}
return out, nil
}
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).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 = path.Clean(strings.TrimPrefix(name, "/"))
split := strings.Split(name, "/")
if split[0] == ".." {
if f.parent == nil {
//This should only happen on the root FS
return nil, &fs.PathError{
Op: "stat",
Path: name,
//TODO: make error clearer
Err: errors.New("Trying to get file outside of squashfs"),
}
}
return f.parent.Stat(strings.Join(split[1:], "/"))
}
for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match {
if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil {
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: err,
}
}
return FileInfo{
i: in,
parent: &f,
r: f.r,
name: f.entries[i].Name,
}, nil
}
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).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 = path.Clean(strings.TrimPrefix(dir, "/"))
split := strings.Split(dir, "/")
if split[0] == ".." {
if f.parent == nil {
//This should only happen on the root FS
return nil, &fs.PathError{
Op: "sub",
Path: dir,
//TODO: make error clearer
Err: errors.New("Trying to get file outside of squashfs"),
}
}
return f.parent.Sub(strings.Join(split[1:], "/"))
}
for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match {
if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: err,
}
}
ents, err := f.r.readDirFromInode(in)
if err != nil {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: err,
}
}
return &FS{
r: f.r,
parent: &f,
name: f.entries[i].Name,
entries: ents,
}, nil
}
sub, err := f.Sub(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "sub"
pathErr.Path = dir
return nil, pathErr
}
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: err,
}
}
return sub, nil
}
}
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: fs.ErrNotExist,
}
}
func (f FS) path() string {
return f.parent.path() + "/" + f.name
}
//ExtractTo extracts the File to the given folder with the default options.
//It extracts the directory's contents to the folder.
func (f FS) ExtractTo(folder string) error {
return f.ExtractWithOptions(folder, DefaultOptions())
}
//ExtractSymlink extracts the File to the folder with the DereferenceSymlink option.
//It extracts the directory's contents to the folder.
func (f FS) ExtractSymlink(folder string) error {
return f.ExtractWithOptions(folder, ExtractionOptions{
DereferenceSymlink: true,
FolderPerm: fs.ModePerm,
})
}
//ExtractWithOptions extracts the File to the given folder with the given ExtrationOptions.
//It extracts the directory's contents to the folder.
func (f FS) ExtractWithOptions(folder string, op ExtractionOptions) error {
op.notBase = true
folder = path.Clean(folder)
err := os.MkdirAll(folder, op.FolderPerm)
if err != nil {
return err
}
errChan := make(chan error)
for i := 0; i < len(f.entries); i++ {
go func(ent *DirEntry) {
fil, goErr := ent.File()
if goErr != nil {
errChan <- goErr
return
}
errChan <- fil.ExtractWithOptions(folder, op)
fil.Close()
return
}(&DirEntry{
en: f.entries[i],
parent: &f,
r: f.r,
})
}
for i := 0; i < len(f.entries); i++ {
err := <-errChan
if err != nil {
return err
}
}
return nil
}
+150
View File
@@ -0,0 +1,150 @@
package squashfs
import (
"bytes"
"context"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/seaweedfs/fuse"
"github.com/seaweedfs/fuse/fs"
)
// Mounts the archive to the given mountpoint using fuse2. Non-blocking.
// If Unmount does not get called, the mount point must be unmounted using umount before the directory can be used again.
func (r *Reader) MountFuse2(mountpoint string) (err error) {
if r.con != nil {
return errors.New("squashfs archive already mounted")
}
r.con2, err = fuse.Mount(mountpoint, fuse.ReadOnly())
if err != nil {
return
}
<-r.con2.Ready
r.mount2Done = make(chan struct{})
go func() {
fs.Serve(r.con2, squashFuse2{r: r})
close(r.mount2Done)
}()
return
}
// Blocks until the mount ends.
// Fuse2 version.
func (r *Reader) MountWaitFuse2() {
if r.mount2Done != nil {
<-r.mount2Done
}
}
// Unmounts the archive.
// Fuse2 version.
func (r *Reader) UnmountFuse2() error {
if r.con != nil {
defer func() { r.con = nil }()
return r.con.Close()
}
return errors.New("squashfs archive is not mounted")
}
type squashFuse2 struct {
r *Reader
}
func (s squashFuse2) Root() (fs.Node, error) {
return fileNode2{File: s.r.FS.File}, nil
}
type fileNode2 struct {
*File
}
func (f fileNode2) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Blocks = f.r.s.Size / 512
if f.r.s.Size%512 > 0 {
attr.Blocks++
}
attr.Gid = f.r.ids[f.i.GidInd]
attr.Inode = uint64(f.i.Num)
attr.Mode = f.i.Mode()
attr.Nlink = f.i.LinkCount()
attr.Size = f.i.Size()
attr.Uid = f.r.ids[f.i.UidInd]
return nil
}
func (f fileNode2) Id() uint64 {
return uint64(f.i.Num)
}
func (f fileNode2) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
return f.SymlinkPath(), nil
}
func (f fileNode2) Lookup(ctx context.Context, name string) (fs.Node, error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
ret, err := asFS.OpenFile(name)
if err != nil {
return nil, fuse.ENOENT
}
return fileNode2{File: ret}, nil
}
func (f fileNode2) ReadAll(ctx context.Context) ([]byte, error) {
if f.IsRegular() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
return buf.Bytes(), err
}
return nil, ENODATA
}
func (f fileNode2) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
if f.IsRegular() {
buf := make([]byte, req.Size)
n, err := f.File.ReadAt(buf, req.Offset)
if err == io.EOF {
resp.Data = buf[:n]
}
return nil
}
return ENODATA
}
func (f fileNode2) ReadDirAll(ctx context.Context) (out []fuse.Dirent, err error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
var t fuse.DirentType
for i := range asFS.e {
switch asFS.e[i].Type {
case inode.Fil:
t = fuse.DT_File
case inode.Dir:
t = fuse.DT_Dir
case inode.Block:
t = fuse.DT_Block
case inode.Sym:
t = fuse.DT_Link
case inode.Char:
t = fuse.DT_Char
case inode.Fifo:
t = fuse.DT_FIFO
case inode.Sock:
t = fuse.DT_Socket
default:
t = fuse.DT_Unknown
}
out = append(out, fuse.Dirent{
Inode: uint64(asFS.e[i].Num),
Type: t,
Name: asFS.e[i].Name,
})
}
return
}
+148
View File
@@ -0,0 +1,148 @@
package squashfs
import (
"bytes"
"context"
"errors"
"io"
"github.com/CalebQ42/fuse"
"github.com/CalebQ42/fuse/fs"
"github.com/CalebQ42/squashfs/internal/inode"
)
// Mounts the archive to the given mountpoint using fuse3. Non-blocking.
// If Unmount does not get called, the mount point must be unmounted using umount before the directory can be used again.
func (r *Reader) Mount(mountpoint string) (err error) {
if r.con != nil {
return errors.New("squashfs archive already mounted")
}
r.con, err = fuse.Mount(mountpoint, fuse.ReadOnly())
if err != nil {
return
}
<-r.con.Ready
r.mountDone = make(chan struct{})
go func() {
fs.Serve(r.con, squashFuse{r: r})
close(r.mountDone)
}()
return
}
// Blocks until the mount ends.
func (r *Reader) MountWait() {
if r.mountDone != nil {
<-r.mountDone
}
}
// Unmounts the archive.
func (r *Reader) Unmount() error {
if r.con != nil {
defer func() { r.con = nil }()
return r.con.Close()
}
return errors.New("squashfs archive is not mounted")
}
type squashFuse struct {
r *Reader
}
func (s squashFuse) Root() (fs.Node, error) {
return fileNode{File: s.r.FS.File}, nil
}
type fileNode struct {
*File
}
func (f fileNode) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Blocks = f.r.s.Size / 512
if f.r.s.Size%512 > 0 {
attr.Blocks++
}
attr.Gid = f.r.ids[f.i.GidInd]
attr.Inode = uint64(f.i.Num)
attr.Mode = f.i.Mode()
attr.Nlink = f.i.LinkCount()
attr.Size = f.i.Size()
attr.Uid = f.r.ids[f.i.UidInd]
return nil
}
func (f fileNode) Id() uint64 {
return uint64(f.i.Num)
}
func (f fileNode) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
return f.SymlinkPath(), nil
}
func (f fileNode) Lookup(ctx context.Context, name string) (fs.Node, error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
ret, err := asFS.OpenFile(name)
if err != nil {
return nil, fuse.ENOENT
}
return fileNode{File: ret}, nil
}
func (f fileNode) ReadAll(ctx context.Context) ([]byte, error) {
if f.IsRegular() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
return buf.Bytes(), err
}
return nil, ENODATA
}
func (f fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
if f.IsRegular() {
buf := make([]byte, req.Size)
n, err := f.File.ReadAt(buf, req.Offset)
if err == io.EOF {
resp.Data = buf[:n]
}
return nil
}
return ENODATA
}
func (f fileNode) ReadDirAll(ctx context.Context) (out []fuse.Dirent, err error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
var t fuse.DirentType
for i := range asFS.e {
switch asFS.e[i].Type {
case inode.Fil:
t = fuse.DT_File
case inode.Dir:
t = fuse.DT_Dir
case inode.Block:
t = fuse.DT_Block
case inode.Sym:
t = fuse.DT_Link
case inode.Char:
t = fuse.DT_Char
case inode.Fifo:
t = fuse.DT_FIFO
case inode.Sock:
t = fuse.DT_Socket
default:
t = fuse.DT_Unknown
}
out = append(out, fuse.Dirent{
Inode: uint64(asFS.e[i].Num),
Type: t,
Name: asFS.e[i].Name,
})
}
return
}
+7
View File
@@ -0,0 +1,7 @@
package squashfs
import (
"golang.org/x/sys/unix"
)
var ENODATA = unix.Errno(unix.ENODATA)
+5
View File
@@ -0,0 +1,5 @@
package squashfs
import "github.com/CalebQ42/fuse"
var ENODATA = fuse.ENODATA
+3
View File
@@ -0,0 +1,3 @@
package squashfs
var ENODATA = windows.Errno(windows.ENODATA)
+9 -14
View File
@@ -1,19 +1,14 @@
module github.com/CalebQ42/squashfs
go 1.15
go 1.21
require (
github.com/CalebQ42/GoAppImage v0.5.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.6
github.com/kr/text v0.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.3
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/ulikunitz/xz v0.5.9
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-20210107192922-496545a6307b // indirect
github.com/CalebQ42/fuse v0.1.0
github.com/klauspost/compress v1.16.7
github.com/pierrec/lz4/v4 v4.1.18
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e
github.com/seaweedfs/fuse v1.2.2
github.com/therootcompany/xz v1.0.1
github.com/ulikunitz/xz v0.5.11
golang.org/x/sys v0.11.0
)
+16 -76
View File
@@ -1,76 +1,16 @@
github.com/CalebQ42/GoAppImage v0.5.0 h1:znoKNXtliH754tS9sYwyOIg/0wFDjFN5Twc7PAh1rSM=
github.com/CalebQ42/GoAppImage v0.5.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.6 h1:EgWPCW6O3n1D5n99Zq3xXBt9uCwRGvpwGOusOLNBRSQ=
github.com/klauspost/compress v1.11.6/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.3 h1:/dvQpkb0o1pVlSgKNQqfkavlnXaIK+hJ0LXsKRUN9D4=
github.com/pierrec/lz4/v4 v4.1.3/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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
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/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/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-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/CalebQ42/fuse v0.1.0 h1:KLCNjun7zcd2kBNVFfH+SWJyhuwJdE0nhw5/q8K8HGQ=
github.com/CalebQ42/fuse v0.1.0/go.mod h1:pJpoKG03HJKVhsp8o0YQYqmfbFsr3Eowt90yQGQVO+4=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/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/seaweedfs/fuse v1.2.2 h1:01l8OjIdyATRNqVc/gDPgFobuC8ubQF3hRKOPColROw=
github.com/seaweedfs/fuse v1.2.2/go.mod h1:iwbDQv5BZACY54r6AO/6xsLNuMaYcBKSkLTZVfmK594=
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.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-75
View File
@@ -1,75 +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 {
wrt *zlib.Writer
gzipInit
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
err := binary.Read(r, binary.LittleEndian, &gzip.gzipInit)
if err != nil {
return nil, err
}
//TODO: proper support for window size and strategies
gzip.HasCustomWindow = gzip.WindowSize != 15
gzip.HasStrategies = gzip.Strategies != 0 && gzip.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
var err error
if g.wrt == nil {
if g.CompressionLevel == 0 {
g.wrt = zlib.NewWriter(&buf)
} else {
g.wrt, err = zlib.NewWriterLevel(&buf, int(g.CompressionLevel))
if err != nil {
return nil, err
}
}
}
wrt, err := zlib.NewWriterLevel(&buf, int(g.CompressionLevel))
if err != nil {
return nil, err
}
_, err = wrt.Write(data)
if err != nil {
return nil, err
}
wrt.Close()
return buf.Bytes(), nil
}
-55
View File
@@ -1,55 +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
}
//Compress implements compression.Compress
func (l *Lz4) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := lz4.NewWriter(&buf)
if l.HC {
err := w.Apply(lz4.CompressionLevelOption(lz4.Level9))
if err != nil {
return nil, err
}
}
_, err := w.Write(data)
if err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
-40
View File
@@ -1,40 +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
}
//Compress implements compression.Compress
func (l *Lzma) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w, err := lzma.NewWriter(&buf)
if err != nil {
return nil, err
}
_, err = w.Write(data)
if err != nil {
return nil, err
}
w.Close()
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)
}
-75
View File
@@ -1,75 +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
}
//Compress implements compression.Compress
func (x *Xz) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w, err := xz.NewWriter(&buf)
if err != nil {
return nil, err
}
w.DictCap = int(x.DictionarySize)
err = w.Verify()
if err != nil {
return nil, err
}
_, err = w.Write(data)
if err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
-51
View File
@@ -1,51 +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
}
//Compress impelements compression.Compress
func (z *Zstd) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w, err := zstd.NewWriter(&buf, zstd.WithEncoderLevel(zstd.EncoderLevel(z.CompressionLevel)))
if err != nil {
return nil, err
}
_, err = w.Write(data)
if err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
+235
View File
@@ -0,0 +1,235 @@
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
fileSize uint64
}
func NewFullReader(r io.ReaderAt, start uint64, d decompress.Decompressor, blockSizes []uint32, blockSize uint32, fileSize uint64) *FullReader {
return &FullReader{
r: r,
start: start,
blockSize: blockSize,
sizes: blockSizes,
d: d,
fileSize: fileSize,
}
}
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 {
outSize := r.blockSize
if r.fileSize < uint64(r.blockSize) {
outSize = uint32(r.fileSize)
}
out <- outDat{
i: index,
err: nil,
data: make([]byte, outSize),
}
return
}
// rdr := io.LimitReader(toreader.NewReader(r.r, offset), int64(size))
if size == r.sizes[index] {
if dec, ok := r.d.(decompress.Decoder); ok {
dat = make([]byte, size)
_, err = r.r.ReadAt(dat, offset)
if err == nil {
dat, err = dec.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) ReadAt(p []byte, off int64) (n int, err error) {
out := make(chan outDat, len(r.sizes))
offset := r.start
num := len(r.sizes)
start := off / int64(r.blockSize)
end := len(p) / int(r.blockSize)
if end%int(r.blockSize) > 0 {
end++
}
if end > len(r.sizes) {
if r.fragRdr != nil {
end = len(r.sizes)
} else {
end = len(r.sizes) + 1
}
}
for i := 0; i < num; i++ {
if i < int(start) || i > end {
offset += uint64(realSize(r.sizes[i]))
continue
}
if i == num-1 && r.fragRdr != nil {
go func() {
rdr, e := r.fragRdr()
if e != 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)
for cur := start; cur < int64(end); {
dat := <-out
if dat.err != nil {
err = dat.err
return
}
if dat.i != int(cur) {
cache[dat.i] = dat
continue
}
if cur == start {
dat.data = dat.data[off%int64(r.blockSize):]
}
for i := range dat.data {
p[n+i] = dat.data[i]
}
n += len(dat.data)
cur++
var ok bool
for {
dat, ok = cache[int(cur)]
if !ok {
break
}
for i := range dat.data {
p[n+i] = dat.data[i]
}
n += len(dat.data)
cur++
delete(cache, int(cur))
}
}
if n < len(p) {
err = io.EOF
}
return
}
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 rs, ok := r.d.(decompress.Resetable); ok {
if r.comRdr == nil {
r.cur, err = r.d.Reader(r.cur)
if err != nil {
return
}
} else {
err = rs.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
}
+17
View File
@@ -0,0 +1,17 @@
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) Reset(old, src io.Reader) error {
return old.(zlib.Resetter).Reset(src, nil)
}
+22
View File
@@ -0,0 +1,22 @@
package decompress
import (
"io"
)
type Decompressor interface {
//Creates a new decompressor reading from src.
Reader(src io.Reader) (io.ReadCloser, error)
}
type Resetable interface {
//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
}
type Decoder interface {
//Decodes a chunk of data all at once.
Decode(in []byte) ([]byte, error)
}
+18
View File
@@ -0,0 +1,18 @@
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) Reset(old, src io.Reader) error {
old.(*lz4.Reader).Reset(src)
return nil
}
+14
View File
@@ -0,0 +1,14 @@
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
}
+18
View File
@@ -0,0 +1,18 @@
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
}
+18
View File
@@ -0,0 +1,18 @@
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) Reset(old, src io.Reader) error {
return old.(*xz.Reader).Reset(src)
}
+27
View File
@@ -0,0 +1,27 @@
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) Reset(old, src io.Reader) error {
return old.(*zstd.Decoder).Reset(src)
}
func (z Zstd) Decode(in []byte) ([]byte, error) {
if z.writeToReader == nil {
z.writeToReader, _ = zstd.NewReader(nil)
}
return z.writeToReader.DecodeAll(in, nil)
}
+61 -68
View File
@@ -6,82 +6,75 @@ 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
}
//EntryRaw is the values that can be easily decoded
type EntryRaw 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 {
Name string
InodeOffset uint32
InodeBlockOffset uint16
Type uint16
Name string
BlockStart uint32
Type uint16
Offset uint16
Num uint32
}
//NewEntry creates a new directory entry
func NewEntry(rdr io.Reader) (*Entry, error) {
var raw EntryRaw
err := binary.Read(rdr, binary.LittleEndian, &raw)
func readEntry(r io.Reader) (e entry, err error) {
err = binary.Read(r, binary.LittleEndian, &e.entryInit)
if err != nil {
return nil, err
}
tmp := make([]byte, raw.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
return &Entry{
InodeBlockOffset: raw.Offset,
Type: raw.Type,
Name: string(tmp),
}, nil
}
//NewDirectory reads the directory from rdr
func NewDirectory(base io.Reader, size uint32) (entries []*Entry, err error) {
tmp := make([]byte, size)
base.Read(tmp)
rdr := bytes.NewBuffer(tmp)
for {
var hdr Header
err = binary.Read(rdr, binary.LittleEndian, &hdr)
if err == io.ErrUnexpectedEOF {
err = nil
break
} else if err != nil {
return nil, err
}
hdr.Count++
headers := hdr.Count / 256
if hdr.Count%256 > 0 {
headers++
}
for i := uint32(0); i < hdr.Count; i++ {
if i != 0 && i%256 == 0 {
err = binary.Read(rdr, binary.LittleEndian, &hdr)
if err != nil {
return nil, err
}
}
var ent *Entry
ent, err = NewEntry(rdr)
if err != nil {
return nil, err
}
ent.InodeOffset = hdr.InodeOffset
entries = append(entries, ent)
}
return
}
e.Name = make([]byte, e.NameSize+1)
err = binary.Read(r, binary.LittleEndian, &e.Name)
return
}
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 {
err = binary.Read(r, binary.LittleEndian, &h)
if err == io.EOF {
err = nil
return
} else if err != nil {
return
}
h.Entries++
for i := 0; i < int(h.Entries); i++ {
if i != 0 && i%256 == 0 {
err = binary.Read(r, binary.LittleEndian, &h)
if err != nil {
return
}
}
en, err = readEntry(r)
if err != nil {
return
}
e = append(e, Entry{
Name: string(en.Name),
BlockStart: h.InodeStart,
Type: en.Type,
Offset: en.Offset,
Num: h.Num + uint32(en.NumOffset),
})
}
}
}
+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
}
+143
View File
@@ -0,0 +1,143 @@
package inode
import (
"encoding/binary"
"errors"
"io"
"io/fs"
"strconv"
)
const (
Dir = uint16(iota + 1)
Fil
Sym
Block
Char
Fifo
Sock
EDir
EFil
ESym
EBlock
EChar
EFifo
ESock
)
type Header struct {
Type uint16
Perm uint16
UidInd uint16
GidInd uint16
ModTime uint32
Num uint32
}
type Inode struct {
Header
Data any
}
func Read(r io.Reader, blockSize uint32) (i Inode, err error) {
err = binary.Read(r, binary.LittleEndian, &i.Header)
if err != nil {
return
}
switch i.Type {
case Dir:
i.Data, err = ReadDir(r)
case Fil:
i.Data, err = ReadFile(r, blockSize)
case Sym:
i.Data, err = ReadSym(r)
case Block:
fallthrough
case Char:
i.Data, err = ReadDevice(r)
case Fifo:
fallthrough
case Sock:
i.Data, err = ReadIPC(r)
case EDir:
i.Data, err = ReadEDir(r)
case EFil:
i.Data, err = ReadEFile(r, blockSize)
case ESym:
i.Data, err = ReadESym(r)
case EBlock:
fallthrough
case EChar:
i.Data, err = ReadEDevice(r)
case EFifo:
fallthrough
case ESock:
i.Data, err = ReadEIPC(r)
default:
return i, errors.New("invalid inode type " + strconv.Itoa(int(i.Type)))
}
return
}
func (i Inode) Mode() (out fs.FileMode) {
out = fs.FileMode(i.Perm)
switch i.Data.(type) {
case Directory:
out |= fs.ModeDir
case EDirectory:
out |= fs.ModeDir
case Symlink:
out |= fs.ModeSymlink
case ESymlink:
out |= fs.ModeSymlink
case Device:
out |= fs.ModeDevice
case EDevice:
out |= fs.ModeDevice
case IPC:
out |= fs.ModeNamedPipe
case EIPC:
out |= fs.ModeNamedPipe
}
return
}
func (i Inode) LinkCount() uint32 {
switch i.Data.(type) {
case EFile:
return i.Data.(EFile).LinkCount
case Directory:
return i.Data.(Directory).LinkCount
case EDirectory:
return i.Data.(EDirectory).LinkCount
case Device:
return i.Data.(Device).LinkCount
case EDevice:
return i.Data.(EDevice).LinkCount
case IPC:
return i.Data.(IPC).LinkCount
case EIPC:
return i.Data.(EIPC).LinkCount
case Symlink:
return i.Data.(Symlink).LinkCount
case ESymlink:
return i.Data.(ESymlink).LinkCount
default:
return 0
}
}
func (i Inode) Size() uint64 {
switch i.Data.(type) {
case File:
return uint64(i.Data.(File).Size)
case EFile:
return i.Data.(EFile).Size
// case Directory:
// return uint64(i.Data.(Directory).Size)
// case EDirectory:
// return uint64(i.Data.(EDirectory).Size)
default:
return 0
}
}
-257
View File
@@ -1,257 +0,0 @@
package inode
import (
"encoding/binary"
"io"
)
//The different types of inodes as defined by inodetype
const (
DirType = iota + 1
FileType
SymType
BlockDevType
CharDevType
FifoType
SocketType
ExtDirType
ExtFileType
ExtSymType
ExtBlockDeviceType
ExtCharDeviceType
ExtFifoType
ExtSocketType
)
//Header is the common header for all inodes
type Header struct {
Type uint16
Permissions uint16
UID uint16
GID uint16
ModifiedTime uint32
Number uint32
}
//Dir is self explainatory
type Dir struct {
DirectoryIndex uint32
HardLinks uint32
DirectorySize uint16
DirectoryOffset uint16
ParentInodeNumber uint32
}
//ExtDirInit is the information that can be directoy decoded
type ExtDirInit struct {
HardLinks uint32
DirectorySize uint32
DirectoryIndex uint32
ParentInodeNumber uint32
IndexCount uint16 //one less then directory indexes following structure
DirectoryOffset uint16
XattrIndex uint32
}
//ExtDir is a directory with extra info
type ExtDir struct {
Indexes []DirIndex
ExtDirInit
}
//NewExtendedDirectory creates a new ExtendedDirectory
func NewExtendedDirectory(rdr io.Reader) (ExtDir, error) {
var inode ExtDir
err := binary.Read(rdr, binary.LittleEndian, &inode.ExtDirInit)
if err != nil {
return inode, err
}
for i := uint16(0); i < inode.IndexCount; i++ {
var tmp DirIndex
tmp, err = NewDirectoryIndex(rdr)
if err != nil {
return inode, err
}
inode.Indexes = append(inode.Indexes, tmp)
}
return inode, err
}
//DirIndexInit holds the values that can be easily decoded
type DirIndexInit struct {
Offset uint32
DirTableOffset uint32
NameSize uint32
}
//DirIndex is a quick lookup provided by an ExtendedDirectory
type DirIndex struct {
Name string
DirIndexInit
}
//NewDirectoryIndex return a new DirectoryIndex
func NewDirectoryIndex(rdr io.Reader) (DirIndex, error) {
var index DirIndex
err := binary.Read(rdr, binary.LittleEndian, &index.DirIndexInit)
if err != nil {
return index, err
}
tmp := make([]byte, index.NameSize+1, index.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return index, err
}
index.Name = string(tmp)
return index, nil
}
//FileInit is the information that can be directly decoded
type FileInit struct {
BlockStart uint32
FragmentIndex uint32
FragmentOffset uint32
Size uint32
}
//File is self explainatory
type File struct {
BlockSizes []uint32
Fragmented bool
FileInit
}
//NewFile creates a new File
func NewFile(rdr io.Reader, blockSize uint32) (File, error) {
var inode File
err := binary.Read(rdr, binary.LittleEndian, &inode.FileInit)
if err != nil {
return inode, err
}
inode.Fragmented = inode.FragmentIndex != 0xFFFFFFFF
blocks := inode.Size / blockSize
if inode.Size%blockSize > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//ExtFileInit is the information that can be directly decoded
type ExtFileInit struct {
BlockStart uint64
Size uint64
Sparse uint64
HardLinks uint32
FragmentIndex uint32
FragmentOffset uint32
XattrIndex uint32
}
//ExtFile is a file with more information
type ExtFile struct {
BlockSizes []uint32
Fragmented bool
ExtFileInit
}
//NewExtendedFile creates a new ExtendedFile
func NewExtendedFile(rdr io.Reader, blockSize uint32) (ExtFile, error) {
var inode ExtFile
err := binary.Read(rdr, binary.LittleEndian, &inode.ExtFileInit)
if err != nil {
return inode, err
}
inode.Fragmented = inode.FragmentIndex != 0xFFFFFFFF
blocks := inode.Size / uint64(blockSize)
if inode.Size%uint64(blockSize) > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//SymInit is all the values that can be directly decoded
type SymInit struct {
HardLinks uint32
TargetPathSize uint32
}
//Sym is a symlink
type Sym struct {
Path string
targetPath []byte //len is TargetPathSize
SymInit
}
//NewSymlink creates a new Symlink
func NewSymlink(rdr io.Reader) (Sym, error) {
var inode Sym
err := binary.Read(rdr, binary.LittleEndian, &inode.SymInit)
if err != nil {
return inode, err
}
inode.targetPath = make([]byte, inode.TargetPathSize, inode.TargetPathSize)
err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath)
if err != nil {
return inode, err
}
inode.Path = string(inode.targetPath)
return inode, err
}
//ExtSymInit is all the values that can be directly decoded
type ExtSymInit struct {
HardLinks uint32
TargetPathSize uint32
}
//ExtSym is a symlink with extra information
type ExtSym struct {
Path string
targetPath []uint8
ExtSymInit
XattrIndex uint32
}
//NewExtendedSymlink creates a new ExtendedSymlink
func NewExtendedSymlink(rdr io.Reader) (ExtSym, error) {
var inode ExtSym
err := binary.Read(rdr, binary.LittleEndian, &inode.ExtSymInit)
if err != nil {
return inode, err
}
inode.targetPath = make([]uint8, inode.TargetPathSize, inode.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
}
//Device is a device
type Device struct {
HardLinks uint32
Device uint32
}
//ExtDevice is a device with more info
type ExtDevice struct {
Device
XattrIndex uint32
}
//IPC is a Fifo or Socket device
type IPC struct {
HardLink uint32
}
//ExtIPC is a IPC device with extra info
type ExtIPC struct {
IPC
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
}
-128
View File
@@ -1,128 +0,0 @@
package inode
import (
"encoding/binary"
"errors"
"io"
"strconv"
)
//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 {
Info interface{} //Info is the parsed specific data. It's type is defined by Type.
Header
}
//ProcessInode tries to read an inode from the BlockReader
func ProcessInode(br io.Reader, blockSize uint32) (*Inode, error) {
var in Inode
err := binary.Read(br, binary.LittleEndian, &in.Header)
if err != nil {
return nil, err
}
switch in.Type {
case DirType:
var inode Dir
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case FileType:
var inode File
inode, err = NewFile(br, blockSize)
if err != nil {
return nil, err
}
in.Info = inode
case SymType:
var inode Sym
inode, err = NewSymlink(br)
if err != nil {
return nil, err
}
in.Info = inode
case BlockDevType:
var inode Device
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case CharDevType:
var inode Device
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case FifoType:
var inode IPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case SocketType:
var inode IPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case ExtDirType:
var inode ExtDir
inode, err = NewExtendedDirectory(br)
if err != nil {
return nil, err
}
in.Info = inode
case ExtFileType:
var inode ExtFile
inode, err = NewExtendedFile(br, blockSize)
if err != nil {
return nil, err
}
in.Info = inode
case ExtSymType:
var inode ExtSym
inode, err = NewExtendedSymlink(br)
if err != nil {
return nil, err
}
in.Info = inode
case ExtBlockDeviceType:
var inode ExtDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case ExtCharDeviceType:
var inode ExtDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case ExtFifoType:
var inode ExtIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
case ExtSocketType:
var inode ExtIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
in.Info = inode
default:
return nil, errors.New("Unsupported inode type: " + strconv.Itoa(int(in.Type)))
}
return &in, 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 _, ok := r.d.(decompress.Resetable); !ok {
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 rs, ok := r.d.(decompress.Resetable); ok {
if r.comRdr == nil {
r.cur, err = r.d.Reader(r.cur)
if err != nil {
return
}
} else {
err = rs.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
}
+23
View File
@@ -0,0 +1,23 @@
package threadmanager
type Manager struct {
c chan int
}
func NewManager(maxRoutines int) *Manager {
m := &Manager{
c: make(chan int, maxRoutines),
}
for i := 0; i < maxRoutines; i++ {
m.c <- i
}
return m
}
func (m *Manager) Lock() int {
return <-m.c
}
func (m *Manager) Unlock(n int) {
m.c <- n
}
+19
View File
@@ -0,0 +1,19 @@
package toreader
import "io"
type OffsetReader struct {
r io.ReaderAt
off int64
}
func NewOffsetReader(r io.ReaderAt, off int64) *OffsetReader {
return &OffsetReader{
r: r,
off: off,
}
}
func (r OffsetReader) ReadAt(p []byte, off int64) (n int, e error) {
return r.r.ReadAt(p, off+r.off)
}
+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
}
+24
View File
@@ -0,0 +1,24 @@
package toreader
import "io"
type ReaderAt struct {
d []byte
}
func NewReaderAt(r io.Reader) (ra *ReaderAt, err error) {
ra = new(ReaderAt)
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
headers []*metadata
data []byte
offset int64
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
}
+189 -134
View File
@@ -7,173 +7,228 @@ import (
"math"
"time"
"github.com/CalebQ42/squashfs/internal/compression"
"github.com/CalebQ42/fuse"
"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"
fuse2 "github.com/seaweedfs/fuse"
)
const (
magic uint32 = 0x73717368
)
type Reader struct {
*FS
con *fuse.Conn
con2 *fuse2.Conn
mountDone chan struct{}
mount2Done chan struct{}
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")
)
//TODO: implement fs.FS, possibly more FS types for compatibility. Most of this work will mostly be handed off to root anyway so this shouldn't be too difficult.
// The types of compression supported by squashfs
const (
GZipCompression = uint16(iota + 1)
LZMACompression
LZOCompression
XZCompression
LZ4Compression
ZSTDCompression
)
//Reader processes and reads a squashfs archive.
type Reader struct {
FS
r *io.SectionReader
decompressor compression.Decompressor
root *File
fragOffsets []uint64
idTable []uint32
super superblock
flags SuperblockFlags
func NewReaderAtOffset(r io.ReaderAt, off int64) (*Reader, error) {
return NewReader(toreader.NewOffsetReader(r, off))
}
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
var rdr Reader
err := binary.Read(io.NewSectionReader(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
}
rdr.r = io.NewSectionReader(r, 0, int64(rdr.super.BytesUsed))
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")
if !squash.s.checkMagic() {
return nil, ErrorMagic
}
rdr.r.Seek(96, io.SeekStart)
hasUnsupportedOptions := false
rdr.flags = rdr.super.GetFlags()
if rdr.flags.compressorOptions {
switch rdr.super.CompressionType {
case GzipCompression:
var gzip *compression.Gzip
gzip, err = compression.NewGzipCompressorWithOptions(rdr.r)
if err != nil {
return nil, err
}
if gzip.HasCustomWindow || gzip.HasStrategies {
hasUnsupportedOptions = true
}
rdr.decompressor = gzip
case XzCompression:
var xz *compression.Xz
xz, err = compression.NewXzCompressorWithOptions(rdr.r)
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:
var lz4 *compression.Lz4
lz4, err = compression.NewLz4CompressorWithOptions(rdr.r)
if err != nil {
return nil, err
}
rdr.decompressor = lz4
case ZstdCompression:
var zstd *compression.Zstd
zstd, err = compression.NewZstdCompressorWithOptions(rdr.r)
if err != nil {
return nil, err
}
rdr.decompressor = zstd
default:
return nil, errIncompatibleCompression
}
} 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.checkBlockLog() {
return nil, ErrorLog
}
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.checkVersion() {
return nil, ErrorVersion
}
unread := rdr.super.IDCount
blockOffsets := make([]uint64, int(math.Ceil(float64(rdr.super.IDCount)/2048)))
rdr.r.Seek(int64(rdr.super.IDTableStart), io.SeekStart)
for i := range blockOffsets {
err = binary.Read(rdr.r, binary.LittleEndian, &blockOffsets[i])
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
}
var idRdr *metadataReader
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)
} 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
}
}
unread -= read
}
metaRdr, err := rdr.newMetadataReaderFromInodeRef(rdr.super.RootInodeRef)
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, err
}
} 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
}
offset = int(squash.s.IdCount - toRead)
for i := range tmp {
squash.ids[offset+i] = tmp[i]
}
toRead -= curRead
}
}
}
root, err := squash.inodeFromRef(squash.s.RootInodeRef)
if err != nil {
return nil, err
}
i, err := inode.ProcessInode(metaRdr, rdr.super.BlockSize)
rootEnts, err := squash.readDirectory(root)
if err != nil {
return nil, err
}
entries, err := rdr.readDirFromInode(i)
if err != nil {
return nil, err
enType := root.Type
if enType == inode.EDir {
enType = inode.Dir
}
rdr.FS = FS{
r: &rdr,
name: "/",
entries: entries,
squash.FS = &FS{
e: rootEnts,
File: &File{
rdr: &squash,
i: root,
e: directory.Entry{
Name: "",
Type: enType,
},
r: &squash,
},
}
if hasUnsupportedOptions {
return &rdr, ErrOptions
}
return &rdr, nil
return &squash, nil
}
//ModTime is the last time the file was modified/created.
func (r *Reader) ModTime() time.Time {
return time.Unix(int64(r.super.CreationTime), 0)
// 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
// }
// 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)
}
+518
View File
@@ -0,0 +1,518 @@
package squashfs
import (
"errors"
"io"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/CalebQ42/squashfs/internal/data"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/threadmanager"
)
// File represents a file inside a squashfs archive.
type File struct {
i inode.Inode
rdr io.Reader
fullRdr *data.FullReader
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 *data.FullReader
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
}
// Mode returns the file's fs.FileMode
func (f File) Mode() fs.FileMode {
switch f.e.Type {
case inode.Dir:
return fs.FileMode(f.i.Perm) | fs.ModeDir
case inode.Char:
return fs.FileMode(f.i.Perm) | fs.ModeCharDevice
case inode.Block:
return fs.FileMode(f.i.Perm) | fs.ModeDevice
case inode.Sym:
return fs.FileMode(f.i.Perm) | fs.ModeSymlink
}
return fs.FileMode(f.i.Perm)
}
// 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)
}
func (f File) ReadAt(p []byte, off int64) (int, error) {
if f.i.Type != inode.Fil && f.i.Type != inode.EFil {
return 0, ErrReadNotFile
}
return f.fullRdr.ReadAt(p, off)
}
// 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) {
if f.i.Type != inode.Fil && f.i.Type != inode.EFil {
return 0, ErrReadNotFile
}
return f.fullRdr.WriteTo(w)
}
// Close simply nils the underlying reader.
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
}
func (f File) isDeviceOrFifo() bool {
return f.i.Type == inode.Char || f.i.Type == inode.Block || f.i.Type == inode.EChar || f.i.Type == inode.EBlock || f.i.Type == inode.Fifo || f.i.Type == inode.EFifo
}
func (f File) deviceDevices() (maj uint32, min uint32) {
var dev uint32
if f.i.Type == inode.Char || f.i.Type == inode.Block {
dev = f.i.Data.(inode.Device).Dev
} else if f.i.Type == inode.EChar || f.i.Type == inode.EBlock {
dev = f.i.Data.(inode.EDevice).Dev
}
return dev >> 8, dev & 0x000FF
}
// 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 {
manager *threadmanager.Manager
LogOutput io.Writer //Where error log should write.
DereferenceSymlink bool //Replace symlinks with the target file.
UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink.
Verbose bool //Prints extra info to log on an error.
IgnorePerm bool //Ignore file's permissions and instead use Perm.
Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0755.
notFirst bool
}
func DefaultOptions() *ExtractionOptions {
return &ExtractionOptions{
Perm: 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.realExtract(folder, DefaultOptions())
}
// ExtractVerbose extracts the File to the folder with the Verbose option.
func (f File) ExtractVerbose(folder string) error {
op := DefaultOptions()
op.Verbose = true
return f.realExtract(folder, op)
}
// ExtractIgnorePermissions extracts the File to the folder with the IgnorePerm option.
func (f File) ExtractIgnorePermissions(folder string) error {
op := DefaultOptions()
op.IgnorePerm = true
return f.realExtract(folder, op)
}
// 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 {
op := DefaultOptions()
op.DereferenceSymlink = true
return f.realExtract(folder, op)
}
// 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 && op.LogOutput != nil {
log.SetOutput(op.LogOutput)
}
return f.realExtract(folder, op)
}
func (f File) realExtract(folder string, op *ExtractionOptions) (err error) {
if op.manager == nil {
op.manager = threadmanager.NewManager(runtime.NumCPU())
}
extDir := folder + "/" + f.e.Name
if !op.notFirst {
op.notFirst = true
if f.IsDir() {
extDir = folder
_, err = os.Open(folder)
if err != nil && os.IsNotExist(err) {
err = os.Mkdir(extDir, op.Perm)
}
if err != nil {
if op.Verbose {
log.Println("Error while making", folder)
}
return
}
if !op.IgnorePerm {
defer os.Chmod(extDir, f.Mode())
defer os.Chown(extDir, int(f.r.ids[f.i.UidInd]), int(f.r.ids[f.i.GidInd]))
}
}
}
switch {
case f.IsDir():
if folder != extDir && f.e.Name != "" {
//First extract it with a permisive permission.
err = os.Mkdir(extDir, op.Perm)
if err != nil {
if op.Verbose {
log.Println("Error while making directory", extDir)
}
return
}
//Then set it to it's actual permissions once we're done with it
if !op.IgnorePerm {
defer os.Chmod(extDir, f.Mode())
defer os.Chown(extDir, int(f.r.ids[f.i.UidInd]), int(f.r.ids[f.i.GidInd]))
}
}
var filFS *FS
filFS, err = f.FS()
if err != nil {
if op.Verbose {
log.Println("Error while converting", f.path(), "to FS")
}
return err
}
errChan := make(chan error, len(filFS.e))
files := make([]directory.Entry, 0)
//Focus on making the folder tree first...
var i int
for i = 0; i < len(filFS.e); i++ {
if filFS.e[i].Type == inode.Fil {
files = append(files, filFS.e[i])
} else {
go func(index int) {
subF, goErr := f.r.newFile(filFS.e[index], filFS)
if goErr != nil {
if op.Verbose {
log.Println("Error while resolving", extDir)
}
errChan <- goErr
return
}
errChan <- subF.ExtractWithOptions(extDir, op)
}(i)
}
}
for i = 0; i < len(filFS.e)-len(files); i++ {
err = <-errChan
if err != nil {
return err
}
}
//Then we extract the files.
for i = 0; i < len(files); i++ {
go func(index int) {
n := op.manager.Lock()
defer op.manager.Unlock(n)
subF, goErr := f.r.newFile(files[index], filFS)
if goErr != nil {
if op.Verbose {
log.Println("Error while resolving", extDir)
}
errChan <- goErr
return
}
errChan <- subF.ExtractWithOptions(extDir, op)
}(i)
}
for i = 0; i < len(files); i++ {
err = <-errChan
if err != nil {
return err
}
}
case f.IsRegular():
var fil *os.File
fil, err = os.Create(extDir)
if os.IsExist(err) {
os.Remove(extDir)
fil, err = os.Create(extDir)
if err != nil {
if op.Verbose {
log.Println("Error while creating", extDir)
}
return err
}
} else if err != nil {
if op.Verbose {
log.Println("Error while creating", extDir)
}
return err
}
defer fil.Close()
_, err = io.Copy(fil, f)
if err != nil {
if op.Verbose {
log.Println("Error while copying data to", extDir)
}
return err
}
if op.IgnorePerm {
os.Chmod(extDir, op.Perm|(f.Mode()&fs.ModeType))
} else {
os.Chmod(extDir, f.Mode())
os.Chown(extDir, int(f.r.ids[f.i.UidInd]), int(f.r.ids[f.i.GidInd]))
}
case f.IsSymlink():
symPath := f.SymlinkPath()
if op.DereferenceSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", extDir)
}
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:", extDir)
}
return err
}
return nil
} else if op.UnbreakSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", extDir)
}
return errors.New("cannot get symlink target")
}
extractLoc := filepath.Join(folder, filepath.Dir(symPath))
err = fil.realExtract(extractLoc, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting ", extDir)
}
return err
}
}
err = os.Symlink(f.SymlinkPath(), extDir)
if os.IsExist(err) {
os.Remove(extDir)
err = os.Symlink(f.SymlinkPath(), extDir)
}
if err != nil {
if op.Verbose {
log.Println("Error while making symlink:", extDir)
}
return err
}
if op.IgnorePerm {
os.Chmod(extDir, op.Perm|(f.Mode()&fs.ModeType))
} else {
os.Chmod(extDir, f.Mode())
os.Chown(extDir, int(f.r.ids[f.i.UidInd]), int(f.r.ids[f.i.GidInd]))
}
case f.isDeviceOrFifo():
if runtime.GOOS == "windows" {
if op.Verbose {
log.Println(extDir, "ignored since it's a device link and can't be created on Windows.")
}
return nil
}
_, err = exec.LookPath("mknod")
if err != nil {
if op.Verbose {
log.Println("Extracting Fifo IPC or Device and mknod is not in PATH")
}
return err
}
var typ string
if f.i.Type == inode.Char || f.i.Type == inode.EChar {
typ = "c"
} else if f.i.Type == inode.Block || f.i.Type == inode.EBlock {
typ = "b"
} else { //Fifo IPC
if runtime.GOOS == "darwin" {
if op.Verbose {
log.Println(extDir, "ignored since it's a Fifo file and can't be created on Darwin.")
}
return nil
}
typ = "p"
}
cmd := exec.Command("mknod", extDir, typ)
if typ != "p" {
maj, min := f.deviceDevices()
cmd.Args = append(cmd.Args, strconv.Itoa(int(maj)), strconv.Itoa(int(min)))
}
if op.Verbose {
cmd.Stdout = op.LogOutput
cmd.Stderr = op.LogOutput
}
err = cmd.Run()
if err != nil {
if op.Verbose {
log.Println("Error while running mknod for", extDir)
}
return err
}
if op.IgnorePerm {
os.Chmod(extDir, op.Perm|(f.Mode()&fs.ModeType))
} else {
os.Chmod(extDir, f.Mode())
os.Chown(extDir, int(f.r.ids[f.i.UidInd]), int(f.r.ids[f.i.GidInd]))
}
case f.e.Type == inode.Sock:
if op.Verbose {
log.Println(extDir, "ignored since it's a socket file.")
}
default:
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.i.Type)))
}
return nil
}
+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)
}
+380
View File
@@ -0,0 +1,380 @@
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
}
// Opens the file at name. Returns a squashfs.File.
func (f FS) OpenFile(name string) (*File, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return f.File, nil
}
split := strings.Split(name, "/")
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.OpenFile(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,
}
}
// Opens the file at name. Returns a io/fs.File.
func (f FS) Open(name string) (fs.File, error) {
return f.OpenFile(name)
}
// Glob returns the name of the files at the given pattern.
// All paths are relative to the FS.
// Uses filepath.Match to compare names.
func (f FS) Glob(pattern string) (out []string, err error) {
pattern = filepath.Clean(pattern)
if !fs.ValidPath(pattern) {
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: fs.ErrInvalid,
}
}
split := strings.Split(pattern, "/")
for i := 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) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return f.File.ReadDir(-1)
}
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) {
name = filepath.Clean(strings.TrimPrefix(name, "/"))
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: fs.ErrInvalid,
}
}
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) {
dir = filepath.Clean(dir)
if !fs.ValidPath(dir) {
return nil, &fs.PathError{
Op: "sub",
Path: dir,
Err: fs.ErrInvalid,
}
}
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,
}
}
+117
View File
@@ -0,0 +1,117 @@
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
var fileSize uint64
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
fileSize = uint64(i.Data.(inode.File).Size)
} 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))
fileSize = i.Data.(inode.EFile).Size
} 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, fileSize)
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)
}
-223
View File
@@ -1,223 +0,0 @@
package squashfs
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strconv"
"testing"
"time"
goappimage "github.com/CalebQ42/GoAppImage"
)
const (
downloadURL = "https://github.com/srevinsaju/Firefox-Appimage/releases/download/firefox-v84.0.r20201221152838/firefox-84.0.r20201221152838-x86_64.AppImage"
appImageName = "firefox-84.0.r20201221152838-x86_64.AppImage"
squashfsName = "balenaEtcher-1.5.113-x64.AppImage.sfs"
)
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)
}
fmt.Println("stuff", rdr.super.CompressionType)
// fil := rdr.GetFileAtPath("*.desktop")
// if fil == nil {
// t.Fatal("Can't find desktop fil")
// }
// errs := fil.ExtractTo(wd + "/testing")
// if len(errs) > 0 {
// t.Fatal(errs)
// }
// errs = rdr.ExtractTo(wd + "/testing/" + squashfsName + ".d")
// if len(errs) > 0 {
// t.Fatal(errs)
// }
t.Fatal("No Problems")
}
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) {
err = downloadTestAppImage(wd + "/testing")
if err != nil {
t.Fatal(err)
}
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)
}
os.RemoveAll(wd + "/testing/firefox")
err = rdr.ExtractTo(wd + "/testing/firefox")
t.Fatal(err)
}
func TestUnsquashfs(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) {
err = downloadTestAppImage(wd + "/testing")
if err != nil {
t.Fatal(err)
}
aiFil, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
os.RemoveAll(wd + "/testing/unsquashFirefox")
os.RemoveAll(wd + "/testing/firefox")
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
fmt.Println("Command:", "unsquashfs", "-d", wd+"/testing/unsquashFirefox", "-o", strconv.Itoa(int(ai.Offset)), aiFil.Name())
cmd := exec.Command("unsquashfs", "-d", wd+"/testing/unsquashFirefox", "-o", strconv.Itoa(int(ai.Offset)), aiFil.Name())
start := time.Now()
err = cmd.Run()
if err != nil {
t.Fatal(err)
}
fmt.Println(time.Since(start))
t.Fatal("HI")
}
func BenchmarkDragRace(b *testing.B) {
wd, err := os.Getwd()
if err != nil {
b.Fatal(err)
}
aiFil, err := os.Open(wd + "/testing/" + appImageName)
if os.IsNotExist(err) {
err = downloadTestAppImage(wd + "/testing")
if err != nil {
b.Fatal(err)
}
aiFil, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
b.Fatal(err)
}
} else if err != nil {
b.Fatal(err)
}
stat, _ := aiFil.Stat()
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
os.RemoveAll(wd + "/testing/unsquashFirefox")
os.RemoveAll(wd + "/testing/firefox")
cmd := exec.Command("unsquashfs", "-d", wd+"/testing/unsquashFirefox", "-o", strconv.Itoa(int(ai.Offset)), aiFil.Name())
start := time.Now()
err = cmd.Run()
if err != nil {
b.Fatal(err)
}
unsquashTime := time.Since(start)
start = time.Now()
rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset))
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractTo(wd + "/testing/firefox")
if err != nil {
b.Fatal(err)
}
libTime := time.Since(start)
b.Log("Unsqushfs:", unsquashTime.Round(time.Millisecond))
b.Log("Library:", libTime.Round(time.Millisecond))
b.Log("unsquashfs is", strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64)+"x faster")
}
func downloadTestAppImage(dir string) error {
//seems to time out on slow connections. Might fix that at some point... or not. It's just a test...
os.Mkdir(dir, os.ModePerm)
appImage, err := os.Create(dir + "/" + appImageName)
if err != nil {
return err
}
defer appImage.Close()
check := http.Client{
CheckRedirect: func(r *http.Request, _ []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
resp, err := check.Get(downloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(appImage, resp.Body)
if err != nil {
return err
}
return nil
}
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) {
err = downloadTestAppImage(wd + "/testing")
if err != nil {
t.Fatal(err)
}
_, 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)
}
}
+207
View File
@@ -0,0 +1,207 @@
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/files/LinuxPATest.sfs"
squashfsName = "bug.sqfs"
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 TestMisc(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
rdr, err := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
_ = rdr
// Put testing here
t.Fatal("UM")
}
func BenchmarkRace(b *testing.B) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
b.Fatal(err)
}
libPath := filepath.Join(tmpDir, "ExtractLib")
unsquashPath := filepath.Join(tmpDir, "ExtractSquashfs")
os.RemoveAll(libPath)
os.RemoveAll(unsquashPath)
var libTime, unsquashTime time.Duration
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)
}
os.RemoveAll(filepath.Join(tmpDir, "testLog.txt"))
logFil, _ := os.Create(filepath.Join(tmpDir, "testLog.txt"))
op := squashfs.DefaultOptions()
op.Verbose = true
op.IgnorePerm = true
op.LogOutput = logFil
err = rdr.ExtractWithOptions(libPath, op)
if err != nil {
t.Fatal(err)
}
cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name())
err = cmd.Run()
if err != nil {
t.Fatal(err)
}
//Then compare the sizes and existance between the two (using unsquashfs as a reference).
//If the file doesn't exist, or the size is different, we exit.
//TODO: Add long test that checks contents.
squashFils := os.DirFS(unsquashPath)
err = fs.WalkDir(squashFils, ".", func(path string, 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)
}
err = f.(*squashfs.File).ExtractWithOptions("testing", &squashfs.ExtractionOptions{Verbose: true})
if err != nil {
t.Fatal(err)
}
t.Fatal("HI")
}
func TestFuse(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)
}
err = rdr.Mount("testing/fuseTest")
if err != nil {
t.Fatal(err)
}
defer rdr.Unmount()
rdr.MountWait()
t.Fatal("testing")
}
+55 -101
View File
@@ -1,31 +1,22 @@
package squashfs
//The types of compression supported by 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
@@ -33,94 +24,57 @@ type superblock struct {
ExportTableStart uint64
}
//SuperblockFlags is the series of flags describing how a squashfs archive is packed.
type SuperblockFlags struct {
//If true, inodes are stored uncompressed.
UncompressedInodes bool
//If true, data is stored uncompressed.
UncompressedData bool
check bool
//If true, fragments are stored uncompressed.
UncompressedFragments bool
//If true, ALL data is stored in sequential data blocks instead of utilizing fragments.
NoFragments bool
//If true, the last block of data will always be stored as a fragment if it's less then the block size.
AlwaysFragment bool
//If true, duplicate files are only stored once. (Currently unsupported)
RemoveDuplicates bool
//If true, the export table is populated. (Currently unsupported)
Exportable bool
//If true, the xattr table is uncompressed. (Currently unsupported)
UncompressedXattr bool
//If true, the xattr table is not populated. (Currently unsupported)
NoXattr bool
compressorOptions bool
//If true, the UID/GID table is stored uncompressed.
UncompressedIDs bool
func (s superblock) checkMagic() bool {
return s.Magic == 0x73717368
}
//DefaultFlags are the default SuperblockFlags that are used.
var DefaultFlags = SuperblockFlags{
RemoveDuplicates: true,
Exportable: true,
func (s superblock) checkBlockLog() bool {
return s.BlockLog == uint16(math.Log2(float64(s.BlockSize)))
}
//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,
AlwaysFragment: s.Flags&0x20 == 0x20,
RemoveDuplicates: 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) checkVersion() bool {
return s.VerMaj == 4 && s.VerMin == 0
}
//ToUint returns the uint16 representation of the given SuperblockFlags
func (s *SuperblockFlags) ToUint() uint16 {
var out uint16
if s.UncompressedInodes {
out = out | 0x1
}
if s.UncompressedData {
out = out | 0x2
}
if s.check {
out = out | 0x4
}
if s.UncompressedFragments {
out = out | 0x8
}
if s.NoFragments {
out = out | 0x10
}
if s.AlwaysFragment {
out = out | 0x20
}
if s.RemoveDuplicates {
out = out | 0x40
}
if s.Exportable {
out = out | 0x80
}
if s.UncompressedXattr {
out = out | 0x100
}
if s.NoXattr {
out = out | 0x200
}
if s.compressorOptions {
out = out | 0x400
}
if s.UncompressedIDs {
out = out | 0x800
}
return out
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
}
-390
View File
@@ -1,390 +0,0 @@
package squashfs
import (
"errors"
"io"
"io/fs"
"log"
"os"
"path"
"sort"
"strings"
"syscall"
"github.com/CalebQ42/squashfs/internal/compression"
)
//Writer is used to creaste squashfs archives. Currently unusable.
//TODO: Make usable
type Writer struct {
compressor compression.Compressor
structure map[string][]*fileHolder
symlinkTable map[string]string
folders []string
uidGUIDTable []int
frags []fragment
superblock superblock
compressionType int
//BlockSize is how large the data blocks are. Can be between 4096 (4KB) and 1048576 (1 MB).
//If BlockSize is not inside that range, it will be set to within the range before writing.
//Default is 1048576.
BlockSize uint32
//Flags are the SuperblockFlags used when writing the archive.
//Currently Duplicates, Exportable, UncompressedXattr, NoXattr values are ignored
Flags SuperblockFlags
allowErrors bool
}
//NewWriter creates a new with the default options (Gzip compression and allow errors)
func NewWriter() (*Writer, error) {
return NewWriterWithOptions(GzipCompression, true)
}
//NewWriterWithOptions creates a new squashfs.Writer with the given options.
//compressionType can be of any types, except LZO (which this library doesn't have support for yet)
//allowErrors determines if, when adding folders, it allows errors encountered with it's sub-directories and instead logs the errors.
func NewWriterWithOptions(compressionType int, allowErrors bool) (*Writer, error) {
if compressionType < 0 || compressionType > 6 {
return nil, errors.New("Incorrect compression type")
}
if compressionType == 3 {
return nil, errors.New("Lzo compression is not (currently) supported")
}
writer := &Writer{
structure: map[string][]*fileHolder{
"/": make([]*fileHolder, 0),
},
folders: []string{
"/",
},
symlinkTable: make(map[string]string),
compressionType: compressionType,
allowErrors: allowErrors,
BlockSize: uint32(1048576),
Flags: DefaultFlags,
}
switch compressionType {
case 1:
writer.compressor = &compression.Gzip{}
case 2:
writer.compressor = &compression.Lzma{}
case 4:
writer.compressor = &compression.Xz{}
case 5:
writer.compressor = &compression.Lz4{}
case 6:
writer.compressor = &compression.Zstd{}
}
return writer, nil
}
//fileHolder holds the necessary information about a given file inside of a squashfs
type fileHolder struct {
reader io.Reader
path string
name string
symLocation string
blockSizes []uint32
GUID int
perm int
size uint64
UID int
folder bool
symlink bool
fragIndex int
fragOffset int
}
//AddFile attempts to add an os.File to the archive's root directory.
func (w *Writer) AddFile(file *os.File) error {
return w.AddFileToFolder("/", file)
}
//AddFileToFolder adds the given file to the squashfs archive, placing it inside the given folder.
func (w *Writer) AddFileToFolder(folder string, file *os.File) error {
name := path.Base(file.Name())
if !strings.HasSuffix(folder, "/") {
folder += "/"
}
return w.AddFileTo(folder+name, file)
}
//AddFileTo adds the given file to the squashfs archive at the given filepath.
func (w *Writer) AddFileTo(filepath string, file *os.File) error {
filepath = path.Clean(filepath)
if !strings.HasPrefix(filepath, "/") {
filepath = "/" + filepath
}
if w.Contains(filepath) {
return errors.New("File already exists at " + filepath)
}
var holder fileHolder
holder.path = path.Dir(filepath)
holder.name = path.Base(filepath)
holder.reader = file
stat, err := file.Stat()
if err != nil {
return err
}
holder.folder = stat.IsDir()
holder.symlink = (stat.Mode()&os.ModeSymlink == os.ModeSymlink)
holder.perm = int(stat.Mode().Perm())
//Thanks to https://stackoverflow.com/questions/58179647/getting-uid-and-gid-of-a-file for uid and guid getting
if stat, ok := stat.Sys().(*syscall.Stat_t); ok {
holder.UID = int(stat.Uid)
holder.GUID = int(stat.Gid)
}
if sort.SearchInts(w.uidGUIDTable, holder.UID) == len(w.uidGUIDTable) {
w.uidGUIDTable = append(w.uidGUIDTable, holder.UID)
sort.Ints(w.uidGUIDTable)
}
if sort.SearchInts(w.uidGUIDTable, holder.GUID) == len(w.uidGUIDTable) {
w.uidGUIDTable = append(w.uidGUIDTable, holder.GUID)
sort.Ints(w.uidGUIDTable)
}
if holder.symlink {
holder.reader = file
target, err := os.Readlink(file.Name())
if err != nil {
return err
}
holder.symLocation = target
} else if holder.folder {
subDirNames, err := file.Readdirnames(-1)
if err != nil {
return err
}
dirsAdded := make([]string, 0)
for _, subDir := range subDirNames {
fil, err := os.Open(file.Name() + subDir)
if err != nil {
return err
}
err = w.AddFileToFolder(holder.path+"/"+holder.name, fil)
if err != nil && !w.allowErrors {
for _, dir := range dirsAdded {
w.Remove(dir)
}
return err
} else if err != nil {
log.Println("Error while adding", fil.Name())
log.Println(err)
}
if !w.allowErrors {
dirsAdded = append(dirsAdded, holder.path+"/"+holder.name)
}
}
} else if stat.Mode().IsRegular() {
holder.reader = file
} else {
return errors.New("Unsupported file type " + file.Name())
}
if _, ok := w.structure[holder.path]; ok {
w.folders = append(w.folders, holder.path)
sort.Strings(w.folders)
}
w.structure[holder.path] = append(w.structure[holder.path], &holder)
return nil
}
//AddReaderTo adds the data from the given reader to the archive as a file located at the given filepath.
//Data from the reader is not read until the squashfs archive is writen.
//If the given reader implements io.Closer, it will be closed after it is fully read.
func (w *Writer) AddReaderTo(filepath string, reader io.Reader, size uint64) error {
filepath = path.Clean(filepath)
if !strings.HasPrefix(filepath, "/") {
filepath = "/" + filepath
}
if w.Contains(filepath) {
return errors.New("File already exists at " + filepath)
}
var holder fileHolder
holder.path = path.Dir(filepath)
holder.name = path.Base(filepath)
holder.size = size
holder.reader = reader
if _, ok := w.structure[holder.path]; ok {
w.folders = append(w.folders, holder.path)
sort.Strings(w.folders)
}
w.structure[holder.path] = append(w.structure[holder.path], &holder)
return nil
}
//AddFolderTo adds a folder at the given path. IF the folder is already present, it sets the folder's permissions.
//If the path points to a non-folder (such as a file or symlink), an error is returned
func (w *Writer) AddFolderTo(folderpath string, permission fs.FileMode) error {
folderpath = path.Clean(folderpath)
tmp := w.holderAt(folderpath)
if tmp != nil {
if !tmp.folder {
return errors.New("Path is not a folder: " + folderpath)
}
tmp.perm = int(permission.Perm())
return nil
}
file := fileHolder{
path: path.Dir(folderpath),
name: path.Base(folderpath),
perm: int(permission | fs.ModePerm),
folder: true,
}
w.structure[file.path] = append(w.structure[file.path], &file)
return nil
}
//Remove tries to remove the file(s) at the given filepath. If wildcards are used, it will remove all files that match.
//Returns true if one or more files are removed.
func (w *Writer) Remove(filepath string) bool {
var matchFound bool
filepath = path.Clean(filepath)
if !strings.HasPrefix(filepath, "/") {
filepath = "/" + filepath
}
dir, name := path.Split(filepath)
for structDir, files := range w.structure {
if match, _ := path.Match(dir, structDir); match {
for i, fil := range files {
if match, _ = path.Match(name, fil.name); match {
matchFound = true
if len(w.structure[structDir]) > 1 {
w.structure[structDir][i] = w.structure[structDir][len(w.structure[structDir])-1]
w.structure[structDir] = w.structure[structDir][:len(w.structure[structDir])-1]
} else {
w.structure[structDir] = nil
}
}
}
}
}
return matchFound
}
//FixSymlinks will scan through the squashfs archive and try to find broken symlinks and fix them.
//This done by replacing the symlink with the target file and then pointing other symlinks to that file.
//If all symlinks can be resolved, the error slice will be nil, and the bool false, otherwise all errors occured will be in the slice.
func (w *Writer) FixSymlinks() (errs []error, problems bool) {
for dir, holderSlice := range w.structure {
for i, holder := range holderSlice {
if !holder.symlink {
continue
}
sym := holder.symLocation
if !path.IsAbs(holder.symLocation) {
sym = path.Join(dir, holder.symLocation)
}
if path, ok := w.symlinkTable[sym]; ok {
w.structure[dir][i].symLocation = path
continue
}
if path.IsAbs(sym) || strings.HasPrefix(sym, "../") {
var symFil *os.File
var err error
if strings.HasPrefix(sym, "../") {
holderFil, ok := holder.reader.(*os.File)
if !ok {
problems = true
errs = append(errs, errors.New("Cannot resolve symlink at "+dir+holder.name))
continue
}
symFilPath := path.Dir(holderFil.Name())
symFilPath = path.Join(symFilPath, holder.symLocation)
symFil, err = os.Open(symFilPath)
} else {
symFil, err = os.Open(sym)
}
if err != nil {
problems = true
errs = append(errs, err)
continue
}
suc := w.Remove(dir + holder.name)
if !suc {
problems = true
errs = append(errs, errors.New("Cannot resolve symlink at "+dir+holder.name))
continue
}
err = w.AddFileTo(dir+holder.name, symFil)
if err != nil {
w.structure[dir] = append(w.structure[dir], holder)
problems = true
errs = append(errs, err)
continue
}
w.symlinkTable[sym] = dir + holder.name
} else {
symHolder := w.holderAt(sym)
if symHolder != nil {
w.symlinkTable[sym] = sym
continue
}
holderFil, ok := holder.reader.(*os.File)
if !ok {
problems = true
errs = append(errs, errors.New("Cannot resolve symlink at "+dir+holder.name))
continue
}
symFilPath := path.Dir(holderFil.Name())
symFilPath = path.Join(symFilPath, holder.symLocation)
symFil, err := os.Open(symFilPath)
if err != nil {
problems = true
errs = append(errs, err)
continue
}
err = w.AddFileTo(sym, symFil)
if err != nil {
problems = true
errs = append(errs, err)
continue
}
w.symlinkTable[sym] = sym
}
}
}
return
}
func (w *Writer) holderAt(filepath string) *fileHolder {
filepath = path.Clean(filepath)
if !strings.HasPrefix(filepath, "/") {
filepath = "/" + filepath
}
dir, name := path.Split(filepath)
if holderSlice, ok := w.structure[dir]; ok {
for _, holder := range holderSlice {
if holder.name == name {
return holder
}
}
}
return nil
}
//Contains returns whether a file is present at the given filepath
func (w *Writer) Contains(filepath string) bool {
filepath = path.Clean(filepath)
if !strings.HasPrefix(filepath, "/") {
filepath = "/" + filepath
}
dir, name := path.Split(filepath)
if holderSlice, ok := w.structure[dir]; ok {
for _, holder := range holderSlice {
if holder.name == name {
return true
}
}
}
return false
}
//WriteToFilename creates the squashfs archive with the given filepath.
func (w *Writer) WriteToFilename(filepath string) error {
newFil, err := os.Create(filepath)
if err != nil {
return err
}
_, err = w.WriteTo(newFil)
return err
}
-17
View File
@@ -1,17 +0,0 @@
package squashfs
import "reflect"
func (w *Writer) compressData(data []byte) ([]byte, error) {
if reflect.DeepEqual(data, make([]byte, len(data))) {
return nil, nil
}
compressedData, err := w.compressor.Compress(data)
if err != nil {
return nil, err
}
if len(data) <= len(compressedData) {
return data, nil
}
return compressedData, nil
}
-15
View File
@@ -1,15 +0,0 @@
package squashfs
//TODO: Allow settings the options
// func (w *Writer) SetGzipOptions() error {}
// func (w *Writer) SetLzmaOptions() error {}
// func (w *Writer) SetLzoOptions() error {}
// func (w *Writer) SetXzOptions() error {}
// func (w *Writer) SetLz4Options() error {}
// func (w *Writer) SetZstdOptions() error {}
-69
View File
@@ -1,69 +0,0 @@
package squashfs
type fragment struct {
w *Writer
files []*fileHolder
sizes []uint32
}
func (f *fragment) SizeLeft() uint32 {
totalSize := uint32(0)
for _, siz := range f.sizes {
totalSize += siz
}
return f.w.BlockSize - uint32(totalSize)
}
func (f *fragment) AddFragment(fil *fileHolder) {
//SizeLeft should already be checked
fil.fragOffset = len(f.files)
f.files = append(f.files, fil)
f.sizes = append(f.sizes, fil.blockSizes[len(fil.blockSizes)-1])
}
func (w *Writer) addToFragments(fil *fileHolder) {
fragSize := fil.blockSizes[len(fil.blockSizes)-1]
//only fragment if the final block is less then 80% of a full block or AlwaysFragment
if w.Flags.AlwaysFragment || fragSize < uint32(float32(w.BlockSize)*0.8) {
var possibleFrags []int
for i := range w.frags {
left := w.frags[i].SizeLeft()
if left == fragSize {
fil.fragIndex = i
w.frags[i].AddFragment(fil)
return
} else if left > fragSize {
possibleFrags = append(possibleFrags, i)
}
}
if len(possibleFrags) > 0 {
fil.fragIndex = possibleFrags[0]
} else {
fil.fragIndex = len(w.frags)
w.frags = append(w.frags, fragment{
w: w,
files: []*fileHolder{fil},
sizes: []uint32{fragSize},
})
}
}
}
func (w *Writer) calculateFragsAndBlockSizes() {
for _, files := range w.structure {
for i := range files {
files[i].fragIndex = -1
files[i].blockSizes = make([]uint32, files[i].size/uint64(w.BlockSize))
for j := range files[i].blockSizes {
files[i].blockSizes[j] = w.BlockSize
}
fragSize := uint32(files[i].size % uint64(w.BlockSize))
if fragSize > 0 {
files[i].blockSizes = append(files[i].blockSizes, fragSize)
if !w.Flags.NoFragments {
w.addToFragments(files[i])
}
}
}
}
}
-9
View File
@@ -1,9 +0,0 @@
package squashfs
func (w *Writer) countInodes() (out uint32) {
for _, files := range w.structure {
out++
out += uint32(len(files))
}
return
}
-56
View File
@@ -1,56 +0,0 @@
package squashfs
import (
"errors"
"io"
"io/fs"
"math"
"time"
)
func (w *Writer) fixFolders() error {
for folder := range w.structure {
if folder == "/" || w.Contains(folder) {
continue
}
err := w.AddFolderTo(folder, fs.ModePerm)
if err != nil {
return err
}
}
return nil
}
//WriteTo attempts to write the archive to the given io.Writer.
//Folder that aren't present (such as if you add a file at /folder/file, but not the folder /folder)
//are added with full permission (777).
//
//Not working. Yet.
func (w *Writer) WriteTo(write io.Writer) (int64, error) {
err := w.fixFolders()
if err != nil {
return 0, err
}
if w.BlockSize > 1048576 {
w.BlockSize = 1048576
} else if w.BlockSize < 4096 {
w.BlockSize = 4096
}
w.Flags.RemoveDuplicates = false
w.Flags.Exportable = false
w.Flags.NoXattr = true
w.calculateFragsAndBlockSizes()
w.superblock = superblock{
Magic: magic,
InodeCount: w.countInodes(),
CreationTime: uint32(time.Now().Unix()),
BlockSize: w.BlockSize,
CompressionType: uint16(w.compressionType),
BlockLog: uint16(math.Log2(float64(w.BlockSize))),
Flags: w.Flags.ToUint(),
IDCount: uint16(len(w.uidGUIDTable)),
MajorVersion: 4,
MinorVersion: 0,
}
return 0, errors.New("I SAID DON'T")
}