Compare commits

...

9 Commits

Author SHA1 Message Date
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
6 changed files with 318 additions and 93 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: CalebQ42
+135
View File
@@ -0,0 +1,135 @@
package rawreader
import (
"bytes"
"errors"
"io"
)
func ConvertReader(r io.Reader) (RawReader, error) {
if rr, ok := r.(RawReader); ok {
return rr, nil
}
if rs, is := r.(io.ReadSeeker); is {
return &fromReadSeeker{
ReadSeeker: rs,
}, nil
}
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r)
if err != nil {
return nil, err
}
return &fromReader{
data: buf.Bytes(),
}, nil
}
func ConvertReaderAt(r io.ReaderAt) RawReader {
if rr, ok := r.(RawReader); ok {
return rr
}
return &fromReaderAt{
ReaderAt: r,
}
}
//TODO: Add way to discard data from fromReader
//RawReader implements the needed interfaces for reading a squashfs archive.
type RawReader interface {
io.ReadSeeker
io.ReaderAt
}
type fromReader struct {
data []byte
off int
}
func (r *fromReader) ReadAt(p []byte, off int64) (n int, err error) {
n = len(p)
if int(off)+len(p) > len(r.data) {
n = len(r.data) - int(off)
err = io.EOF
}
if n < 0 {
n = 0
}
for i := 0; i < n; i++ {
p[i] = r.data[int(off)+i]
}
return
}
func (r *fromReader) Seek(off int64, whence int) (n int64, err error) {
switch whence {
case io.SeekEnd:
r.off = len(r.data) - int(off)
if r.off < 0 {
r.off = 0
err = io.EOF
}
case io.SeekCurrent:
r.off += int(off)
case io.SeekStart:
r.off = int(off)
}
if r.off > len(r.data) {
r.off = len(r.data)
return int64(r.off), io.EOF
}
return int64(r.off), err
}
func (r *fromReader) Read(p []byte) (n int, err error) {
n = len(p)
if r.off+len(p) > len(r.data) {
n = len(r.data) - r.off
err = io.EOF
}
if n < 0 {
n = 0
}
for i := 0; i < n; i++ {
p[i] = r.data[r.off+i]
}
return
}
type fromReadSeeker struct {
io.ReadSeeker
}
func (r *fromReadSeeker) ReadAt(p []byte, off int64) (n int, err error) {
tmp, _ := r.Seek(0, io.SeekCurrent)
defer r.Seek(tmp, io.SeekStart)
_, err = r.Seek(off, io.SeekStart)
if err != nil {
return
}
return r.Read(p)
}
type fromReaderAt struct {
io.ReaderAt
off int
}
func (r *fromReaderAt) Read(p []byte) (n int, err error) {
n, err = r.ReadAt(p, int64(r.off))
r.off += n
return
}
func (r *fromReaderAt) Seek(off int64, whence int) (n int64, err error) {
switch whence {
case io.SeekEnd:
return 0, errors.New("cannot SeekEnd RawReader")
case io.SeekCurrent:
r.off += int(off)
case io.SeekStart:
r.off = int(off)
}
return int64(r.off), nil
}
+91 -70
View File
@@ -9,6 +9,7 @@ import (
"github.com/CalebQ42/squashfs/internal/compression" "github.com/CalebQ42/squashfs/internal/compression"
"github.com/CalebQ42/squashfs/internal/inode" "github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/rawreader"
) )
const ( const (
@@ -20,16 +21,12 @@ var (
errNoMagic = errors.New("magic number doesn't match. Either isn't a squashfs or corrupted") 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 is returned if the compression type in the superblock doesn't work.
errIncompatibleCompression = errors.New("compression type unsupported") errIncompatibleCompression = errors.New("compression type unsupported")
//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")
) )
//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.
//Reader processes and reads a squashfs archive. //Reader processes and reads a squashfs archive.
type Reader struct { type Reader struct {
FS FS
r *io.SectionReader r rawreader.RawReader
decompressor compression.Decompressor decompressor compression.Decompressor
fragOffsets []uint64 fragOffsets []uint64
idTable []uint32 idTable []uint32
@@ -40,140 +37,164 @@ 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) {
var rdr Reader var rdr Reader
err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super) rdr.r = rawreader.ConvertReaderAt(r)
err := rdr.Init()
if err != nil { if err != nil {
return nil, err return nil, err
} }
rdr.r = io.NewSectionReader(r, 0, int64(rdr.super.BytesUsed)) return &rdr, nil
if rdr.super.Magic != magic { }
return nil, errNoMagic
//NewSquashfsReaderFromReader returns a new squashfs.Reader from an io.Reader.
//If the io.Reader implements io.Seeker, the seek functions are used.
//It is NOT recommended to use a pure io.Reader as due to how squashfs
//archives are formatted, the ENTIRETY of the io.Reader's data is loaded into
//memory first before it can be used.
func NewSquashfsReaderFromReader(r io.Reader) (*Reader, error) {
var rdr Reader
var err error
rdr.r, err = rawreader.ConvertReader(r)
if err != nil {
return nil, err
} }
if rdr.super.BlockLog != uint16(math.Log2(float64(rdr.super.BlockSize))) { err = rdr.Init()
return nil, errors.New("BlockSize and BlockLog doesn't match. The archive is probably corrupt") if err != nil {
return nil, err
} }
rdr.r.Seek(96, io.SeekStart) return &rdr, nil
hasUnsupportedOptions := false }
rdr.flags = rdr.super.GetFlags()
if rdr.flags.compressorOptions { func (r *Reader) Init() error {
switch rdr.super.CompressionType { err := binary.Read(r.r, binary.LittleEndian, &r.super)
if err != nil {
return err
}
if r.super.Magic != magic {
return errNoMagic
}
if r.super.BlockLog != uint16(math.Log2(float64(r.super.BlockSize))) {
return errors.New("BlockSize and BlockLog doesn't match. The archive is probably corrupt")
}
r.r.Seek(96, io.SeekStart)
r.flags = r.super.GetFlags()
if r.flags.compressorOptions {
switch r.super.CompressionType {
case GzipCompression: case GzipCompression:
var gzip *compression.Gzip var gzip *compression.Gzip
gzip, err = compression.NewGzipCompressorWithOptions(rdr.r) gzip, err = compression.NewGzipCompressorWithOptions(r.r)
if err != nil { if err != nil {
return nil, err return err
} }
if gzip.HasCustomWindow || gzip.HasStrategies { r.decompressor = gzip
hasUnsupportedOptions = true
}
rdr.decompressor = gzip
case XzCompression: case XzCompression:
var xz *compression.Xz var xz *compression.Xz
xz, err = compression.NewXzCompressorWithOptions(rdr.r) xz, err = compression.NewXzCompressorWithOptions(r.r)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.decompressor = xz r.decompressor = xz
case LzoCompression: case LzoCompression:
var lz *compression.Lzo var lz *compression.Lzo
lz, err = compression.NewLzoCompressorWithOptions(rdr.r) lz, err = compression.NewLzoCompressorWithOptions(r.r)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.decompressor = lz r.decompressor = lz
case Lz4Compression: case Lz4Compression:
var lz4 *compression.Lz4 var lz4 *compression.Lz4
lz4, err = compression.NewLz4CompressorWithOptions(rdr.r) lz4, err = compression.NewLz4CompressorWithOptions(r.r)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.decompressor = lz4 r.decompressor = lz4
case ZstdCompression: case ZstdCompression:
var zstd *compression.Zstd var zstd *compression.Zstd
zstd, err = compression.NewZstdCompressorWithOptions(rdr.r) zstd, err = compression.NewZstdCompressorWithOptions(r.r)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.decompressor = zstd r.decompressor = zstd
default: default:
return nil, errIncompatibleCompression return errIncompatibleCompression
} }
} else { } else {
switch rdr.super.CompressionType { switch r.super.CompressionType {
case GzipCompression: case GzipCompression:
rdr.decompressor = &compression.Gzip{} r.decompressor = &compression.Gzip{}
case LzmaCompression: case LzmaCompression:
rdr.decompressor = &compression.Lzma{} r.decompressor = &compression.Lzma{}
case LzoCompression: case LzoCompression:
rdr.decompressor = &compression.Lzo{} r.decompressor = &compression.Lzo{}
case XzCompression: case XzCompression:
rdr.decompressor = &compression.Xz{} r.decompressor = &compression.Xz{}
case Lz4Compression: case Lz4Compression:
rdr.decompressor = &compression.Lz4{} r.decompressor = &compression.Lz4{}
case ZstdCompression: case ZstdCompression:
rdr.decompressor = &compression.Zstd{} r.decompressor = &compression.Zstd{}
default: default:
//TODO: all compression types. //TODO: all compression types.
return nil, errIncompatibleCompression return errIncompatibleCompression
} }
} }
fragBlocks := int(math.Ceil(float64(rdr.super.FragCount) / 512)) fragBlocks := int(math.Ceil(float64(r.super.FragCount) / 512))
if fragBlocks > 0 { if fragBlocks > 0 {
offset := int64(rdr.super.FragTableStart) offset := int64(r.super.FragTableStart)
for i := 0; i < fragBlocks; i++ { for i := 0; i < fragBlocks; i++ {
tmp := make([]byte, 8) tmp := make([]byte, 8)
_, err = r.ReadAt(tmp, offset) _, err = r.r.ReadAt(tmp, offset)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.fragOffsets = append(rdr.fragOffsets, binary.LittleEndian.Uint64(tmp)) r.fragOffsets = append(r.fragOffsets, binary.LittleEndian.Uint64(tmp))
offset += 8 offset += 8
} }
} }
unread := rdr.super.IDCount unread := r.super.IDCount
blockOffsets := make([]uint64, int(math.Ceil(float64(rdr.super.IDCount)/2048))) blockOffsets := make([]uint64, int(math.Ceil(float64(r.super.IDCount)/2048)))
rdr.r.Seek(int64(rdr.super.IDTableStart), io.SeekStart) _, err = r.r.Seek(int64(r.super.IDTableStart), io.SeekStart)
for i := range blockOffsets {
err = binary.Read(rdr.r, binary.LittleEndian, &blockOffsets[i])
if err != nil { if err != nil {
return nil, err return err
}
for i := range blockOffsets {
err = binary.Read(r.r, binary.LittleEndian, &blockOffsets[i])
if err != nil {
return err
} }
var idRdr *metadataReader var idRdr *metadataReader
idRdr, err = rdr.newMetadataReader(int64(blockOffsets[i])) idRdr, err = r.newMetadataReader(int64(blockOffsets[i]))
if err != nil { if err != nil {
return nil, err return err
} }
read := uint16(math.Min(float64(unread), 2048)) read := uint16(math.Min(float64(unread), 2048))
for i := uint16(0); i < read; i++ { for i := uint16(0); i < read; i++ {
var tmp uint32 var tmp uint32
err = binary.Read(idRdr, binary.LittleEndian, &tmp) err = binary.Read(idRdr, binary.LittleEndian, &tmp)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.idTable = append(rdr.idTable, tmp) r.idTable = append(r.idTable, tmp)
} }
unread -= read unread -= read
} }
metaRdr, err := rdr.newMetadataReaderFromInodeRef(rdr.super.RootInodeRef) metaRdr, err := r.newMetadataReaderFromInodeRef(r.super.RootInodeRef)
if err != nil { if err != nil {
return nil, err return err
} }
i, err := inode.ProcessInode(metaRdr, rdr.super.BlockSize) i, err := inode.ProcessInode(metaRdr, r.super.BlockSize)
if err != nil { if err != nil {
return nil, err return err
} }
entries, err := rdr.readDirFromInode(i) entries, err := r.readDirFromInode(i)
if err != nil { if err != nil {
return nil, err return err
} }
rdr.FS = FS{ r.FS = FS{
r: &rdr, i: i,
r: r,
name: "/", name: "/",
entries: entries, entries: entries,
} }
if hasUnsupportedOptions { return nil
return &rdr, ErrOptions
}
return &rdr, nil
} }
//ModTime is the last time the file was modified/created. //ModTime is the last time the file was modified/created.
+3 -2
View File
@@ -135,10 +135,11 @@ func (f File) FS() (*FS, error) {
return nil, err return nil, err
} }
return &FS{ return &FS{
entries: ents, i: f.i,
parent: f.parent,
r: f.r, r: f.r,
parent: f.parent,
name: f.name, name: f.name,
entries: ents,
}, nil }, nil
} }
+23 -6
View File
@@ -10,11 +10,13 @@ import (
"strings" "strings"
"github.com/CalebQ42/squashfs/internal/directory" "github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
) )
//FS is a fs.FS representation of a squashfs directory. //FS is a fs.FS representation of a squashfs directory.
//Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS //Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS
type FS struct { type FS struct {
i *inode.Inode
r *Reader r *Reader
parent *FS parent *FS
name string name string
@@ -44,8 +46,11 @@ func (f FS) Open(name string) (fs.File, error) {
} }
return f.parent.Open(strings.Join(split[1:], "/")) return f.parent.Open(strings.Join(split[1:], "/"))
} }
if split[0] == "." {
return &File{i: f.i, r: f.r, parent: f.parent, name: f.name}, nil
}
for i := 0; i < len(f.entries); i++ { for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match { if split[0] == f.entries[i].Name {
if len(split) == 1 { if len(split) == 1 {
return f.r.newFileFromDirEntry(f.entries[i], &f) return f.r.newFileFromDirEntry(f.entries[i], &f)
} }
@@ -183,8 +188,12 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
} }
return f.parent.ReadDir(strings.Join(split[1:], "/")) return f.parent.ReadDir(strings.Join(split[1:], "/"))
} }
if split[0] == "." {
f := &File{i: f.i, r: f.r, parent: f.parent, name: f.name}
return f.ReadDir(-1)
}
for i := 0; i < len(f.entries); i++ { for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match { if split[0] == f.entries[i].Name {
if len(split) == 1 { if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i]) in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil { if err != nil {
@@ -202,7 +211,7 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
Err: err, Err: err,
} }
} }
out := make([]fs.DirEntry, len(f.entries)) out := make([]fs.DirEntry, len(ents))
for i, ent := range ents { for i, ent := range ents {
out[i] = &DirEntry{ out[i] = &DirEntry{
en: ent, en: ent,
@@ -299,8 +308,12 @@ func (f FS) Stat(name string) (fs.FileInfo, error) {
} }
return f.parent.Stat(strings.Join(split[1:], "/")) return f.parent.Stat(strings.Join(split[1:], "/"))
} }
if split[0] == "." {
f := &File{i: f.i, r: f.r, parent: f.parent, name: f.name}
return f.Stat()
}
for i := 0; i < len(f.entries); i++ { for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match { if split[0] == f.entries[i].Name {
if len(split) == 1 { if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i]) in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil { if err != nil {
@@ -382,8 +395,11 @@ func (f FS) Sub(dir string) (fs.FS, error) {
} }
return f.parent.Sub(strings.Join(split[1:], "/")) return f.parent.Sub(strings.Join(split[1:], "/"))
} }
if split[0] == "." {
return f, nil
}
for i := 0; i < len(f.entries); i++ { for i := 0; i < len(f.entries); i++ {
if match, _ := path.Match(split[0], f.entries[i].Name); match { if split[0] == f.entries[i].Name {
if len(split) == 1 { if len(split) == 1 {
in, err := f.r.getInodeFromEntry(f.entries[i]) in, err := f.r.getInodeFromEntry(f.entries[i])
if err != nil { if err != nil {
@@ -401,7 +417,8 @@ func (f FS) Sub(dir string) (fs.FS, error) {
Err: err, Err: err,
} }
} }
return &FS{ return FS{
i: in,
r: f.r, r: f.r,
parent: &f, parent: &f,
name: f.entries[i].Name, name: f.entries[i].Name,
+64 -16
View File
@@ -14,9 +14,10 @@ import (
) )
const ( const (
downloadURL = "https://github.com/srevinsaju/Firefox-Appimage/releases/download/firefox-v84.0.r20201221152838/firefox-84.0.r20201221152838-x86_64.AppImage" appImageURL = "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" appImageName = "firefox-84.0.r20201221152838-x86_64.AppImage"
squashfsName = "balenaEtcher-1.5.113-x64.AppImage.sfs" squashfsURL = "https://darkstorm.tech/LinuxPATest.sfs"
squashfsName = "LinuxPATest.sfs"
) )
func TestSquashfs(t *testing.T) { func TestSquashfs(t *testing.T) {
@@ -25,6 +26,13 @@ func TestSquashfs(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
squashFil, err := os.Open(wd + "/testing/" + squashfsName) squashFil, err := os.Open(wd + "/testing/" + squashfsName)
if os.IsNotExist(err) {
err = downloadTestSquash(wd + "/testing")
if err != nil {
t.Fatal(err)
}
squashFil, err = os.Open(wd + "/testing/" + squashfsName)
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -32,19 +40,33 @@ func TestSquashfs(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fmt.Println("stuff", rdr.super.CompressionType) os.RemoveAll(wd + "/testing/" + squashfsName + ".d")
// fil := rdr.GetFileAtPath("*.desktop") op := DefaultOptions()
// if fil == nil { op.Verbose = true
// t.Fatal("Can't find desktop fil") err = rdr.ExtractWithOptions(wd+"/testing/"+squashfsName+".d", op)
// } if err != nil {
// errs := fil.ExtractTo(wd + "/testing") t.Fatal(err)
// if len(errs) > 0 { }
// t.Fatal(errs) t.Fatal("No Problems")
// } }
// errs = rdr.ExtractTo(wd + "/testing/" + squashfsName + ".d")
// if len(errs) > 0 { func TestSquashfsFromReader(t *testing.T) {
// t.Fatal(errs) resp, err := http.DefaultClient.Get(squashfsURL)
// } if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
rdr, err := NewSquashfsReaderFromReader(resp.Body)
if err != nil {
t.Fatal(err)
}
os.RemoveAll("testing/" + squashfsName + ".d")
op := DefaultOptions()
op.Verbose = true
err = rdr.ExtractWithOptions("testing/"+squashfsName+".d", op)
if err != nil {
t.Fatal(err)
}
t.Fatal("No Problems") t.Fatal("No Problems")
} }
@@ -171,7 +193,7 @@ func downloadTestAppImage(dir string) error {
return nil return nil
}, },
} }
resp, err := check.Get(downloadURL) resp, err := check.Get(appImageURL)
if err != nil { if err != nil {
return err return err
} }
@@ -183,6 +205,32 @@ func downloadTestAppImage(dir string) error {
return nil return nil
} }
func downloadTestSquash(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)
sfs, err := os.Create(dir + "/" + squashfsName)
if err != nil {
return err
}
defer sfs.Close()
check := http.Client{
CheckRedirect: func(r *http.Request, _ []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
resp, err := check.Get(squashfsURL)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(sfs, resp.Body)
if err != nil {
return err
}
return nil
}
func TestCreateSquashFromAppImage(t *testing.T) { func TestCreateSquashFromAppImage(t *testing.T) {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {