Compare commits

..

21 Commits

Author SHA1 Message Date
Caleb Gardner a129b259be Apply sparse file fix to reader 2023-08-11 18:15:11 -05:00
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
17 changed files with 544 additions and 124 deletions
+1
View File
@@ -1 +1,2 @@
testing testing
/go-unsquashfs
+15 -4
View File
@@ -1,8 +1,8 @@
# 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) [![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. Currently has support for reading squashfs files and extracting files and folders.
@@ -14,7 +14,18 @@ Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree
## Limitations ## 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. * 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`
## Performance ## Issues
Testing on a zstd compressed file, my library is anywhere from 5x ~ 7x slower then `unsquashfs` * 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.
+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))
}
+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
}
+13 -13
View File
@@ -24,7 +24,7 @@ func (r *Reader) Mount(mountpoint string) (err error) {
<-r.con.Ready <-r.con.Ready
r.mountDone = make(chan struct{}) r.mountDone = make(chan struct{})
go func() { go func() {
fs.Serve(r.con, &squashFuse{r: r}) fs.Serve(r.con, squashFuse{r: r})
close(r.mountDone) close(r.mountDone)
}() }()
return return
@@ -50,15 +50,15 @@ type squashFuse struct {
r *Reader r *Reader
} }
func (s *squashFuse) Root() (fs.Node, error) { func (s squashFuse) Root() (fs.Node, error) {
return &fileNode{File: s.r.FS.File}, nil return fileNode{File: s.r.FS.File}, nil
} }
type fileNode struct { type fileNode struct {
*File *File
} }
func (f *fileNode) Attr(ctx context.Context, attr *fuse.Attr) error { func (f fileNode) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Blocks = f.r.s.Size / 512 attr.Blocks = f.r.s.Size / 512
if f.r.s.Size%512 > 0 { if f.r.s.Size%512 > 0 {
attr.Blocks++ attr.Blocks++
@@ -72,15 +72,15 @@ func (f *fileNode) Attr(ctx context.Context, attr *fuse.Attr) error {
return nil return nil
} }
func (f *fileNode) Id() uint64 { func (f fileNode) Id() uint64 {
return uint64(f.i.Num) return uint64(f.i.Num)
} }
func (f *fileNode) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) { func (f fileNode) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
return f.SymlinkPath(), nil return f.SymlinkPath(), nil
} }
func (f *fileNode) Lookup(ctx context.Context, name string) (fs.Node, error) { func (f fileNode) Lookup(ctx context.Context, name string) (fs.Node, error) {
asFS, err := f.FS() asFS, err := f.FS()
if err != nil { if err != nil {
return nil, fuse.ENOTDIR return nil, fuse.ENOTDIR
@@ -89,19 +89,19 @@ func (f *fileNode) Lookup(ctx context.Context, name string) (fs.Node, error) {
if err != nil { if err != nil {
return nil, fuse.ENOENT return nil, fuse.ENOENT
} }
return &fileNode{File: ret}, nil return fileNode{File: ret}, nil
} }
func (f *fileNode) ReadAll(ctx context.Context) ([]byte, error) { func (f fileNode) ReadAll(ctx context.Context) ([]byte, error) {
if f.IsRegular() { if f.IsRegular() {
var buf bytes.Buffer var buf bytes.Buffer
_, err := f.WriteTo(&buf) _, err := f.WriteTo(&buf)
return buf.Bytes(), err return buf.Bytes(), err
} }
return nil, fuse.ENODATA return nil, ENODATA
} }
func (f *fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { func (f fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
if f.IsRegular() { if f.IsRegular() {
buf := make([]byte, req.Size) buf := make([]byte, req.Size)
n, err := f.File.ReadAt(buf, req.Offset) n, err := f.File.ReadAt(buf, req.Offset)
@@ -110,10 +110,10 @@ func (f *fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.R
} }
return nil return nil
} }
return fuse.ENODATA return ENODATA
} }
func (f *fileNode) ReadDirAll(ctx context.Context) (out []fuse.Dirent, err error) { func (f fileNode) ReadDirAll(ctx context.Context) (out []fuse.Dirent, err error) {
asFS, err := f.FS() asFS, err := f.FS()
if err != nil { if err != nil {
return nil, fuse.ENOTDIR return nil, fuse.ENOTDIR
+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)
+6 -6
View File
@@ -1,14 +1,14 @@
module github.com/CalebQ42/squashfs module github.com/CalebQ42/squashfs
go 1.19 go 1.21
require ( require (
github.com/CalebQ42/fuse v0.1.0 github.com/CalebQ42/fuse v0.1.0
github.com/klauspost/compress v1.15.12 github.com/klauspost/compress v1.16.7
github.com/pierrec/lz4/v4 v4.1.17 github.com/pierrec/lz4/v4 v4.1.18
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e 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/therootcompany/xz v1.0.1
github.com/ulikunitz/xz v0.5.10 github.com/ulikunitz/xz v0.5.11
golang.org/x/sys v0.11.0
) )
require golang.org/x/sys v0.2.0 // indirect
+10 -8
View File
@@ -1,14 +1,16 @@
github.com/CalebQ42/fuse v0.1.0 h1:KLCNjun7zcd2kBNVFfH+SWJyhuwJdE0nhw5/q8K8HGQ= 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/CalebQ42/fuse v0.1.0/go.mod h1:pJpoKG03HJKVhsp8o0YQYqmfbFsr3Eowt90yQGQVO+4=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 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 h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= 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 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+9 -3
View File
@@ -14,15 +14,17 @@ type FullReader struct {
sizes []uint32 sizes []uint32
blockSize uint32 blockSize uint32
start uint64 start uint64
fileSize uint64
} }
func NewFullReader(r io.ReaderAt, start uint64, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) *FullReader { func NewFullReader(r io.ReaderAt, start uint64, d decompress.Decompressor, blockSizes []uint32, blockSize uint32, fileSize uint64) *FullReader {
return &FullReader{ return &FullReader{
r: r, r: r,
start: start, start: start,
blockSize: blockSize, blockSize: blockSize,
sizes: blockSizes, sizes: blockSizes,
d: d, d: d,
fileSize: fileSize,
} }
} }
@@ -43,10 +45,14 @@ func (r FullReader) process(index int, offset int64, out chan outDat) {
var rdr io.ReadCloser var rdr io.ReadCloser
size := realSize(r.sizes[index]) size := realSize(r.sizes[index])
if size == 0 { if size == 0 {
outSize := r.blockSize
if r.fileSize < uint64(r.blockSize) {
outSize = uint32(r.fileSize)
}
out <- outDat{ out <- outDat{
i: index, i: index,
err: nil, err: nil,
data: make([]byte, r.blockSize), data: make([]byte, outSize),
} }
return return
} }
@@ -102,7 +108,7 @@ func (r FullReader) ReadAt(p []byte, off int64) (n int, err error) {
if i == num-1 && r.fragRdr != nil { if i == num-1 && r.fragRdr != nil {
go func() { go func() {
rdr, e := r.fragRdr() rdr, e := r.fragRdr()
if err != nil { if e != nil {
out <- outDat{ out <- outDat{
i: num - 1, i: num - 1,
err: e, err: e,
+8 -2
View File
@@ -16,15 +16,17 @@ type Reader struct {
blockSizes []uint32 blockSizes []uint32
blockSize uint32 blockSize uint32
resetable bool resetable bool
fileSize uint64
} }
func NewReader(r io.Reader, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) *Reader { func NewReader(r io.Reader, d decompress.Decompressor, blockSizes []uint32, blockSize uint32, fileSize uint64) *Reader {
return &Reader{ return &Reader{
d: d, d: d,
master: r, master: r,
blockSizes: blockSizes, blockSizes: blockSizes,
blockSize: blockSize, blockSize: blockSize,
resetable: true, resetable: true,
fileSize: fileSize,
} }
} }
@@ -49,7 +51,11 @@ func (r *Reader) advance() (err error) {
} else { } else {
size := realSize(r.blockSizes[0]) size := realSize(r.blockSizes[0])
if size == 0 { if size == 0 {
r.cur = bytes.NewReader(make([]byte, r.blockSize)) outSize := r.blockSize
if r.fileSize < uint64(r.blockSize) {
outSize = uint32(r.fileSize)
}
r.cur = bytes.NewReader(make([]byte, outSize))
} else { } else {
r.cur = io.LimitReader(r.master, int64(size)) r.cur = io.LimitReader(r.master, int64(size))
if size == r.blockSizes[0] { if size == r.blockSizes[0] {
+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
}
+3
View File
@@ -13,12 +13,15 @@ import (
"github.com/CalebQ42/squashfs/internal/inode" "github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/metadata" "github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader" "github.com/CalebQ42/squashfs/internal/toreader"
fuse2 "github.com/seaweedfs/fuse"
) )
type Reader struct { type Reader struct {
*FS *FS
con *fuse.Conn con *fuse.Conn
con2 *fuse2.Conn
mountDone chan struct{} mountDone chan struct{}
mount2Done chan struct{}
d decompress.Decompressor d decompress.Decompressor
r io.ReaderAt r io.ReaderAt
fragEntries []fragEntry fragEntries []fragEntry
+241 -79
View File
@@ -6,13 +6,16 @@ import (
"io/fs" "io/fs"
"log" "log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"github.com/CalebQ42/squashfs/internal/data" "github.com/CalebQ42/squashfs/internal/data"
"github.com/CalebQ42/squashfs/internal/directory" "github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode" "github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/threadmanager"
) )
// File represents a file inside a squashfs archive. // File represents a file inside a squashfs archive.
@@ -58,6 +61,21 @@ func (f File) Stat() (fs.FileInfo, error) {
return newFileInfo(f.e, f.i), nil 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. // Read reads the data from the file. Only works if file is a normal file.
func (f File) Read(p []byte) (int, error) { func (f File) Read(p []byte) (int, error) {
if f.i.Type != inode.Fil && f.i.Type != inode.EFil { if f.i.Type != inode.Fil && f.i.Type != inode.EFil {
@@ -70,16 +88,22 @@ func (f File) Read(p []byte) (int, error) {
} }
func (f File) ReadAt(p []byte, off int64) (int, error) { 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) return f.fullRdr.ReadAt(p, off)
} }
// WriteTo writes all data from the file to the writer. This is multi-threaded. // 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. // The underlying reader is seperate from the one used with Read and can be reused.
func (f File) WriteTo(w io.Writer) (int64, error) { 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) return f.fullRdr.WriteTo(w)
} }
// Close simply nils the underlying reader. Here mostly to satisfy fs.File // Close simply nils the underlying reader.
func (f *File) Close() error { func (f *File) Close() error {
f.rdr = nil f.rdr = nil
return nil return nil
@@ -89,7 +113,7 @@ func (f *File) Close() error {
// If n <= 0 all fs.DirEntry's are returned. // If n <= 0 all fs.DirEntry's are returned.
func (f *File) ReadDir(n int) (out []fs.DirEntry, err error) { func (f *File) ReadDir(n int) (out []fs.DirEntry, err error) {
if !f.IsDir() { if !f.IsDir() {
return nil, errors.New("File is not a directory") return nil, errors.New("file is not a directory")
} }
ents, err := f.r.readDirectory(f.i) ents, err := f.r.readDirectory(f.i)
if err != nil { if err != nil {
@@ -146,6 +170,20 @@ func (f File) IsSymlink() bool {
return f.i.Type == inode.Sym || f.i.Type == inode.ESym 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. // SymlinkPath returns the symlink's target path. Is the File isn't a symlink, returns an empty string.
func (f File) SymlinkPath() string { func (f File) SymlinkPath() string {
switch f.i.Type { switch f.i.Type {
@@ -182,134 +220,197 @@ func (f File) GetSymlinkFile() *File {
// ExtractionOptions are available options on how to extract. // ExtractionOptions are available options on how to extract.
type ExtractionOptions struct { type ExtractionOptions struct {
LogOutput io.Writer //Where error log should write. If nil, uses os.Stdout. Has no effect if verbose is false. manager *threadmanager.Manager
DereferenceSymlink bool //Replace symlinks with the target file LogOutput io.Writer //Where error log should write.
UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink DereferenceSymlink bool //Replace symlinks with the target file.
Verbose bool //Prints extra info to log on an error UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink.
FolderPerm fs.FileMode //The permissions used when creating the extraction folder 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
} }
// DefaultOptions is the default ExtractionOptions. func DefaultOptions() *ExtractionOptions {
func DefaultOptions() ExtractionOptions { return &ExtractionOptions{
return ExtractionOptions{ Perm: 0755,
FolderPerm: 0755,
} }
} }
// ExtractTo extracts the File to the given folder with the default options. // 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. // If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractTo(folder string) error { func (f File) ExtractTo(folder string) error {
return f.ExtractWithOptions(folder, DefaultOptions()) 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. // 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. // If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractSymlink(folder string) error { func (f File) ExtractSymlink(folder string) error {
return f.ExtractWithOptions(folder, ExtractionOptions{ op := DefaultOptions()
DereferenceSymlink: true, op.DereferenceSymlink = true
FolderPerm: 0755, return f.realExtract(folder, op)
})
} }
// ExtractWithOptions extracts the File to the given folder with the given ExtrationOptions. // 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. // 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 { func (f File) ExtractWithOptions(folder string, op *ExtractionOptions) error {
if op.Verbose { if op.Verbose && op.LogOutput != nil {
if op.LogOutput == nil {
op.LogOutput = os.Stdout
}
log.SetOutput(op.LogOutput) log.SetOutput(op.LogOutput)
} }
return f.realExtract(folder, op) return f.realExtract(folder, op)
} }
func (f File) realExtract(folder string, op ExtractionOptions) error { func (f File) realExtract(folder string, op *ExtractionOptions) (err error) {
err := os.MkdirAll(folder, op.FolderPerm) if op.manager == nil {
folder = filepath.Clean(folder) op.manager = threadmanager.NewManager(runtime.NumCPU())
if err != nil && !os.IsExist(err) {
if op.Verbose {
log.Println("Error while creating extraction folder")
}
return err
} }
if f.IsDir() { extDir := folder + "/" + f.e.Name
filFS, _ := f.FS() if !op.notFirst {
var ents []directory.Entry op.notFirst = true
ents, err = f.r.readDirectory(f.i) 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 err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while reading children of", f.path()) log.Println("Error while converting", f.path(), "to FS")
} }
return err return err
} }
errChan := make(chan error) errChan := make(chan error, len(filFS.e))
for i := 0; i < len(ents); i++ { files := make([]directory.Entry, 0)
go func(ent directory.Entry) { //Focus on making the folder tree first...
fil, goErr := f.r.newFile(ent, filFS) var i int
if goErr != nil { for i = 0; i < len(filFS.e); i++ {
if op.Verbose { if filFS.e[i].Type == inode.Fil {
log.Println("Error while reading info for", filepath.Join(f.path(), ent.Name)) files = append(files, filFS.e[i])
} } else {
errChan <- goErr go func(index int) {
return subF, goErr := f.r.newFile(filFS.e[index], filFS)
} if goErr != nil {
if fil.IsDir() {
info, _ := fil.Stat()
err = os.Mkdir(filepath.Join(folder, fil.e.Name), info.Mode())
if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while creating", filepath.Join(folder, fil.e.Name)) log.Println("Error while resolving", extDir)
} }
errChan <- err errChan <- goErr
return return
} }
errChan <- fil.realExtract(filepath.Join(folder, fil.e.Name), op) errChan <- subF.ExtractWithOptions(extDir, op)
} else { }(i)
errChan <- fil.realExtract(folder, op) }
}
fil.Close()
}(ents[i])
} }
for i := 0; i < len(ents); i++ { for i = 0; i < len(filFS.e)-len(files); i++ {
err = <-errChan err = <-errChan
if err != nil { if err != nil {
return err return err
} }
} }
return nil //Then we extract the files.
} else if f.IsRegular() { 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 var fil *os.File
fil, err = os.Create(folder + "/" + f.e.Name) fil, err = os.Create(extDir)
if os.IsExist(err) { if os.IsExist(err) {
os.Remove(folder + "/" + f.e.Name) os.Remove(extDir)
fil, err = os.Create(folder + "/" + f.e.Name) fil, err = os.Create(extDir)
if err != nil { if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while creating", folder+"/"+f.e.Name) log.Println("Error while creating", extDir)
} }
return err return err
} }
} else if err != nil { } else if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while creating", folder+"/"+f.e.Name) log.Println("Error while creating", extDir)
} }
return err return err
} }
defer fil.Close()
_, err = io.Copy(fil, f) _, err = io.Copy(fil, f)
if err != nil { if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while copying data to", folder+"/"+f.e.Name) log.Println("Error while copying data to", extDir)
} }
return err return err
} }
return nil if op.IgnorePerm {
} else if f.IsSymlink() { 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() symPath := f.SymlinkPath()
if op.DereferenceSymlink { if op.DereferenceSymlink {
fil := f.GetSymlinkFile() fil := f.GetSymlinkFile()
if fil == nil { if fil == nil {
if op.Verbose { if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name) log.Println("Symlink path(", symPath, ") is unobtainable:", extDir)
} }
return errors.New("cannot get symlink target") return errors.New("cannot get symlink target")
} }
@@ -317,7 +418,7 @@ func (f File) realExtract(folder string, op ExtractionOptions) error {
err = fil.realExtract(folder, op) err = fil.realExtract(folder, op)
if err != nil { if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while extracting the symlink's file:", folder+"/"+f.e.Name) log.Println("Error while extracting the symlink's file:", extDir)
} }
return err return err
} }
@@ -326,31 +427,92 @@ func (f File) realExtract(folder string, op ExtractionOptions) error {
fil := f.GetSymlinkFile() fil := f.GetSymlinkFile()
if fil == nil { if fil == nil {
if op.Verbose { if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name) log.Println("Symlink path(", symPath, ") is unobtainable:", extDir)
} }
return errors.New("cannot get symlink target") return errors.New("cannot get symlink target")
} }
extractLoc := filepath.Clean(folder + "/" + filepath.Dir(symPath)) extractLoc := filepath.Join(folder, filepath.Dir(symPath))
err = fil.realExtract(extractLoc, op) err = fil.realExtract(extractLoc, op)
if err != nil { if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while extracting ", folder+"/"+f.e.Name) log.Println("Error while extracting ", extDir)
} }
return err return err
} }
} }
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name) err = os.Symlink(f.SymlinkPath(), extDir)
if os.IsExist(err) { if os.IsExist(err) {
os.Remove(folder + "/" + f.e.Name) os.Remove(extDir)
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name) err = os.Symlink(f.SymlinkPath(), extDir)
} }
if err != nil { if err != nil {
if op.Verbose { if op.Verbose {
log.Println("Error while making symlink:", folder+"/"+f.e.Name) log.Println("Error while making symlink:", extDir)
} }
return err return err
} }
return nil 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 errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.i.Type))) return nil
} }
+5 -2
View File
@@ -36,23 +36,26 @@ func (r Reader) getReaders(i inode.Inode) (full *data.FullReader, rdr *data.Read
var blockSizes []uint32 var blockSizes []uint32
var fragInd uint32 var fragInd uint32
var fragSize uint32 var fragSize uint32
var fileSize uint64
if i.Type == inode.Fil { if i.Type == inode.Fil {
fragOffset = uint64(i.Data.(inode.File).FragOffset) fragOffset = uint64(i.Data.(inode.File).FragOffset)
blockOffset = uint64(i.Data.(inode.File).BlockStart) blockOffset = uint64(i.Data.(inode.File).BlockStart)
blockSizes = i.Data.(inode.File).BlockSizes blockSizes = i.Data.(inode.File).BlockSizes
fragInd = i.Data.(inode.File).FragInd fragInd = i.Data.(inode.File).FragInd
fragSize = i.Data.(inode.File).Size % r.s.BlockSize fragSize = i.Data.(inode.File).Size % r.s.BlockSize
fileSize = uint64(i.Data.(inode.File).Size)
} else if i.Type == inode.EFil { } else if i.Type == inode.EFil {
fragOffset = uint64(i.Data.(inode.EFile).FragOffset) fragOffset = uint64(i.Data.(inode.EFile).FragOffset)
blockOffset = i.Data.(inode.EFile).BlockStart blockOffset = i.Data.(inode.EFile).BlockStart
blockSizes = i.Data.(inode.EFile).BlockSizes blockSizes = i.Data.(inode.EFile).BlockSizes
fragInd = i.Data.(inode.EFile).FragInd fragInd = i.Data.(inode.EFile).FragInd
fragSize = uint32(i.Data.(inode.EFile).Size % uint64(r.s.BlockSize)) fragSize = uint32(i.Data.(inode.EFile).Size % uint64(r.s.BlockSize))
fileSize = i.Data.(inode.EFile).Size
} else { } else {
return nil, nil, errors.New("getReaders called on non-file type") 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) rdr = data.NewReader(toreader.NewReader(r.r, int64(blockOffset)), r.d, blockSizes, r.s.BlockSize, fileSize)
full = data.NewFullReader(r.r, uint64(blockOffset), r.d, blockSizes, r.s.BlockSize) full = data.NewFullReader(r.r, uint64(blockOffset), r.d, blockSizes, r.s.BlockSize, fileSize)
if fragInd != 0xFFFFFFFF { if fragInd != 0xFFFFFFFF {
full.AddFragment(func() (io.Reader, error) { full.AddFragment(func() (io.Reader, error) {
var fragRdr io.Reader var fragRdr io.Reader
+7 -6
View File
@@ -18,8 +18,8 @@ import (
) )
const ( const (
squashfsURL = "https://darkstorm.tech/LinuxPATest.sfs" squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs"
squashfsName = "LinuxPATest.sfs" squashfsName = "bug.sqfs"
filePath = "PortableApps/Notepad++Portable/App/DefaultData/Config/contextMenu.xml" filePath = "PortableApps/Notepad++Portable/App/DefaultData/Config/contextMenu.xml"
) )
@@ -106,7 +106,6 @@ func BenchmarkRace(b *testing.B) {
} }
func TestExtractQuick(t *testing.T) { func TestExtractQuick(t *testing.T) {
//First, setup everything and extract the archive using the library and unsquashfs //First, setup everything and extract the archive using the library and unsquashfs
// tmpDir := b.TempDir() // tmpDir := b.TempDir()
@@ -123,8 +122,12 @@ func TestExtractQuick(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
os.RemoveAll(filepath.Join(tmpDir, "testLog.txt"))
logFil, _ := os.Create(filepath.Join(tmpDir, "testLog.txt"))
op := squashfs.DefaultOptions() op := squashfs.DefaultOptions()
op.Verbose = true op.Verbose = true
op.IgnorePerm = true
op.LogOutput = logFil
err = rdr.ExtractWithOptions(libPath, op) err = rdr.ExtractWithOptions(libPath, op)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -176,9 +179,7 @@ func TestSingleFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
op := squashfs.DefaultOptions() err = f.(*squashfs.File).ExtractWithOptions("testing", &squashfs.ExtractionOptions{Verbose: true})
op.Verbose = true
err = f.(*squashfs.File).ExtractWithOptions("testing", op)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }