Compare commits

...

15 Commits

Author SHA1 Message Date
Caleb Gardner 35d22b4bd0 Removed close function from squashfs.File
This seems to cause issues in ver specific circumstances and ultimately, isn't needed.
2021-01-06 12:56:09 -06:00
Caleb Gardner 97b12090c6 Trying to figure out the best way to convert files 2021-01-05 05:53:16 -06:00
Caleb Gardner 43fe4f91a2 More work on Writer
Adding files to the Writer should work properly now (except symlinks)
Threaded the adding of files.
Added ability to ignore errors when adding files.
2021-01-03 04:39:39 -06:00
Caleb Gardner 9524a2c192 More work on Writer 2021-01-02 17:10:21 -06:00
Caleb Gardner 162b228881 Some beginning work on squashfs.Writer
Just laying out some of the functions and what they do.
2021-01-01 10:54:09 -06:00
Caleb Gardner 7f87999a8f Merge branch 'main' of https://github.com/CalebQ42/squashfs into main 2020-12-28 11:51:08 -06:00
Caleb Gardner 47c28baf87 Improved data structure for structs.
Thanks gopls with VS Code
2020-12-28 11:50:56 -06:00
Caleb Gardner a298a3d7b5 Fixed first byte of data blocks being cut off 2020-12-27 02:10:51 -06:00
Caleb Gardner 89ec7eb0fb Fixed GetSymlinkFile
Added GetSymlinkFileRecursive
2020-12-16 02:39:35 -06:00
Caleb Gardner d63ba47818 Added Reader.ModTime 2020-12-11 01:39:08 -06:00
Caleb Gardner 495d2345a4 File now implements os.FileInfo 2020-12-11 01:38:53 -06:00
Caleb Gardner 6dfcb1cf80 Merge branch 'main' of https://github.com/CalebQ42/squashfs into main 2020-12-10 08:52:58 -06:00
Caleb Gardner c7593eaff3 Added Reader.ExtractTo for ease of use 2020-12-10 08:52:49 -06:00
Caleb Gardner 135403032f Updated README 2020-12-09 01:45:58 -06:00
Caleb Gardner 5c3bf8d528 Implemented the rest of the compression types
Haven't implemented LZO due to limited libraries
2020-12-09 01:40:30 -06:00
14 changed files with 496 additions and 121 deletions
+8 -8
View File
@@ -1,14 +1,14 @@
# squashfs [![PkgGoDev](https://pkg.go.dev/badge/github.com/CalebQ42/squashfs)](https://pkg.go.dev/github.com/CalebQ42/squashfs)
# squashfs (WIP)
[![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.
Currently, you can read a squashfs and extract files (folder extraction not supported. Yet).
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.
Special thanks to https://dr-emann.github.io/squashfs/ for some VERY important information in an easy to understand format.
The only major thing missing from squashfs reading is Xattr parsing.
Special thanks to <https://dr-emann.github.io/squashfs/> for some VERY important information in an easy to understand format.
Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree/master/internal/squashfs) as I referenced it to figure some things out (and double check others).
# [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
# Where I'm at
* Working on the File interface that should make it easier to deal with squashfs files. I'm also trying to make them capable for when I get squashing working.
## [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
+4 -10
View File
@@ -18,11 +18,11 @@ var (
//DataReader reads data from data blocks.
type dataReader struct {
r *Reader
offset int64 //offset relative to the beginning of the squash file
blocks []dataBlock
curBlock int //Which block in sizes is currently cached
curData []byte
curReadOffset int //offset relative to the currently cached data
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
}
//DataBlock holds info about a given data block from it's size
@@ -145,12 +145,6 @@ func (d *dataReader) readCurBlock() error {
return err
}
//Close frees up the curData from memory
func (d *dataReader) Close() error {
d.curData = nil
return nil
}
func (d *dataReader) Read(p []byte) (int, error) {
if d.curData == nil {
err := d.readCurBlock()
@@ -175,12 +169,12 @@ func (d *dataReader) Read(p []byte) (int, error) {
d.curReadOffset = 0
}
for ; read < len(p); read++ {
d.curReadOffset++
if d.curReadOffset < len(d.curData) {
p[read] = d.curData[d.curReadOffset]
} else {
break
}
d.curReadOffset++
}
}
if read != len(p) {
+106 -59
View File
@@ -7,6 +7,7 @@ import (
"os"
"path"
"strings"
"time"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
@@ -26,14 +27,17 @@ var (
//File is the main way to interact with files within squashfs, or when putting files into a squashfs.
//File can be either a file or folder. When reading from a squashfs, it reads from the datablocks.
//When writing, this holds the information on WHERE the file will be placed inside the archive.
//
//Implements os.FileInfo and io.ReadCloser
type File struct {
Name string //The name of the file or folder. Root folder will not have a name ("")
Parent *File //The parent directory. Should ALWAYS be a folder. If it's the root directory, will be nil
Reader io.Reader //Underlying reader. When writing, will probably be an os.File. When reading this is kept nil UNTIL reading to save memory.
path string //The path to the folder the File is located in.
r *Reader //The squashfs.Reader where this file is contained.
in *inode.Inode //Underlyting inode when reading.
filType int //The file's type, using inode types.
Reader io.Reader
Parent *File
r *Reader //Underlying reader. When writing, will probably be an os.File. When reading this is kept nil UNTIL reading to save memory.
in *inode.Inode
name string
path string
filType int //The file's type, using inode types.
}
//get a File from a directory.entry
@@ -43,12 +47,50 @@ func (r *Reader) newFileFromDirEntry(entry *directory.Entry) (fil *File, err err
if err != nil {
return nil, err
}
fil.Name = entry.Name
fil.name = entry.Name
fil.r = r
fil.filType = fil.in.Type
return
}
//Name is the file's name
func (f *File) Name() string {
return f.name
}
//Size is the complete size of the file. Zero if it's not a file.
func (f *File) Size() int64 {
switch f.filType {
case inode.BasicFileType:
return int64(f.in.Info.(inode.BasicFile).Init.Size)
case inode.ExtFileType:
return int64(f.in.Info.(inode.ExtendedFile).Init.Size)
default:
return 0
}
}
//ModTime is the time of last modification.
func (f *File) ModTime() time.Time {
return time.Unix(int64(f.in.Header.ModifiedTime), 0)
}
//Sys returns the underlying reader, file.Reader. If the reader isn't initialized, it will initialize it.
//If called on something other then a file, returns nil.
func (f *File) Sys() interface{} {
if f.IsFile() {
if f.Reader == nil && f.r != nil {
var err error
f.Reader, err = f.r.newFileReader(f.in)
if err != nil {
return nil
}
}
return f.Reader
}
return nil
}
//GetChildren returns a *squashfs.File slice of every direct child of the directory. If the File is not a directory, will return ErrNotDirectory
func (f *File) GetChildren() (children []*File, err error) {
children = make([]*File, 0)
@@ -69,7 +111,7 @@ func (f *File) GetChildren() (children []*File, err error) {
return
}
fil.Parent = f
if f.Name != "" {
if f.name != "" {
fil.path = f.Path()
}
children = append(children, fil)
@@ -117,10 +159,10 @@ func (f *File) GetChildrenRecursively() (children []*File, err error) {
//Path returns the path of the file within the archive.
func (f *File) Path() string {
if f.Name == "" {
if f.name == "" {
return f.path
}
return f.path + "/" + f.Name
return f.path + "/" + f.name
}
//GetFileAtPath tries to return the File at the given path, relative to the file.
@@ -139,7 +181,7 @@ func (f *File) GetFileAtPath(dirPath string) *File {
dirPath = strings.TrimPrefix(dirPath, "./")
}
split := strings.Split(dirPath, "/")
if split[0] == ".." && f.Name == "" {
if split[0] == ".." && f.name == "" {
return nil
} else if split[0] == ".." {
if f.Parent != nil {
@@ -152,7 +194,7 @@ func (f *File) GetFileAtPath(dirPath string) *File {
return nil
}
for _, child := range children {
eq, _ := path.Match(split[0], child.Name)
eq, _ := path.Match(split[0], child.name)
if eq {
return child.GetFileAtPath(strings.Join(split[1:], "/"))
}
@@ -188,7 +230,8 @@ func (f *File) SymlinkPath() string {
}
}
//GetSymlinkFile tries to return the squashfs.File associated with the symlink
//GetSymlinkFile tries to return the squashfs.File associated with the symlink. If the file isn't a symlink
//or the symlink points to a location outside the archive, nil is returned.
func (f *File) GetSymlinkFile() *File {
if !f.IsSymlink() {
return nil
@@ -196,11 +239,32 @@ func (f *File) GetSymlinkFile() *File {
if strings.HasSuffix(f.SymlinkPath(), "/") {
return nil
}
return f.r.GetFileAtPath(f.SymlinkPath())
return f.Parent.GetFileAtPath(f.SymlinkPath())
}
//Permission returns the os.FileMode of the File. Sets mode bits for directories and symlinks.
func (f *File) Permission() os.FileMode {
//GetSymlinkFileRecursive tries to return the squasfs.File associated with the symlink. It will recursively
//try to get the symlink's file. This will return either a non-symlink File, or nil.
func (f *File) GetSymlinkFileRecursive() *File {
if !f.IsSymlink() {
return nil
}
if strings.HasSuffix(f.SymlinkPath(), "/") {
return nil
}
sym := f
for {
sym = sym.GetSymlinkFile()
if sym == nil {
return nil
}
if !sym.IsSymlink() {
return sym
}
}
}
//Mode returns the os.FileMode of the File. Sets mode bits for directories and symlinks.
func (f *File) Mode() os.FileMode {
mode := os.FileMode(f.in.Header.Permissions)
switch {
case f.IsDir():
@@ -244,21 +308,21 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
}
switch {
case f.IsDir():
if f.Name != "" {
if f.name != "" {
//TODO: check if folder is present, and if so, try to set it's permission
err = os.Mkdir(path+"/"+f.Name, os.ModePerm)
err = os.Mkdir(path+"/"+f.name, os.ModePerm)
if err != nil {
if verbose {
fmt.Println("Error while making: ", path+"/"+f.Name)
fmt.Println("Error while making: ", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err := os.Open(path + "/" + f.Name)
fil, err := os.Open(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while opening:", path+"/"+f.Name)
fmt.Println("Error while opening:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -273,10 +337,10 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
// }
// errs = append(errs, err)
// }
err = fil.Chmod(f.Permission())
err = fil.Chmod(f.Mode())
if err != nil {
if verbose {
fmt.Println("Error while changing owner:", path+"/"+f.Name)
fmt.Println("Error while changing owner:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -292,13 +356,12 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
return
}
finishChan := make(chan []error)
defer close(finishChan)
for _, child := range children {
go func(child *File) {
if f.Name == "" {
if f.name == "" {
finishChan <- child.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
} else {
finishChan <- child.ExtractWithOptions(path+"/"+f.Name, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
finishChan <- child.ExtractWithOptions(path+"/"+f.name, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
}
}(child)
}
@@ -307,21 +370,21 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
}
return
case f.IsFile():
fil, err := os.Create(path + "/" + f.Name)
fil, err := os.Create(path + "/" + f.name)
if os.IsExist(err) {
err = os.Remove(path + "/" + f.Name)
err = os.Remove(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err = os.Create(path + "/" + f.Name)
fil, err = os.Create(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -329,7 +392,7 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
}
} else if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.Name)
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -338,13 +401,12 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
_, err = io.Copy(fil, f)
if err != nil {
if verbose {
fmt.Println("Error while Copying data to:", path+"/"+f.Name)
fmt.Println("Error while Copying data to:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
f.Close()
fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID]))
//don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0
// if err != nil {
@@ -355,10 +417,10 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
// errs = append(errs, err)
// return
// }
err = fil.Chmod(f.Permission())
err = fil.Chmod(f.Mode())
if err != nil {
if verbose {
fmt.Println("Error while setting permissions for:", path+"/"+f.Name)
fmt.Println("Error while setting permissions for:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -370,15 +432,15 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
fil := f.GetSymlinkFile()
if fil == nil {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.Name)
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name)
}
return
}
fil.Name = f.Name
fil.name = f.name
extracSymErrs := fil.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.Name)
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
@@ -392,22 +454,22 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
extracSymErrs := fil.ExtractWithOptions(strings.Join(paths[:len(paths)-1], "/"), dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.Name)
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
}
} else {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.Name)
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name)
}
return
}
}
err = os.Symlink(f.SymlinkPath(), path+"/"+f.Name)
err = os.Symlink(f.SymlinkPath(), path+"/"+f.name)
if err != nil {
if verbose {
fmt.Println("Error while making symlink:", path+"/"+f.Name)
fmt.Println("Error while making symlink:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
@@ -416,21 +478,6 @@ func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlin
return
}
//Close frees up the memory held up by the underlying reader. Should NOT be called when writing.
//When reading, Close is safe to use, but any subsequent Read calls resets to the beginning of the file.
func (f *File) Close() error {
if f.IsDir() {
return errNotFile
}
if f.Reader != nil {
if closer, is := f.Reader.(io.Closer); is {
closer.Close()
}
f.Reader = nil
}
return nil
}
//Read from the file. Doesn't do anything fancy, just pases it to the underlying io.Reader. If a directory, return io.EOF.
func (f *File) Read(p []byte) (int, error) {
if f.IsDir() {
-9
View File
@@ -57,15 +57,6 @@ func (r *Reader) newFileReader(in *inode.Inode) (*fileReader, error) {
return &rdr, nil
}
//Close runs Close on the data reader and frees the fragmentdata
func (f *fileReader) Close() error {
if f.data != nil {
f.data.Close()
}
f.fragmentData = nil
return nil
}
func (f *fileReader) Read(p []byte) (int, error) {
if f.fragOnly {
n, err := bytes.NewBuffer(f.fragmentData[f.read:]).Read(p)
+3 -1
View File
@@ -7,9 +7,11 @@ require (
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.4
github.com/kr/text v0.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.2
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.8
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-20200615113413-eeeca48fe776 // indirect
+6 -2
View File
@@ -18,6 +18,8 @@ github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3
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.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/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=
@@ -27,6 +29,8 @@ 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.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
github.com/pierrec/lz4/v4 v4.1.2/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=
@@ -38,8 +42,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
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=
+27 -1
View File
@@ -3,11 +3,37 @@ 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{}
type Gzip struct {
CompressionLevel int32
HasCustomWindow bool
HasStrategies bool
}
//NewGzipCompressorWithOptions creates a new gzip compressor/decompressor with options read from the given reader.
func NewGzipCompressorWithOptions(r io.Reader) (*Gzip, error) {
var gzip Gzip
var init gzipInit
err := binary.Read(r, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
gzip.CompressionLevel = init.CompressionLevel
//TODO: proper support for window size and strategies
gzip.HasCustomWindow = init.WindowSize != 15
gzip.HasStrategies = init.Strategies != 0 && init.Strategies != 1
return &gzip, nil
}
//Decompress reads the entirety of the given reader and returns it uncompressed as a byte slice.
func (g *Gzip) Decompress(r io.Reader) ([]byte, error) {
+37
View File
@@ -0,0 +1,37 @@
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
}
+36
View File
@@ -0,0 +1,36 @@
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
}
+1 -1
View File
@@ -16,9 +16,9 @@ type metadata struct {
//MetadataReader is a block reader for metadata. It will automatically read the next block, when it reaches the end of a block.
type metadataReader struct {
s *Reader
offset int64
headers []*metadata
data []byte
offset int64
readOffset int
}
+57 -8
View File
@@ -5,6 +5,7 @@ import (
"errors"
"io"
"math"
"time"
"github.com/CalebQ42/squashfs/internal/compression"
"github.com/CalebQ42/squashfs/internal/inode"
@@ -21,20 +22,23 @@ var (
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")
)
//Reader processes and reads a squashfs archive.
type Reader struct {
r io.ReaderAt
super superblock
flags superblockFlags
decompressor compression.Decompressor
fragOffsets []uint64
idTable []uint32
super superblock
flags superblockFlags
}
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
hasUnsupportedOptions := false
var rdr Reader
rdr.r = r
err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super)
@@ -50,8 +54,17 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
rdr.flags = rdr.super.GetFlags()
if rdr.flags.CompressorOptions {
switch rdr.super.CompressionType {
case xzCompression:
xz, err := compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 1000)) //1000 is technically too much, but it's just an easy way to do it.
case GzipCompression:
gzip, err := compression.NewGzipCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if gzip.HasCustomWindow || gzip.HasStrategies {
hasUnsupportedOptions = true
}
rdr.decompressor = gzip
case XzCompression:
xz, err := compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
@@ -59,17 +72,36 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
return nil, errors.New("XZ compression options has filters. These are not yet supported")
}
rdr.decompressor = xz
case Lz4Compression:
lz4, err := compression.NewLz4CompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if lz4.HC {
hasUnsupportedOptions = true
}
rdr.decompressor = lz4
case ZstdCompression:
zstd, err := compression.NewZstdCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 4))
if err != nil {
return nil, err
}
rdr.decompressor = zstd
default:
return nil, errCompressorOptions
return nil, errIncompatibleCompression
}
} else {
switch rdr.super.CompressionType {
case gzipCompression:
case GzipCompression:
rdr.decompressor = &compression.Gzip{}
case lzmaCompression:
case LzmaCompression:
rdr.decompressor = &compression.Lzma{}
case xzCompression:
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
@@ -111,9 +143,26 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
}
unread -= read
}
if hasUnsupportedOptions {
return &rdr, ErrOptions
}
return &rdr, 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)
}
//ExtractTo tries to extract ALL files to the given path. This is the same as getting the root folder and extracting that.
func (r *Reader) ExtractTo(path string) []error {
root, err := r.GetRootFolder()
if err != nil {
return []error{err}
}
return root.ExtractTo(path)
}
//GetRootFolder returns a squashfs.File that references the root directory of the squashfs archive.
func (r *Reader) GetRootFolder() (root *File, err error) {
root = new(File)
+23 -16
View File
@@ -1,6 +1,7 @@
package squashfs
import (
"fmt"
"io"
"net/http"
"os"
@@ -10,12 +11,11 @@ import (
)
const (
downloadURL = "https://github.com/Swordfish90/cool-retro-term/releases/download/1.1.1/Cool-Retro-Term-1.1.1-x86_64.AppImage"
appImageName = "Cool-Retro-Term.AppImage"
squashfsName = "airootfs.sfs" //testing with a ArchLinux root fs from the live img
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" //testing with a ArchLinux root fs from the live img
)
//Right now, don't use. Arch linux sfs uses XZ compression and when tested, most files just completely fail to extract.
func TestSquashfs(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
@@ -29,10 +29,20 @@ func TestSquashfs(t *testing.T) {
if err != nil {
t.Fatal(err)
}
os.RemoveAll(wd + "/testing/" + squashfsName + ".d")
root, _ := rdr.GetRootFolder()
errs := root.ExtractWithOptions(wd+"/testing/"+squashfsName+".d", false, false, os.ModePerm, true)
t.Fatal(errs)
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) {
@@ -58,11 +68,8 @@ func TestAppImage(t *testing.T) {
if err != nil {
t.Fatal(err)
}
fil := rdr.GetFileAtPath(".DirIcon")
if fil == nil {
t.Fatal("Can't find desktop file")
}
errs := fil.ExtractSymlink(wd + "/testing/")
os.RemoveAll(wd + "testing/firefox")
errs := rdr.ExtractTo(wd + "/testing/firefox")
if len(errs) > 0 {
t.Fatal(errs)
}
@@ -74,15 +81,15 @@ func TestAppImage(t *testing.T) {
}
func downloadTestAppImage(t *testing.T, dir string) {
//seems to time out on slow connections. Might fix that at some point... or not
os.Mkdir(dir, 0777)
//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 {
t.Fatal(err)
}
defer appImage.Close()
check := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
CheckRedirect: func(r *http.Request, _ []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
+7 -6
View File
@@ -1,12 +1,13 @@
package squashfs
//The types of compression supported by squashfs.
const (
gzipCompression = 1 + iota
lzmaCompression
lzoCompression
xzCompression
lz4Compression
zstdCompression
GzipCompression = 1 + iota
LzmaCompression
LzoCompression
XzCompression
Lz4Compression
ZstdCompression
)
//Superblock contains important information about a squashfs file. Located at the very front of the archive.
+181
View File
@@ -0,0 +1,181 @@
package squashfs
import (
"errors"
"log"
"os"
"path"
"strings"
"github.com/CalebQ42/squashfs/internal/inode"
)
//Writer is an interface to write a squashfs. Doesn't write until you call Write (TODO: maybe not do Write...).
//If AllowErrors is true, when errors are encountered, it just prints to the log instead of failing.
type Writer struct {
files map[string][]*File
symlinkTable map[string]string //[oldpath]newpath
symTableTemp map[string]string
directories []string
compression int
ResolveSymlinks bool
AllowErrors bool
}
//NewWriter creates a new squashfs.Writer with the default settings (gzip compression, autoresolving symlinks, and allowErrors)
func NewWriter() (*Writer, error) {
return NewWriterWithOptions(true, true, GzipCompression)
}
//NewWriterWithOptions creates a new squashfs.Writer with the given options.
//ResolveSymlinks tries to make sure symlinks aren't broken. It will either try to make the link's location work
func NewWriterWithOptions(resolveSymlinks, allowErrors bool, compressionType int) (*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")
}
out := Writer{
files: map[string][]*File{
"/": make([]*File, 0),
},
ResolveSymlinks: resolveSymlinks,
AllowErrors: allowErrors,
compression: compressionType,
}
if resolveSymlinks {
out.symlinkTable = make(map[string]string)
}
return &out, nil
}
type fileError struct {
err error
files []*File
}
//convertFile converts the given os.File to a squashfs.File. Returns the errors and converted file to the channels.
func (w *Writer) convertFile(squashfsPath string, file *os.File, subDir bool, fileErrChan chan fileError) {
var out fileError
var fil File
fil.Reader = file
fil.path = squashfsPath
fil.name = path.Base(file.Name())
mode := fil.Mode()
if mode.IsRegular() {
fil.filType = inode.BasicFileType
goto successExit
} else if mode.IsDir() {
fil.filType = inode.BasicSymlinkType
subDirs, err := file.Readdirnames(-1)
if err != nil {
if w.AllowErrors && !subDir {
log.Println("Can't get sub-directories for", file.Name())
log.Println(err)
} else {
out.err = err
}
goto failExit
}
subDirChan := make(chan fileError)
for _, filName := range subDirs {
go func(filename string, returnChan chan fileError) {
subFil, err := os.Open(filename)
if err != nil {
out.err = err
returnChan <- fileError{
err: err,
}
return
}
w.convertFile(fil.Path(), subFil, true, subDirChan)
}(file.Name()+filName, subDirChan)
}
for range subDirs {
filErr := <-subDirChan
if filErr.err != nil {
if w.AllowErrors && !subDir {
log.Println("Error while adding subdirectory of", file.Name())
log.Println(filErr.err)
} else if subDir {
if out.err == nil {
out.err = filErr.err
}
} else {
out.err = err
goto failExit
}
continue
}
out.files = append(out.files, filErr.files...)
}
goto successExit
} else if mode&os.ModeSymlink == os.ModeSymlink {
fil.filType = inode.BasicSymlinkType
symLocation, err := os.Readlink(file.Name())
if err != nil {
if w.AllowErrors && !subDir {
log.Println("Error while getting symlink's information for", file.Name())
log.Println(err)
} else {
out.err = err
}
goto failExit
}
if w.ResolveSymlinks {
if val, ok := w.symlinkTable[symLocation]; ok {
symLocation = val
} else if val, ok := w.symTableTemp[symLocation]; ok {
symLocation = val
} else {
//TODO: either add the file, or place the file in this location. Maybe defer this until after all the other files are added?
}
}
//TODO: store the symLocation inside the File somehow....
}
if w.AllowErrors && !subDir {
log.Println("Unsupported file type for", file.Name())
} else {
out.err = errors.New("Unsupported file type")
}
failExit: //before this is used, make sure to log or set the error.
fileErrChan <- out
return
successExit:
out.files = []*File{&fil}
fileErrChan <- out
return
}
//AddFilesToPath adds the give os.Files to the given path within the squashfs archive.
//If AllowErrors is true, this will ALWAYS return nil
func (w *Writer) AddFilesToPath(squashfsPath string, files ...*os.File) error {
squashfsPath = path.Clean(squashfsPath)
if strings.HasPrefix(squashfsPath, "/") {
squashfsPath = strings.TrimPrefix(squashfsPath, "/")
}
if squashfsPath == "." {
squashfsPath = "/"
}
fileErrChan := make(chan fileError)
for _, fil := range files {
go w.convertFile(squashfsPath, fil, false, fileErrChan)
}
return errors.New("Not yet ready")
}
//AddFiles adds all files given to the root directory
//If AllowErrors is true, this will ALWAYS return nil
func (w *Writer) AddFiles(files ...*os.File) error {
return w.AddFilesToPath("/", files...)
}
//RemoveFileAt removes the file at filepath from the Writer.
//If multiple files match the given filepath (such as if there are wildcards), all matching files are removed.
//If one or more files are removed, returns true.
func (w *Writer) RemoveFileAt(filepath string) bool {
//TODO
return false
}