Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dfcb1cf80 | |||
| c7593eaff3 | |||
| 135403032f | |||
| 5c3bf8d528 |
@@ -1,8 +1,12 @@
|
|||||||
# squashfs [](https://pkg.go.dev/github.com/CalebQ42/squashfs)
|
# squashfs (WIP)
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/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 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.
|
||||||
|
|
||||||
|
The only major thing missing from squashfs reading is Xattr parsing.
|
||||||
|
|
||||||
Special thanks to https://dr-emann.github.io/squashfs/ for some VERY important information in an easy to understand format.
|
Special thanks to https://dr-emann.github.io/squashfs/ for some VERY important information in an easy to understand format.
|
||||||
Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree/master/internal/squashfs) as I referenced it to figure some things out (and double check others).
|
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).
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ require (
|
|||||||
github.com/adrg/xdg v0.2.3 // indirect
|
github.com/adrg/xdg v0.2.3 // indirect
|
||||||
github.com/google/go-cmp v0.5.4 // indirect
|
github.com/google/go-cmp v0.5.4 // indirect
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||||
|
github.com/klauspost/compress v1.11.3
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.1
|
||||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.8
|
github.com/ulikunitz/xz v0.5.8
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
|
||||||
|
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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/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/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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.1 h1:cS6aGkNLJr4u+UwaA21yp+gbWN3WJWtKo1axmPDObMA=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.1/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
|||||||
@@ -3,11 +3,37 @@ package compression
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type gzipInit struct {
|
||||||
|
CompressionLevel int32
|
||||||
|
WindowSize int16
|
||||||
|
Strategies int16
|
||||||
|
}
|
||||||
|
|
||||||
//Gzip is a decompressor for gzip type compression. Uses zlib for compression and decompression
|
//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.
|
//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) {
|
func (g *Gzip) Decompress(r io.Reader) ([]byte, error) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ var (
|
|||||||
errIncompatibleCompression = errors.New("Compression type unsupported")
|
errIncompatibleCompression = errors.New("Compression type unsupported")
|
||||||
//ErrCompressorOptions is returned if compressor options is present. It's not currently supported.
|
//ErrCompressorOptions is returned if compressor options is present. It's not currently supported.
|
||||||
errCompressorOptions = errors.New("Compressor options is 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.
|
//Reader processes and reads a squashfs archive.
|
||||||
@@ -35,6 +37,7 @@ type Reader struct {
|
|||||||
|
|
||||||
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
|
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
|
||||||
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
||||||
|
hasUnsupportedOptions := false
|
||||||
var rdr Reader
|
var rdr Reader
|
||||||
rdr.r = r
|
rdr.r = r
|
||||||
err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super)
|
err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super)
|
||||||
@@ -50,8 +53,17 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
|||||||
rdr.flags = rdr.super.GetFlags()
|
rdr.flags = rdr.super.GetFlags()
|
||||||
if rdr.flags.CompressorOptions {
|
if rdr.flags.CompressorOptions {
|
||||||
switch rdr.super.CompressionType {
|
switch rdr.super.CompressionType {
|
||||||
|
case gzipCompression:
|
||||||
|
gzip, err := compression.NewGzipCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if gzip.HasCustomWindow || gzip.HasStrategies {
|
||||||
|
hasUnsupportedOptions = true
|
||||||
|
}
|
||||||
|
rdr.decompressor = gzip
|
||||||
case xzCompression:
|
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.
|
xz, err := compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,6 +71,21 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
|||||||
return nil, errors.New("XZ compression options has filters. These are not yet supported")
|
return nil, errors.New("XZ compression options has filters. These are not yet supported")
|
||||||
}
|
}
|
||||||
rdr.decompressor = xz
|
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:
|
default:
|
||||||
return nil, errCompressorOptions
|
return nil, errCompressorOptions
|
||||||
}
|
}
|
||||||
@@ -70,6 +97,10 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
|||||||
rdr.decompressor = &compression.Lzma{}
|
rdr.decompressor = &compression.Lzma{}
|
||||||
case xzCompression:
|
case xzCompression:
|
||||||
rdr.decompressor = &compression.Xz{}
|
rdr.decompressor = &compression.Xz{}
|
||||||
|
case lz4Compression:
|
||||||
|
rdr.decompressor = &compression.Lz4{}
|
||||||
|
case zstdCompression:
|
||||||
|
rdr.decompressor = &compression.Zstd{}
|
||||||
default:
|
default:
|
||||||
//TODO: all compression types.
|
//TODO: all compression types.
|
||||||
return nil, errIncompatibleCompression
|
return nil, errIncompatibleCompression
|
||||||
@@ -111,9 +142,21 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
|
|||||||
}
|
}
|
||||||
unread -= read
|
unread -= read
|
||||||
}
|
}
|
||||||
|
if hasUnsupportedOptions {
|
||||||
|
return &rdr, ErrOptions
|
||||||
|
}
|
||||||
return &rdr, nil
|
return &rdr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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.
|
//GetRootFolder returns a squashfs.File that references the root directory of the squashfs archive.
|
||||||
func (r *Reader) GetRootFolder() (root *File, err error) {
|
func (r *Reader) GetRootFolder() (root *File, err error) {
|
||||||
root = new(File)
|
root = new(File)
|
||||||
|
|||||||
+1
-5
@@ -58,11 +58,7 @@ func TestAppImage(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fil := rdr.GetFileAtPath(".DirIcon")
|
errs := rdr.ExtractTo(wd + "/testing/cool-retro")
|
||||||
if fil == nil {
|
|
||||||
t.Fatal("Can't find desktop file")
|
|
||||||
}
|
|
||||||
errs := fil.ExtractSymlink(wd + "/testing/")
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user