diff --git a/direntry_fileinfo.go b/direntry_fileinfo.go deleted file mode 100644 index a8ab09f..0000000 --- a/direntry_fileinfo.go +++ /dev/null @@ -1,137 +0,0 @@ -package squashfs - -import ( - "io" - "io/fs" - "time" - - "github.com/CalebQ42/squashfs/internal/directory" - "github.com/CalebQ42/squashfs/internal/inode" -) - -//DirEntry is a child of a directory. -type DirEntry struct { - en *directory.Entry - parent *FS - r *Reader -} - -func (r *Reader) newDirEntry(en *directory.Entry, parent *FS) *DirEntry { - return &DirEntry{ - en: en, - parent: parent, - r: r, - } -} - -//Name returns the DirEntry's name -func (d DirEntry) Name() string { - return d.en.Name -} - -//IsDir Yep. -func (d DirEntry) IsDir() bool { - return d.en.Type == inode.DirType -} - -//Type returns the type bits of fs.FileMode of the DirEntry. -func (d DirEntry) Type() fs.FileMode { - switch d.en.Type { - case inode.DirType: - return fs.ModeDir - case inode.SymType: - return fs.ModeSymlink - default: - return 0 - } -} - -//Info returns the fs.FileInfo for the given DirEntry. -func (d DirEntry) Info() (fs.FileInfo, error) { - in, err := d.r.getInodeFromEntry(d.en) - if err != nil { - return nil, err - } - return &FileInfo{ - name: d.en.Name, - i: in, - parent: d.parent, - r: d.r, - }, nil -} - -//GetInodeFromEntry returns the inode associated with a given directory.Entry -func (r *Reader) getInodeFromEntry(en *directory.Entry) (*inode.Inode, error) { - br, err := r.newMetadataReader(int64(r.super.InodeTableStart + uint64(en.InodeOffset))) - if err != nil { - return nil, err - } - _, err = br.Seek(int64(en.InodeBlockOffset), io.SeekStart) - if err != nil { - return nil, err - } - i, err := inode.ProcessInode(br, r.super.BlockSize) - if err != nil { - return nil, err - } - return i, nil -} - -//FileInfo is a fs.FileInfo for a file. -type FileInfo struct { - i *inode.Inode - parent *FS - r *Reader - name string -} - -//Name is the file's name. -func (f FileInfo) Name() string { - return f.name -} - -//Size is the file's size if it's a regular file. Otherwise, returns 0. -func (f FileInfo) Size() int64 { - switch f.i.Type { - case inode.FileType: - return int64(f.i.Info.(inode.File).Size) - case inode.ExtFileType: - return int64(f.i.Info.(inode.ExtFile).Size) - } - return 0 -} - -//Mode returns the fs.FileMode bits of the file. -func (f FileInfo) Mode() fs.FileMode { - mode := fs.FileMode(f.i.Permissions) - switch f.i.Type { - case inode.DirType | inode.ExtDirType: - return mode | fs.ModeDir - case inode.ExtDirType: - return mode | fs.ModeDir - case inode.SymType: - return mode | fs.ModeSymlink - case inode.ExtSymType: - return mode | fs.ModeSymlink - } - return mode -} - -//ModTime is the last time the file was modified. -func (f FileInfo) ModTime() time.Time { - return time.Unix(int64(f.i.ModifiedTime), 0) -} - -//IsDir yep. -func (f FileInfo) IsDir() bool { - return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType -} - -//Sys returns the File for the FileInfo. If something goes wrong, nil is returned. -func (f FileInfo) Sys() interface{} { - fil, err := f.File() - if err != nil { - return nil - } - return fil -} diff --git a/fileinfo.go b/fileinfo.go new file mode 100644 index 0000000..0670db3 --- /dev/null +++ b/fileinfo.go @@ -0,0 +1,67 @@ +package squashfs + +import ( + "io/fs" + "time" + + "github.com/CalebQ42/squashfs/internal/directory" + "github.com/CalebQ42/squashfs/internal/inode" +) + +type FileInfo struct { + e directory.Entry + size int64 + perm uint32 + modTime uint32 +} + +func (r Reader) newFileInfo(e directory.Entry) (FileInfo, error) { + i, err := r.inodeFromDir(e) + if err != nil { + return FileInfo{}, err + } + return newFileInfo(e, i), nil +} + +func newFileInfo(e directory.Entry, i inode.Inode) FileInfo { + var size int64 + if i.Type == inode.Fil { + size = int64(i.Data.(inode.File).Size) + } else if i.Type == inode.EFil { + size = int64(i.Data.(inode.EFile).Size) + } + return FileInfo{ + e: e, + size: size, + perm: uint32(i.Perm), + modTime: i.ModTime, + } +} + +func (f FileInfo) Name() string { + return f.e.Name +} + +func (f FileInfo) Size() int64 { + return f.size +} + +func (f FileInfo) Mode() fs.FileMode { + if f.IsDir() { + return fs.FileMode(f.perm | uint32(fs.ModeDir)) + } + return fs.FileMode(f.perm) +} + +func (f FileInfo) ModTime() time.Time { + return time.Unix(int64(f.modTime), 0) +} + +func (f FileInfo) IsDir() bool { + return f.e.Type == inode.Dir +} + +func (f FileInfo) Sys() any { + //TODO + return nil +} diff --git a/fragment.go b/fragment.go index 067b65d..ddac0e7 100644 --- a/fragment.go +++ b/fragment.go @@ -1,80 +1,22 @@ package squashfs import ( - "encoding/binary" - "errors" "io" - "github.com/CalebQ42/squashfs/internal/inode" + "github.com/CalebQ42/squashfs/internal/toreader" ) -//FragmentEntry is an entry in the fragment table -type fragmentEntry struct { +type fragEntry struct { Start uint64 Size uint32 - _ uint32 //unused + _ uint32 } -//GetFragmentDataFromInode returns the fragment data for a given inode. -//If the inode does not have a fragment, harmlessly returns an empty slice without an error. -func (r *Reader) getFragmentDataFromInode(in *inode.Inode) ([]byte, error) { - var size uint64 - var fragIndex uint32 - var fragOffset uint32 - if in.Type == inode.FileType { - bf := in.Info.(inode.File) - if !bf.Fragmented { - return make([]byte, 0), nil - } - if bf.BlockStart == 0 { - size = uint64(bf.Size) - } else { - size = uint64(bf.BlockSizes[len(bf.BlockSizes)-1]) - } - fragIndex = bf.FragmentIndex - fragOffset = bf.FragmentOffset - } else if in.Type == inode.ExtFileType { - bf := in.Info.(inode.ExtFile) - if !bf.Fragmented { - return make([]byte, 0), nil - } - if bf.BlockStart == 0 { - size = bf.Size - } else { - size = uint64(bf.BlockSizes[len(bf.BlockSizes)-1]) - } - fragIndex = bf.FragmentIndex - fragOffset = bf.FragmentOffset - } else { - return nil, errors.New("inode type not supported") +func (r Reader) fragReader(index uint32) (io.Reader, error) { + realSize := r.fragEntries[index].Size &^ (1 << 24) + rdr := io.LimitReader(toreader.NewReader(r.r, int64(r.fragEntries[index].Start)), int64(realSize)) + if realSize == r.fragEntries[index].Size { + return rdr, nil } - //reading the fragment entry first - fragEntryRdr, err := r.newMetadataReader(int64(r.fragOffsets[int(fragIndex/512)])) - if err != nil { - return nil, err - } - _, err = fragEntryRdr.Seek(int64(16*fragIndex), io.SeekStart) - if err != nil { - return nil, err - } - var entry fragmentEntry - err = binary.Read(fragEntryRdr, binary.LittleEndian, &entry) - if err != nil { - return nil, err - } - //now reading the actual fragment - dr, err := r.newDataReader(int64(entry.Start), []uint32{entry.Size}) - if err != nil { - return nil, err - } - _, err = dr.Read(make([]byte, fragOffset)) - if err != nil { - return nil, err - } - tmp := make([]byte, size) - err = binary.Read(dr, binary.LittleEndian, &tmp) - if err != nil { - return nil, err - } - return tmp, nil + return r.d.Reader(rdr) } diff --git a/go.mod b/go.mod index 49a8c66..f4bdfa1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/CalebQ42/squashfs -go 1.17 +go 1.18 require ( github.com/CalebQ42/GoAppImage v0.5.0 diff --git a/internal/compression/gzip.go b/internal/compression/gzip.go deleted file mode 100644 index 8e921c9..0000000 --- a/internal/compression/gzip.go +++ /dev/null @@ -1,76 +0,0 @@ -package compression - -import ( - "bytes" - "encoding/binary" - "io" - - "github.com/klauspost/compress/zlib" -) - -type gzipInit struct { - CompressionLevel int32 - WindowSize int16 - Strategies int16 -} - -//Gzip is a decompressor for gzip type compression. Uses zlib for compression and decompression -type Gzip struct { - wrt *zlib.Writer - gzipInit - HasCustomWindow bool - HasStrategies bool -} - -//NewGzipCompressorWithOptions creates a new gzip compressor/decompressor with options read from the given reader. -func NewGzipCompressorWithOptions(r io.Reader) (*Gzip, error) { - var gzip Gzip - err := binary.Read(r, binary.LittleEndian, &gzip.gzipInit) - if err != nil { - return nil, err - } - //TODO: proper support for window size and strategies - gzip.HasCustomWindow = gzip.WindowSize != 15 - gzip.HasStrategies = gzip.Strategies != 0 && gzip.Strategies != 1 - return &gzip, nil -} - -//Decompress reads the entirety of the given reader and returns it uncompressed as a byte slice. -func (g *Gzip) Decompress(r io.Reader) ([]byte, error) { - rdr, err := zlib.NewReader(r) - if err != nil { - return nil, err - } - var data bytes.Buffer - _, err = io.Copy(&data, rdr) - if err != nil { - return nil, err - } - return data.Bytes(), nil -} - -//Compress compresses the given data (as a byte array) and returns the compressed data. -func (g *Gzip) Compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - var err error - if g.wrt == nil { - if g.CompressionLevel == 0 { - g.wrt = zlib.NewWriter(&buf) - } else { - g.wrt, err = zlib.NewWriterLevel(&buf, int(g.CompressionLevel)) - if err != nil { - return nil, err - } - } - } - wrt, err := zlib.NewWriterLevel(&buf, int(g.CompressionLevel)) - if err != nil { - return nil, err - } - _, err = wrt.Write(data) - if err != nil { - return nil, err - } - wrt.Close() - return buf.Bytes(), nil -} diff --git a/internal/compression/lz4.go b/internal/compression/lz4.go deleted file mode 100644 index 3f0e4d8..0000000 --- a/internal/compression/lz4.go +++ /dev/null @@ -1,55 +0,0 @@ -package compression - -import ( - "bytes" - "encoding/binary" - "io" - - "github.com/pierrec/lz4/v4" -) - -//Lz4 is a Lz4 Compressor/Decompressor -type Lz4 struct { - HC bool -} - -//NewLz4CompressorWithOptions creates a new lz4 compressor/decompressor with options read from the given reader. -func NewLz4CompressorWithOptions(r io.Reader) (*Lz4, error) { - var lz4 Lz4 - var init struct { - Version int32 - Flags int32 - } - err := binary.Read(r, binary.LittleEndian, &init) - if err != nil { - return nil, err - } - lz4.HC = init.Flags == 1 - return &lz4, nil -} - -//Decompress decompresses all data from r and returns the uncompressed bytes -func (l *Lz4) Decompress(r io.Reader) ([]byte, error) { - rdr := lz4.NewReader(r) - var buf bytes.Buffer - _, err := io.Copy(&buf, rdr) - return buf.Bytes(), err -} - -//Compress implements compression.Compress -func (l *Lz4) Compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - w := lz4.NewWriter(&buf) - if l.HC { - err := w.Apply(lz4.CompressionLevelOption(lz4.Level9)) - if err != nil { - return nil, err - } - } - _, err := w.Write(data) - if err != nil { - return nil, err - } - w.Close() - return buf.Bytes(), nil -} diff --git a/internal/compression/lzma.go b/internal/compression/lzma.go deleted file mode 100644 index c5de987..0000000 --- a/internal/compression/lzma.go +++ /dev/null @@ -1,40 +0,0 @@ -package compression - -import ( - "bytes" - "io" - - "github.com/ulikunitz/xz/lzma" -) - -//Lzma is a lzma decompressor -type Lzma struct{} - -//Decompress decompresses all the data in the given reader and returns the uncompressed bytes. -func (l *Lzma) Decompress(rdr io.Reader) ([]byte, error) { - r, err := lzma.NewReader(rdr) - if err != nil { - return nil, err - } - var buf bytes.Buffer - _, err = io.Copy(&buf, r) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -//Compress implements compression.Compress -func (l *Lzma) Compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - w, err := lzma.NewWriter(&buf) - if err != nil { - return nil, err - } - _, err = w.Write(data) - if err != nil { - return nil, err - } - w.Close() - return buf.Bytes(), nil -} diff --git a/internal/compression/lzo.go b/internal/compression/lzo.go deleted file mode 100644 index 740646b..0000000 --- a/internal/compression/lzo.go +++ /dev/null @@ -1,30 +0,0 @@ -package compression - -import ( - "encoding/binary" - "io" - - lzo "github.com/rasky/go-lzo" -) - -type Lzo struct { - Algorithm int32 - Level int32 -} - -func NewLzoCompressorWithOptions(rdr io.Reader) (*Lzo, error) { - var lz Lzo - err := binary.Read(rdr, binary.LittleEndian, &lz) - if err != nil { - return nil, err - } - return &lz, nil -} - -func (l Lzo) Decompress(rdr io.Reader) ([]byte, error) { - byt, err := lzo.Decompress1X(rdr, 0, 0) - if err != nil { - return nil, err - } - return byt, nil -} diff --git a/internal/compression/types.go b/internal/compression/types.go deleted file mode 100644 index 14442a8..0000000 --- a/internal/compression/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package compression - -import "io" - -//Compressor is a squashfs decompressor interface. Allows for easy compression. -type Compressor interface { - Compress([]byte) ([]byte, error) -} - -//Decompressor is a squashfs decompressor interface. Allows for easy decompression no matter the type of compression. -type Decompressor interface { - Decompress(io.Reader) ([]byte, error) -} diff --git a/internal/compression/xz.go b/internal/compression/xz.go deleted file mode 100644 index 272d11c..0000000 --- a/internal/compression/xz.go +++ /dev/null @@ -1,60 +0,0 @@ -package compression - -import ( - "bytes" - "encoding/binary" - "io" - - "github.com/therootcompany/xz" - - wrtXz "github.com/ulikunitz/xz" -) - -type Xz struct { - DictionarySize int32 - Filters int32 -} - -//NewXzCompressorWithOptions creates a new Xz compressor/decompressor that reads the compressor options from the given reader. -func NewXzCompressorWithOptions(rdr io.Reader) (*Xz, error) { - var x Xz - err := binary.Read(rdr, binary.LittleEndian, &x) - if err != nil { - return nil, err - } - return &x, nil -} - -//Decompress decompresses all the data from the rdr and returns the uncompressed bytes. -func (x *Xz) Decompress(rdr io.Reader) ([]byte, error) { - r, err := xz.NewReader(rdr, 0) - if err != nil { - return nil, err - } - var buf bytes.Buffer - _, err = io.Copy(&buf, r) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -//Compress implements compression.Compress -func (x *Xz) Compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - w, err := wrtXz.NewWriter(&buf) - if err != nil { - return nil, err - } - w.DictCap = int(x.DictionarySize) - err = w.Verify() - if err != nil { - return nil, err - } - _, err = w.Write(data) - if err != nil { - return nil, err - } - w.Close() - return buf.Bytes(), nil -} diff --git a/internal/compression/zstd.go b/internal/compression/zstd.go deleted file mode 100644 index 074cc78..0000000 --- a/internal/compression/zstd.go +++ /dev/null @@ -1,51 +0,0 @@ -package compression - -import ( - "bytes" - "encoding/binary" - "io" - - "github.com/klauspost/compress/zstd" -) - -//Zstd is a zstd compressor/decompressor -type Zstd struct { - CompressionLevel int32 -} - -//NewZstdCompressorWithOptions creates a new Zstd with options read from the given reader -func NewZstdCompressorWithOptions(r io.Reader) (*Zstd, error) { - var zstd Zstd - err := binary.Read(r, binary.LittleEndian, &zstd) - if err != nil { - return nil, err - } - return &zstd, nil -} - -//Decompress decompresses all data from the reader and returns the uncompressed data -func (z *Zstd) Decompress(r io.Reader) ([]byte, error) { - rdr, err := zstd.NewReader(r) - if err != nil { - return nil, err - } - defer rdr.Close() - var buf bytes.Buffer - _, err = io.Copy(&buf, rdr) - return buf.Bytes(), err -} - -//Compress impelements compression.Compress -func (z *Zstd) Compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - w, err := zstd.NewWriter(&buf, zstd.WithEncoderLevel(zstd.EncoderLevel(z.CompressionLevel))) - if err != nil { - return nil, err - } - _, err = w.Write(data) - if err != nil { - return nil, err - } - w.Close() - return buf.Bytes(), nil -} diff --git a/internal/data/fullreader.go b/internal/data/fullreader.go new file mode 100644 index 0000000..2127862 --- /dev/null +++ b/internal/data/fullreader.go @@ -0,0 +1,115 @@ +package data + +import ( + "io" + + "github.com/CalebQ42/squashfs/internal/decompress" + "github.com/CalebQ42/squashfs/internal/toreader" +) + +type FullReader struct { + r io.ReaderAt + d decompress.Decompressor + fragRdr io.Reader + sizes []uint32 + blockSize uint32 + start uint64 +} + +func NewFullReader(r io.ReaderAt, start uint64, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) *FullReader { + return &FullReader{ + r: r, + start: start, + blockSize: blockSize, + sizes: blockSizes, + d: d, + } +} + +func (r *FullReader) AddFragment(rdr io.Reader) { + r.fragRdr = rdr +} + +type outDat struct { + err error + data []byte + i int +} + +func (r FullReader) process(index int, offset int64, out chan outDat) { + var err error + var dat []byte + size := realSize(r.sizes[index]) + rdr := io.LimitReader(toreader.NewReader(r.r, offset), int64(size)) + if size == r.sizes[index] { + rdr, err = r.d.Reader(rdr) + } + if err == nil { + dat, err = io.ReadAll(rdr) + } + out <- outDat{ + i: index, + err: err, + data: dat, + } + if clr, ok := rdr.(io.Closer); ok { + clr.Close() + } +} + +func (r FullReader) WriteTo(w io.Writer) (n int64, err error) { + out := make(chan outDat, len(r.sizes)) + offset := r.start + num := len(r.sizes) + for i := 0; i < num; i++ { + if i == num-1 && r.fragRdr != nil { + go func() { + dat, e := io.ReadAll(r.fragRdr) + out <- outDat{ + i: num, + err: e, + data: dat, + } + if clr, ok := r.fragRdr.(io.Closer); ok { + clr.Close() + } + }() + continue + } + go r.process(i, int64(offset), out) + offset += uint64(realSize(r.sizes[i])) + } + cache := make(map[int]outDat) + var tmpN int + for cur := 0; cur < num; { + dat := <-out + if dat.err != nil { + err = dat.err + return + } + if dat.i != cur { + cache[dat.i] = dat + continue + } + tmpN, err = w.Write(dat.data) + n += int64(tmpN) + if err != nil { + return + } + cur++ + var ok bool + for { + dat, ok = cache[cur] + if !ok { + break + } + tmpN, err = w.Write(dat.data) + n += int64(tmpN) + if err != nil { + return + } + cur++ + } + } + return +} diff --git a/internal/data/reader.go b/internal/data/reader.go new file mode 100644 index 0000000..1befcfe --- /dev/null +++ b/internal/data/reader.go @@ -0,0 +1,77 @@ +package data + +import ( + "bytes" + "io" + + "github.com/CalebQ42/squashfs/internal/decompress" +) + +type Reader struct { + master io.Reader + cur io.Reader + fragRdr io.Reader + d decompress.Decompressor + blockSizes []uint32 + blockSize uint32 +} + +func NewReader(r io.Reader, d decompress.Decompressor, blockSizes []uint32, blockSize uint32) (*Reader, error) { + var out Reader + out.d = d + out.master = r + out.blockSizes = blockSizes + out.blockSize = blockSize + err := out.advance() + return &out, err +} + +func (r *Reader) AddFragment(rdr io.Reader) { + r.fragRdr = rdr +} + +func realSize(siz uint32) uint32 { + return siz &^ (1 << 24) +} + +func (r *Reader) advance() (err error) { + if clr, ok := r.cur.(io.Closer); ok { + clr.Close() + } + if len(r.blockSizes) == 0 { + return io.EOF + } + if len(r.blockSizes) == 1 && r.fragRdr != nil { + r.cur = r.fragRdr + } else { + size := realSize(r.blockSizes[0]) + if size == 0 { + r.cur = bytes.NewReader(make([]byte, r.blockSize)) + } else { + r.cur = io.LimitReader(r.master, int64(size)) + if size == r.blockSizes[0] { + r.cur, err = r.d.Reader(r.cur) + } + } + } + r.blockSizes = r.blockSizes[1:] + return +} + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.cur.Read(p) + if err == io.EOF { + err = r.advance() + if err != nil { + return + } + var tmpN int + tmp := make([]byte, len(p)-n) + tmpN, err = r.Read(tmp) + for i := range tmp { + p[n+i] = tmp[i] + } + n += tmpN + } + return +} diff --git a/internal/decompress/gzip.go b/internal/decompress/gzip.go new file mode 100644 index 0000000..c7ced32 --- /dev/null +++ b/internal/decompress/gzip.go @@ -0,0 +1,13 @@ +package decompress + +import ( + "io" + + "github.com/klauspost/compress/zlib" +) + +type GZip struct{} + +func (g GZip) Reader(src io.Reader) (io.ReadCloser, error) { + return zlib.NewReader(src) +} diff --git a/internal/decompress/interface.go b/internal/decompress/interface.go new file mode 100644 index 0000000..177e45d --- /dev/null +++ b/internal/decompress/interface.go @@ -0,0 +1,7 @@ +package decompress + +import "io" + +type Decompressor interface { + Reader(src io.Reader) (io.ReadCloser, error) +} diff --git a/internal/decompress/lz4.go b/internal/decompress/lz4.go new file mode 100644 index 0000000..af399ba --- /dev/null +++ b/internal/decompress/lz4.go @@ -0,0 +1,13 @@ +package decompress + +import ( + "io" + + "github.com/pierrec/lz4/v4" +) + +type Lz4 struct{} + +func (l Lz4) Reader(r io.Reader) (io.ReadCloser, error) { + return io.NopCloser(lz4.NewReader(r)), nil +} diff --git a/internal/decompress/lzma.go b/internal/decompress/lzma.go new file mode 100644 index 0000000..9add4ae --- /dev/null +++ b/internal/decompress/lzma.go @@ -0,0 +1,14 @@ +package decompress + +import ( + "io" + + "github.com/ulikunitz/xz/lzma" +) + +type Lzma struct{} + +func (l Lzma) Reader(r io.Reader) (io.ReadCloser, error) { + rdr, err := lzma.NewReader(r) + return io.NopCloser(rdr), err +} diff --git a/internal/decompress/lzo.go b/internal/decompress/lzo.go new file mode 100644 index 0000000..76333e7 --- /dev/null +++ b/internal/decompress/lzo.go @@ -0,0 +1,18 @@ +package decompress + +import ( + "bytes" + "io" + + "github.com/rasky/go-lzo" +) + +type Lzo struct{} + +func (l Lzo) Reader(r io.Reader) (io.ReadCloser, error) { + cache, err := lzo.Decompress1X(r, 0, 0) + if err != nil { + return nil, err + } + return io.NopCloser(bytes.NewReader(cache)), nil +} diff --git a/internal/decompress/xz.go b/internal/decompress/xz.go new file mode 100644 index 0000000..9a22256 --- /dev/null +++ b/internal/decompress/xz.go @@ -0,0 +1,14 @@ +package decompress + +import ( + "io" + + "github.com/therootcompany/xz" +) + +type Xz struct{} + +func (x Xz) Reader(r io.Reader) (io.ReadCloser, error) { + rdr, err := xz.NewReader(r, 0) + return io.NopCloser(rdr), err +} diff --git a/internal/decompress/zstd.go b/internal/decompress/zstd.go new file mode 100644 index 0000000..64a2026 --- /dev/null +++ b/internal/decompress/zstd.go @@ -0,0 +1,14 @@ +package decompress + +import ( + "io" + + "github.com/klauspost/compress/zstd" +) + +type Zstd struct{} + +func (z Zstd) Reader(src io.Reader) (io.ReadCloser, error) { + r, err := zstd.NewReader(src) + return r.IOReadCloser(), err +} diff --git a/internal/directory/directory.go b/internal/directory/directory.go index b7afee1..2632ae0 100644 --- a/internal/directory/directory.go +++ b/internal/directory/directory.go @@ -6,82 +6,73 @@ import ( "io" ) -//Header is the header for a directory in the directory table -type Header struct { - Count uint32 - InodeOffset uint32 - InodeNumber uint32 +type header struct { + Entries uint32 + InodeStart uint32 + Num uint32 } -//EntryRaw is the values that can be easily decoded -type EntryRaw struct { - Offset uint16 - InodeOffset int16 - Type uint16 - NameSize uint16 +type entryInit struct { + Offset uint16 + NumOffset int16 + Type uint16 + NameSize uint16 +} + +type entry struct { + entryInit + Name []byte } -//Entry is an entry in a directory. type Entry struct { - Name string - InodeOffset uint32 - InodeBlockOffset uint16 - Type uint16 + Name string + BlockStart uint32 + Type uint16 + Offset uint16 } -//NewEntry creates a new directory entry -func NewEntry(rdr io.Reader) (*Entry, error) { - var raw EntryRaw - err := binary.Read(rdr, binary.LittleEndian, &raw) +func readEntry(r io.Reader) (e entry, err error) { + err = binary.Read(r, binary.LittleEndian, &e.entryInit) if err != nil { - return nil, err - } - tmp := make([]byte, raw.NameSize+1) - err = binary.Read(rdr, binary.LittleEndian, &tmp) - if err != nil { - return nil, err - } - return &Entry{ - InodeBlockOffset: raw.Offset, - Type: raw.Type, - Name: string(tmp), - }, nil -} - -//NewDirectory reads the directory from rdr -func NewDirectory(base io.Reader, size uint32) (entries []*Entry, err error) { - tmp := make([]byte, size) - base.Read(tmp) - rdr := bytes.NewBuffer(tmp) - for { - var hdr Header - err = binary.Read(rdr, binary.LittleEndian, &hdr) - if err == io.ErrUnexpectedEOF { - err = nil - break - } else if err != nil { - return nil, err - } - hdr.Count++ - headers := hdr.Count / 256 - if hdr.Count%256 > 0 { - headers++ - } - for i := uint32(0); i < hdr.Count; i++ { - if i != 0 && i%256 == 0 { - err = binary.Read(rdr, binary.LittleEndian, &hdr) - if err != nil { - return nil, err - } - } - var ent *Entry - ent, err = NewEntry(rdr) - if err != nil { - return nil, err - } - ent.InodeOffset = hdr.InodeOffset - entries = append(entries, ent) - } + return } + e.Name = make([]byte, e.NameSize+1) + err = binary.Read(r, binary.LittleEndian, &e.Name) return } + +func ReadEntries(rdr io.Reader, size uint32) (e []Entry, err error) { + dat := make([]byte, size) + rdr.Read(dat) + r := bytes.NewReader(dat) + var h header + var en entry + for { + err = binary.Read(r, binary.LittleEndian, &h) + if err == io.ErrUnexpectedEOF { + err = nil + return + } else if err != nil { + return + } + h.Entries++ + for i := 0; i < int(h.Entries); i++ { + if i != 0 && i%256 == 0 { + err = binary.Read(r, binary.LittleEndian, &h) + if err != nil { + return + } + } + en, err = readEntry(r) + if err != nil { + return + } + e = append(e, Entry{ + Name: string(en.Name), + BlockStart: h.InodeStart, + Type: en.Type, + Offset: en.Offset, + }) + } + } +} diff --git a/internal/inode/dir.go b/internal/inode/dir.go new file mode 100644 index 0000000..bffe973 --- /dev/null +++ b/internal/inode/dir.go @@ -0,0 +1,65 @@ +package inode + +import ( + "encoding/binary" + "io" +) + +type Directory struct { + BlockStart uint32 + LinkCount uint32 + Size uint16 + Offset uint16 + ParentNum uint32 +} + +type eDirectoryInit struct { + LinkCount uint32 + Size uint32 + BlockStart uint32 + ParentNum uint32 + IndCount uint16 + Offset uint16 + XattrInd uint32 +} + +type EDirectory struct { + eDirectoryInit + Indexes []DirectoryIndex +} + +type directoryIndexInit struct { + Ind uint32 + Start uint32 + NameSize uint32 +} + +type DirectoryIndex struct { + directoryIndexInit + Name []byte +} + +func ReadDir(r io.Reader) (d Directory, err error) { + err = binary.Read(r, binary.LittleEndian, &d) + return +} + +func ReadEDir(r io.Reader) (d EDirectory, err error) { + err = binary.Read(r, binary.LittleEndian, &d.eDirectoryInit) + if err != nil { + return + } + d.Indexes = make([]DirectoryIndex, d.IndCount) + for i := range d.Indexes { + err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].directoryIndexInit) + if err != nil { + return + } + d.Indexes[i].Name = make([]byte, d.Indexes[i].NameSize+1) + err = binary.Read(r, binary.LittleEndian, &d.Indexes[i].Name) + if err != nil { + return + } + } + return +} diff --git a/internal/inode/file.go b/internal/inode/file.go new file mode 100644 index 0000000..57b5f33 --- /dev/null +++ b/internal/inode/file.go @@ -0,0 +1,54 @@ +package inode + +import ( + "encoding/binary" + "io" + "math" +) + +type fileInit struct { + BlockStart uint32 + FragInd uint32 + Offset uint32 + Size uint32 +} + +type File struct { + fileInit + BlockSizes []uint32 +} + +type eFileInit struct { + BlockStart uint32 + Size uint64 + Sparse uint64 + LinkCount uint32 + FragInd uint32 + Offset uint32 + XattrInd uint32 +} + +type EFile struct { + eFileInit + BlockSizes []uint32 +} + +func ReadFile(r io.Reader, blockSize uint32) (f File, err error) { + err = binary.Read(r, binary.LittleEndian, &f.fileInit) + if err != nil { + return + } + f.BlockSizes = make([]uint32, int(math.Ceil(float64(f.Size)/float64(blockSize)))) + err = binary.Read(r, binary.LittleEndian, &f.BlockSizes) + return +} + +func ReadEFile(r io.Reader, blockSize uint32) (f EFile, err error) { + err = binary.Read(r, binary.LittleEndian, &f.eFileInit) + if err != nil { + return + } + f.BlockSizes = make([]uint32, int(math.Ceil(float64(f.Size)/float64(blockSize)))) + err = binary.Read(r, binary.LittleEndian, &f.BlockSizes) + return +} diff --git a/internal/inode/inode.go b/internal/inode/inode.go new file mode 100644 index 0000000..d5264ea --- /dev/null +++ b/internal/inode/inode.go @@ -0,0 +1,78 @@ +package inode + +import ( + "encoding/binary" + "errors" + "io" +) + +const ( + Dir = uint16(iota + 1) + Fil + Sym + Block + Char + Fifo + Sock + EDir + EFil + ESym + EBlock + EChar + EFifo + ESock +) + +type Header struct { + Type uint16 + Perm uint16 + UidInd uint16 + GidInd uint16 + ModTime uint32 + Num uint32 +} + +type Inode struct { + Header + Data any +} + +func Read(r io.Reader, blockSize uint32) (i Inode, err error) { + err = binary.Read(r, binary.LittleEndian, &i.Header) + if err != nil { + return + } + switch i.Type { + case Dir: + i.Data, err = ReadDir(r) + case Fil: + i.Data, err = ReadFile(r, blockSize) + case Sym: + i.Data, err = ReadSym(r) + case Block: + fallthrough + case Char: + i.Data, err = ReadDevice(r) + case Fifo: + fallthrough + case Sock: + i.Data, err = ReadIPC(r) + case EDir: + i.Data, err = ReadEDir(r) + case EFil: + i.Data, err = ReadEFile(r, blockSize) + case ESym: + i.Data, err = ReadESym(r) + case EBlock: + fallthrough + case EChar: + i.Data, err = ReadEDevice(r) + case EFifo: + fallthrough + case ESock: + i.Data, err = ReadEIPC(r) + default: + return i, errors.New("invalid inode type") + } + return +} diff --git a/internal/inode/inodetypes.go b/internal/inode/inodetypes.go deleted file mode 100644 index ddcba7b..0000000 --- a/internal/inode/inodetypes.go +++ /dev/null @@ -1,257 +0,0 @@ -package inode - -import ( - "encoding/binary" - "io" -) - -//The different types of inodes as defined by inodetype -const ( - DirType = iota + 1 - FileType - SymType - BlockDevType - CharDevType - FifoType - SocketType - ExtDirType - ExtFileType - ExtSymType - ExtBlockDeviceType - ExtCharDeviceType - ExtFifoType - ExtSocketType -) - -//Header is the common header for all inodes -type Header struct { - Type uint16 - Permissions uint16 - UID uint16 - GID uint16 - ModifiedTime uint32 - Number uint32 -} - -//Dir is self explainatory -type Dir struct { - DirectoryIndex uint32 - HardLinks uint32 - DirectorySize uint16 - DirectoryOffset uint16 - ParentInodeNumber uint32 -} - -//ExtDirInit is the information that can be directoy decoded -type ExtDirInit struct { - HardLinks uint32 - DirectorySize uint32 - DirectoryIndex uint32 - ParentInodeNumber uint32 - IndexCount uint16 //one less then directory indexes following structure - DirectoryOffset uint16 - XattrIndex uint32 -} - -//ExtDir is a directory with extra info -type ExtDir struct { - Indexes []DirIndex - ExtDirInit -} - -//NewExtendedDirectory creates a new ExtendedDirectory -func NewExtendedDirectory(rdr io.Reader) (ExtDir, error) { - var inode ExtDir - err := binary.Read(rdr, binary.LittleEndian, &inode.ExtDirInit) - if err != nil { - return inode, err - } - for i := uint16(0); i < inode.IndexCount; i++ { - var tmp DirIndex - tmp, err = NewDirectoryIndex(rdr) - if err != nil { - return inode, err - } - inode.Indexes = append(inode.Indexes, tmp) - } - return inode, err -} - -//DirIndexInit holds the values that can be easily decoded -type DirIndexInit struct { - Offset uint32 - DirTableOffset uint32 - NameSize uint32 -} - -//DirIndex is a quick lookup provided by an ExtendedDirectory -type DirIndex struct { - Name string - DirIndexInit -} - -//NewDirectoryIndex return a new DirectoryIndex -func NewDirectoryIndex(rdr io.Reader) (DirIndex, error) { - var index DirIndex - err := binary.Read(rdr, binary.LittleEndian, &index.DirIndexInit) - if err != nil { - return index, err - } - tmp := make([]byte, index.NameSize+1, index.NameSize+1) - err = binary.Read(rdr, binary.LittleEndian, &tmp) - if err != nil { - return index, err - } - index.Name = string(tmp) - return index, nil -} - -//FileInit is the information that can be directly decoded -type FileInit struct { - BlockStart uint32 - FragmentIndex uint32 - FragmentOffset uint32 - Size uint32 -} - -//File is self explainatory -type File struct { - BlockSizes []uint32 - Fragmented bool - FileInit -} - -//NewFile creates a new File -func NewFile(rdr io.Reader, blockSize uint32) (File, error) { - var inode File - err := binary.Read(rdr, binary.LittleEndian, &inode.FileInit) - if err != nil { - return inode, err - } - inode.Fragmented = inode.FragmentIndex != 0xFFFFFFFF - blocks := inode.Size / blockSize - if inode.Size%blockSize > 0 { - blocks++ - } - inode.BlockSizes = make([]uint32, blocks, blocks) - err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes) - return inode, err -} - -//ExtFileInit is the information that can be directly decoded -type ExtFileInit struct { - BlockStart uint64 - Size uint64 - Sparse uint64 - HardLinks uint32 - FragmentIndex uint32 - FragmentOffset uint32 - XattrIndex uint32 -} - -//ExtFile is a file with more information -type ExtFile struct { - BlockSizes []uint32 - Fragmented bool - ExtFileInit -} - -//NewExtendedFile creates a new ExtendedFile -func NewExtendedFile(rdr io.Reader, blockSize uint32) (ExtFile, error) { - var inode ExtFile - err := binary.Read(rdr, binary.LittleEndian, &inode.ExtFileInit) - if err != nil { - return inode, err - } - inode.Fragmented = inode.FragmentIndex != 0xFFFFFFFF - blocks := inode.Size / uint64(blockSize) - if inode.Size%uint64(blockSize) > 0 { - blocks++ - } - inode.BlockSizes = make([]uint32, blocks, blocks) - err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes) - return inode, err -} - -//SymInit is all the values that can be directly decoded -type SymInit struct { - HardLinks uint32 - TargetPathSize uint32 -} - -//Sym is a symlink -type Sym struct { - Path string - targetPath []byte //len is TargetPathSize - SymInit -} - -//NewSymlink creates a new Symlink -func NewSymlink(rdr io.Reader) (Sym, error) { - var inode Sym - err := binary.Read(rdr, binary.LittleEndian, &inode.SymInit) - if err != nil { - return inode, err - } - inode.targetPath = make([]byte, inode.TargetPathSize, inode.TargetPathSize) - err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath) - if err != nil { - return inode, err - } - inode.Path = string(inode.targetPath) - return inode, err -} - -//ExtSymInit is all the values that can be directly decoded -type ExtSymInit struct { - HardLinks uint32 - TargetPathSize uint32 -} - -//ExtSym is a symlink with extra information -type ExtSym struct { - Path string - targetPath []uint8 - ExtSymInit - XattrIndex uint32 -} - -//NewExtendedSymlink creates a new ExtendedSymlink -func NewExtendedSymlink(rdr io.Reader) (ExtSym, error) { - var inode ExtSym - err := binary.Read(rdr, binary.LittleEndian, &inode.ExtSymInit) - if err != nil { - return inode, err - } - inode.targetPath = make([]uint8, inode.TargetPathSize, inode.TargetPathSize) - err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath) - if err != nil { - return inode, err - } - inode.Path = string(inode.targetPath) - err = binary.Read(rdr, binary.LittleEndian, &inode.XattrIndex) - return inode, err -} - -//Device is a device -type Device struct { - HardLinks uint32 - Device uint32 -} - -//ExtDevice is a device with more info -type ExtDevice struct { - Device - XattrIndex uint32 -} - -//IPC is a Fifo or Socket device -type IPC struct { - HardLink uint32 -} - -//ExtIPC is a IPC device with extra info -type ExtIPC struct { - IPC - XattrIndex uint32 -} diff --git a/internal/inode/misc.go b/internal/inode/misc.go new file mode 100644 index 0000000..8ba0b61 --- /dev/null +++ b/internal/inode/misc.go @@ -0,0 +1,45 @@ +package inode + +import ( + "encoding/binary" + "io" +) + +type Device struct { + LinkCount uint32 + Dev uint32 +} + +type EDevice struct { + Device + XattrInd uint32 +} + +func ReadDevice(r io.Reader) (d Device, err error) { + err = binary.Read(r, binary.LittleEndian, &d) + return +} + +func ReadEDevice(r io.Reader) (d EDevice, err error) { + err = binary.Read(r, binary.LittleEndian, &d) + return +} + +type IPC struct { + LinkCount uint32 +} + +type EIPC struct { + IPC + XattrInd uint32 +} + +func ReadIPC(r io.Reader) (i IPC, err error) { + err = binary.Read(r, binary.LittleEndian, &i) + return +} + +func ReadEIPC(r io.Reader) (i EIPC, err error) { + err = binary.Read(r, binary.LittleEndian, &i) + return +} diff --git a/internal/inode/process.go b/internal/inode/process.go deleted file mode 100644 index 2b2098c..0000000 --- a/internal/inode/process.go +++ /dev/null @@ -1,128 +0,0 @@ -package inode - -import ( - "encoding/binary" - "errors" - "io" - "strconv" -) - -//Inode holds an inode. Header is the header that's common for all inodes. -// -//Info holds the actual Inode. Due to each inode type being a different type, it's store as an interface{} -type Inode struct { - Header - Info interface{} //Info is the parsed specific data. It's type is defined by Type. -} - -//ProcessInode tries to read an inode from the BlockReader -func ProcessInode(br io.Reader, blockSize uint32) (*Inode, error) { - var in Inode - err := binary.Read(br, binary.LittleEndian, &in.Header) - if err != nil { - return nil, err - } - switch in.Type { - case DirType: - var inode Dir - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case FileType: - var inode File - inode, err = NewFile(br, blockSize) - if err != nil { - return nil, err - } - in.Info = inode - case SymType: - var inode Sym - inode, err = NewSymlink(br) - if err != nil { - return nil, err - } - in.Info = inode - case BlockDevType: - var inode Device - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case CharDevType: - var inode Device - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case FifoType: - var inode IPC - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case SocketType: - var inode IPC - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case ExtDirType: - var inode ExtDir - inode, err = NewExtendedDirectory(br) - if err != nil { - return nil, err - } - in.Info = inode - case ExtFileType: - var inode ExtFile - inode, err = NewExtendedFile(br, blockSize) - if err != nil { - return nil, err - } - in.Info = inode - case ExtSymType: - var inode ExtSym - inode, err = NewExtendedSymlink(br) - if err != nil { - return nil, err - } - in.Info = inode - case ExtBlockDeviceType: - var inode ExtDevice - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case ExtCharDeviceType: - var inode ExtDevice - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case ExtFifoType: - var inode ExtIPC - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - case ExtSocketType: - var inode ExtIPC - err = binary.Read(br, binary.LittleEndian, &inode) - if err != nil { - return nil, err - } - in.Info = inode - default: - return nil, errors.New("Unsupported inode type: " + strconv.Itoa(int(in.Type))) - } - return &in, nil -} diff --git a/internal/inode/sym.go b/internal/inode/sym.go new file mode 100644 index 0000000..43659bb --- /dev/null +++ b/internal/inode/sym.go @@ -0,0 +1,46 @@ +package inode + +import ( + "encoding/binary" + "io" +) + +type symlinkInit struct { + LinkCount uint32 + TargetSize uint32 +} + +type Symlink struct { + symlinkInit + Target []byte +} + +type ESymlink struct { + symlinkInit + Target []byte + XattrInd uint32 +} + +func ReadSym(r io.Reader) (s Symlink, err error) { + err = binary.Read(r, binary.LittleEndian, &s.symlinkInit) + if err != nil { + return + } + s.Target = make([]byte, s.TargetSize) + err = binary.Read(r, binary.LittleEndian, &s.Target) + return +} + +func ReadESym(r io.Reader) (s ESymlink, err error) { + err = binary.Read(r, binary.LittleEndian, &s.symlinkInit) + if err != nil { + return + } + s.Target = make([]byte, s.TargetSize) + err = binary.Read(r, binary.LittleEndian, &s.Target) + if err != nil { + return + } + err = binary.Read(r, binary.LittleEndian, &s.XattrInd) + return +} diff --git a/internal/metadata/reader.go b/internal/metadata/reader.go new file mode 100644 index 0000000..9df7a74 --- /dev/null +++ b/internal/metadata/reader.go @@ -0,0 +1,60 @@ +package metadata + +import ( + "encoding/binary" + "io" + + "github.com/CalebQ42/squashfs/internal/decompress" +) + +type Reader struct { + master io.Reader + cur io.Reader + d decompress.Decompressor +} + +func NewReader(r io.Reader, d decompress.Decompressor) (*Reader, error) { + var out Reader + out.d = d + out.master = r + return &out, out.Advance() +} + +func (r *Reader) Advance() error { + if clr, ok := r.cur.(io.Closer); ok { + clr.Close() + } + var size uint16 + err := binary.Read(r.master, binary.LittleEndian, &size) + if err != nil { + return err + } + comp := size&0x8000 != 0x8000 + size &^= 0x8000 + r.cur = io.LimitReader(r.master, int64(size)) + if comp { + r.cur, err = r.d.Reader(r.cur) + if err != nil { + return err + } + } + return nil +} + +func (r Reader) Read(p []byte) (n int, err error) { + n, err = r.cur.Read(p) + if err == io.EOF { + err = r.Advance() + if err != nil { + return + } + var tmpN int + tmp := make([]byte, len(p)-n) + tmpN, err = r.Read(tmp) + for i := range tmp { + p[n+i] = tmp[i] + } + n += tmpN + } + return +} diff --git a/internal/rawreader/rawreader.go b/internal/rawreader/rawreader.go deleted file mode 100644 index b5cbb49..0000000 --- a/internal/rawreader/rawreader.go +++ /dev/null @@ -1,135 +0,0 @@ -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 -} diff --git a/internal/toreader/reader.go b/internal/toreader/reader.go new file mode 100644 index 0000000..6f711fd --- /dev/null +++ b/internal/toreader/reader.go @@ -0,0 +1,25 @@ +package toreader + +import "io" + +type Reader struct { + r io.ReaderAt + off int64 +} + +func NewReader(r io.ReaderAt, start int64) *Reader { + return &Reader{ + r: r, + off: start, + } +} + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.r.ReadAt(p, r.off) + r.off += int64(n) + return +} + +func (r Reader) Offset() int64 { + return r.off +} diff --git a/internal/toreader/readerat.go b/internal/toreader/readerat.go new file mode 100644 index 0000000..be26ac9 --- /dev/null +++ b/internal/toreader/readerat.go @@ -0,0 +1,23 @@ +package toreader + +import "io" + +type ReaderAt struct { + d []byte +} + +func NewReaderAt(r io.Reader) (ra ReaderAt, err error) { + ra.d, err = io.ReadAll(r) + return +} + +func (r ReaderAt) ReadAt(p []byte, off int64) (n int, err error) { + if int(off) >= len(r.d) { + return 0, io.EOF + } + n = copy(p, r.d[off:]) + if n != len(p) { + err = io.EOF + } + return +} diff --git a/metadata.go b/metadata.go deleted file mode 100644 index 02df9da..0000000 --- a/metadata.go +++ /dev/null @@ -1,188 +0,0 @@ -package squashfs - -import ( - "bytes" - "encoding/binary" - "errors" - "io" -) - -type metadata struct { - raw uint16 - size uint16 - compressed bool -} - -//MetadataReader is a block reader for metadata. It will automatically read the next block, when it reaches the end of a block. -type metadataReader struct { - s *Reader - headers []*metadata - data []byte - offset int64 - readOffset int -} - -//NewMetadataReader creates a new MetadataReader, beginning to read at the given offset. It will automatically cache the first block at the location. -func (s *Reader) newMetadataReader(offset int64) (*metadataReader, error) { - var br metadataReader - br.s = s - br.offset = offset - err := br.parseMetadata() - if err != nil { - return nil, err - } - err = br.readNextDataBlock() - if err != nil { - return nil, err - } - return &br, nil -} - -//NewMetadataReaderFromInodeRef creates a new MetadataReader with the offsets set by the given inode reference. -func (s *Reader) newMetadataReaderFromInodeRef(ref uint64) (*metadataReader, error) { - offset, metaOffset := processInodeRef(ref) - br, err := s.newMetadataReader(int64(s.super.InodeTableStart + offset)) - if err != nil { - return nil, err - } - _, err = br.Seek(int64(metaOffset), io.SeekStart) - if err != nil { - return nil, err - } - return br, nil -} - -//ProcessInodeRef processes an inode reference and returns two values -// -//The first value is the inode table offset. AKA, it's where the metadata block of the inode STARTS relative to the inode table. -// -//The second value is the offset of the inode, INSIDE of the metadata. -func processInodeRef(inodeRef uint64) (tableOffset uint64, metaOffset uint64) { - tableOffset = inodeRef >> 16 - metaOffset = inodeRef &^ 0xFFFFFFFF0000 - return -} - -func (br *metadataReader) parseMetadata() error { - var raw uint16 - err := binary.Read(io.NewSectionReader(br.s.r, br.offset, 2), binary.LittleEndian, &raw) - if err != nil { - return err - } - br.offset += 2 - compressed := raw&0x8000 != 0x8000 - size := raw &^ 0x8000 - br.headers = append(br.headers, &metadata{ - raw: raw, - size: size, - compressed: compressed, - }) - return nil -} - -func (br *metadataReader) readNextDataBlock() error { - meta := br.headers[len(br.headers)-1] - r := io.NewSectionReader(br.s.r, br.offset, int64(meta.size)) - if meta.compressed { - byts, err := br.s.decompressor.Decompress(r) - if err != nil { - return err - } - br.offset += int64(meta.size) - br.data = append(br.data, byts...) - return nil - } - var buf bytes.Buffer - _, err := io.Copy(&buf, r) - if err != nil { - return err - } - br.offset += int64(meta.size) - br.data = append(br.data, buf.Bytes()...) - return nil -} - -//Read reads bytes into the given byte slice. Returns the amount of data read. -func (br *metadataReader) Read(p []byte) (int, error) { - if br.readOffset+len(p) <= len(br.data) { - for i := 0; i < len(p); i++ { - p[i] = br.data[br.readOffset+i] - } - br.readOffset += len(p) - return len(p), nil - } - read := 0 - for read < len(p) { - err := br.parseMetadata() - if err != nil { - br.readOffset += read - return read, err - } - err = br.readNextDataBlock() - if err != nil { - br.readOffset += read - return read, err - } - for ; read < len(p); read++ { - if br.readOffset+read < len(br.data) { - p[read] = br.data[br.readOffset+read] - } else { - break - } - } - } - br.readOffset += read - if read != len(p) { - return read, errors.New("didn't read enough data") - } - return read, nil -} - -//Seek will seek to the specified location (if possible). Seeking is relative to the uncompressed data. -//When io.SeekCurrent or io.SeekStart is set, if seeking would put the offset beyond the current cached data, it will try to read the next data blocks to accomodate. On a failure it will seek to the end of the data. -//When io.SeekEnd is set, it wil seek reletive to the currently cached data. -func (br *metadataReader) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekCurrent: - br.readOffset += int(offset) - for { - if br.readOffset < len(br.data) { - break - } - err := br.parseMetadata() - if err != nil { - br.readOffset = len(br.data) - return int64(br.readOffset), err - } - err = br.readNextDataBlock() - if err != nil { - br.readOffset = len(br.data) - return int64(br.readOffset), err - } - } - case io.SeekStart: - br.readOffset = int(offset) - for { - if br.readOffset < len(br.data) { - break - } - err := br.parseMetadata() - if err != nil { - br.readOffset = len(br.data) - return int64(br.readOffset), err - } - err = br.readNextDataBlock() - if err != nil { - br.readOffset = len(br.data) - return int64(br.readOffset), err - } - } - case io.SeekEnd: - br.readOffset = len(br.data) - int(offset) - if br.readOffset < 0 { - br.readOffset = 0 - return int64(br.readOffset), errors.New("trying to seek to a negative value") - } - } - return int64(br.readOffset), nil -} diff --git a/reader.go b/reader.go index 2b529a7..4a90f1f 100644 --- a/reader.go +++ b/reader.go @@ -7,197 +7,226 @@ import ( "math" "time" - "github.com/CalebQ42/squashfs/internal/compression" + "github.com/CalebQ42/squashfs/internal/decompress" + "github.com/CalebQ42/squashfs/internal/directory" "github.com/CalebQ42/squashfs/internal/inode" - "github.com/CalebQ42/squashfs/internal/rawreader" + "github.com/CalebQ42/squashfs/internal/metadata" + "github.com/CalebQ42/squashfs/internal/toreader" +) + +type Reader struct { + *FS + d decompress.Decompressor + r io.ReaderAt + fragEntries []fragEntry + ids []uint32 + exportTable []uint64 + s superblock +} + +var ( + ErrorMagic = errors.New("magic incorrect. probably not reading squashfs archive") + ErrorLog = errors.New("block log is incorrect. possible corrupted archive") ) const ( - magic uint32 = 0x73717368 + GZipCompression = uint16(iota + 1) + LZMACompression + LZOCompression + XZCompression + LZ4Compression + ZSTDCompression ) -var ( - //ErrNoMagic is returned if the magic number in the superblock isn't correct. - errNoMagic = errors.New("magic number doesn't match. Either isn't a squashfs or corrupted") - //ErrIncompatibleCompression is returned if the compression type in the superblock doesn't work. - errIncompatibleCompression = errors.New("compression type unsupported") -) - -//Reader processes and reads a squashfs archive. -type Reader struct { - FS - r rawreader.RawReader - 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) { - var rdr Reader - rdr.r = rawreader.ConvertReaderAt(r) - err := rdr.Init() +func NewReaderFromReader(r io.Reader) (*Reader, error) { + rdr, err := toreader.NewReaderAt(r) if err != nil { return nil, err } - return &rdr, nil + return NewReader(rdr) } -//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) +func NewReader(r io.ReaderAt) (*Reader, error) { + var squash Reader + squash.r = r + err := binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &squash.s) if err != nil { return nil, err } - err = rdr.Init() - if err != nil { - return nil, err + if !squash.s.hasMagic() { + return nil, ErrorMagic } - return &rdr, nil -} - -func (r *Reader) Init() error { - err := binary.Read(r.r, binary.LittleEndian, &r.super) - if err != nil { - return err + if !squash.s.checkBlockLog() { + return nil, ErrorLog } - if r.super.Magic != magic { - return errNoMagic + switch squash.s.CompType { + case GZipCompression: + squash.d = decompress.GZip{} + case LZMACompression: + squash.d = decompress.Lzma{} + case LZOCompression: + squash.d = decompress.Lzo{} + case XZCompression: + squash.d = decompress.Xz{} + case LZ4Compression: + squash.d = decompress.Lz4{} + case ZSTDCompression: + squash.d = decompress.Zstd{} + default: + return nil, errors.New("uh, I need to do this, OR something if very wrong") } - if 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: - var gzip *compression.Gzip - gzip, err = compression.NewGzipCompressorWithOptions(r.r) - if err != nil { - return err - } - r.decompressor = gzip - case XzCompression: - var xz *compression.Xz - xz, err = compression.NewXzCompressorWithOptions(r.r) - if err != nil { - return err - } - r.decompressor = xz - case LzoCompression: - var lz *compression.Lzo - lz, err = compression.NewLzoCompressorWithOptions(r.r) - if err != nil { - return err - } - r.decompressor = lz - case Lz4Compression: - var lz4 *compression.Lz4 - lz4, err = compression.NewLz4CompressorWithOptions(r.r) - if err != nil { - return err - } - r.decompressor = lz4 - case ZstdCompression: - var zstd *compression.Zstd - zstd, err = compression.NewZstdCompressorWithOptions(r.r) - if err != nil { - return err - } - r.decompressor = zstd - default: - return errIncompatibleCompression - } - } else { - switch r.super.CompressionType { - case GzipCompression: - r.decompressor = &compression.Gzip{} - case LzmaCompression: - r.decompressor = &compression.Lzma{} - case LzoCompression: - r.decompressor = &compression.Lzo{} - case XzCompression: - r.decompressor = &compression.Xz{} - case Lz4Compression: - r.decompressor = &compression.Lz4{} - case ZstdCompression: - r.decompressor = &compression.Zstd{} - default: - //TODO: all compression types. - return errIncompatibleCompression - } - } - fragBlocks := int(math.Ceil(float64(r.super.FragCount) / 512)) - if fragBlocks > 0 { - offset := int64(r.super.FragTableStart) - for i := 0; i < fragBlocks; i++ { - tmp := make([]byte, 8) - _, err = r.r.ReadAt(tmp, offset) - if err != nil { - return err - } - r.fragOffsets = append(r.fragOffsets, binary.LittleEndian.Uint64(tmp)) - offset += 8 - } - } - unread := r.super.IDCount - blockOffsets := make([]uint64, int(math.Ceil(float64(r.super.IDCount)/2048))) - _, err = r.r.Seek(int64(r.super.IDTableStart), io.SeekStart) - if err != nil { - return err - } - for i := range blockOffsets { - err = binary.Read(r.r, binary.LittleEndian, &blockOffsets[i]) + if !squash.s.noFragments() && squash.s.FragCount > 0 { + fragOffsets := make([]uint64, int(math.Ceil(float64(squash.s.FragCount)/512))) + err = binary.Read(toreader.NewReader(r, int64(squash.s.FragTableStart)), binary.LittleEndian, &fragOffsets) if err != nil { - return err + return nil, err } - var idRdr *metadataReader - idRdr, err = r.newMetadataReader(int64(blockOffsets[i])) - if err != nil { - return err - } - read := uint16(math.Min(float64(unread), 2048)) - for i := uint16(0); i < read; i++ { - var tmp uint32 - err = binary.Read(idRdr, binary.LittleEndian, &tmp) + squash.fragEntries = make([]fragEntry, squash.s.FragCount) + if len(fragOffsets) == 1 { + var rdr *metadata.Reader + rdr, err = metadata.NewReader(toreader.NewReader(r, int64(fragOffsets[0])), squash.d) if err != nil { - return err + return nil, err + } + err = binary.Read(rdr, binary.LittleEndian, &squash.fragEntries) + if err != nil { + return nil, err + } + } else { + toRead := squash.s.IdCount + var curRead uint16 + var tmp []fragEntry + var rdr *metadata.Reader + var offset int + for i := range fragOffsets { + curRead = uint16(math.Min(512, float64(toRead))) + tmp = make([]fragEntry, curRead) + rdr, err = metadata.NewReader(toreader.NewReader(r, int64(fragOffsets[i])), squash.d) + if err != nil { + return nil, err + } + err = binary.Read(rdr, binary.LittleEndian, &tmp) + if err != nil { + return nil, err + } + offset = int(squash.s.IdCount - toRead) + for i := range tmp { + squash.fragEntries[offset+i] = tmp[i] + } + toRead -= curRead } - r.idTable = append(r.idTable, tmp) } - unread -= read } - metaRdr, err := r.newMetadataReaderFromInodeRef(r.super.RootInodeRef) + if squash.s.IdCount > 0 { + idOffsets := make([]uint64, int(math.Ceil(float64(squash.s.IdCount)/2048))) + err = binary.Read(toreader.NewReader(r, int64(squash.s.IdTableStart)), binary.LittleEndian, &idOffsets) + if err != nil { + return nil, err + } + squash.ids = make([]uint32, squash.s.IdCount) + if len(idOffsets) == 1 { + var rdr *metadata.Reader + rdr, err = metadata.NewReader(toreader.NewReader(r, int64(idOffsets[0])), squash.d) + if err != nil { + return nil, err + } + err = binary.Read(rdr, binary.LittleEndian, &squash.ids) + if err != nil { + return nil, err + } + } else { + toRead := squash.s.IdCount + var curRead uint16 + var tmp []uint32 + var rdr *metadata.Reader + var offset int + for i := range idOffsets { + curRead = uint16(math.Min(2048, float64(toRead))) + tmp = make([]uint32, curRead) + rdr, err = metadata.NewReader(toreader.NewReader(r, int64(idOffsets[i])), squash.d) + if err != nil { + return nil, err + } + err = binary.Read(rdr, binary.LittleEndian, &tmp) + if err != nil { + return nil, err + } + offset = int(squash.s.IdCount - toRead) + for i := range tmp { + squash.ids[offset+i] = tmp[i] + } + toRead -= curRead + } + } + } + root, err := squash.inodeFromRef(squash.s.RootInodeRef) if err != nil { - return err + return nil, err } - i, err := inode.ProcessInode(metaRdr, r.super.BlockSize) + rootEnts, err := squash.readDirectory(root) if err != nil { - return err + return nil, err } - entries, err := r.readDirFromInode(i) + enType := root.Type + if enType == inode.EDir { + enType = inode.Dir + } + squash.FS = &FS{ + e: rootEnts, + File: &File{ + rdr: &squash, + i: root, + e: directory.Entry{ + Name: "root", + Type: enType, + }, + }, + } + return &squash, nil +} + +func (r *Reader) initExport() (err error) { + num := int(math.Ceil(float64(r.s.InodeCount) / 1024)) + offsets := make([]uint64, num) + err = binary.Read(toreader.NewReader(r.r, int64(r.s.ExportTableStart)), binary.LittleEndian, &offsets) if err != nil { - return err + return } - r.FS = FS{ - i: i, - r: r, - name: "/", - entries: entries, + left := r.s.InodeCount + var toRead uint32 + var new []uint64 + var rdr *metadata.Reader + for i := range offsets { + rdr, err = metadata.NewReader(toreader.NewReader(r.r, int64(offsets[i])), r.d) + if err != nil { + return + } + toRead = uint32(math.Min(1024, float64(left))) + new = make([]uint64, toRead) + err = binary.Read(rdr, binary.LittleEndian, &new) + if err != nil { + return + } + left -= toRead + r.exportTable = append(r.exportTable, new...) } return nil } -//ModTime is the last time the file was modified/created. -func (r *Reader) ModTime() time.Time { - return time.Unix(int64(r.super.CreationTime), 0) +func (r *Reader) inode(index uint32) (i inode.Inode, err error) { + if r.s.exportable() { + if r.exportTable == nil { + err = r.initExport() + if err != nil { + return + } + } + return r.inodeFromRef(r.exportTable[index-1]) + } + err = errors.New("archive is not exportable") + return +} + +func (r Reader) ModTime() time.Time { + return time.Unix(int64(r.s.ModTime), 0) } diff --git a/reader_datareader.go b/reader_datareader.go deleted file mode 100644 index dced500..0000000 --- a/reader_datareader.go +++ /dev/null @@ -1,242 +0,0 @@ -package squashfs - -import ( - "bytes" - "errors" - "io" - - "github.com/CalebQ42/squashfs/internal/inode" -) - -var ( - //ErrInodeNotFile is given when giving an inode, but the function requires a file inode. - errInodeNotFile = errors.New("given inode is NOT a file type") - //ErrInodeOnlyFragment is given when trying to make a DataReader from an inode, but the inode only had data in a fragment - errInodeOnlyFragment = errors.New("given inode ONLY has fragment data") -) - -//DataReader reads data from data blocks. -type dataReader struct { - r *Reader - curData []byte - sizes []uint32 - offset int64 //offset relative to the beginning of the squash file - curBlock int //Which block in sizes is currently cached - curReadOffset int //offset relative to the currently cached data -} - -//NewDataReader creates a new data reader at the given offset, with the blocks defined by sizes -func (r *Reader) newDataReader(offset int64, sizes []uint32) (*dataReader, error) { - var dr dataReader - dr.r = r - dr.offset = offset - dr.sizes = sizes - err := dr.readCurBlock() - if err != nil { - return nil, err - } - return &dr, nil -} - -//NewDataReaderFromInode creates a new DataReader from a given inode. Inode must be of BasicFile or ExtendedFile types -func (r *Reader) newDataReaderFromInode(i *inode.Inode) (*dataReader, error) { - var rdr dataReader - rdr.r = r - switch i.Type { - case inode.FileType: - fil := i.Info.(inode.File) - if fil.BlockStart == 0 { - return nil, errInodeOnlyFragment - } - rdr.offset = int64(fil.BlockStart) - rdr.sizes = append(rdr.sizes, fil.BlockSizes...) - if fil.Fragmented { - rdr.sizes = rdr.sizes[:len(rdr.sizes)-1] - } - case inode.ExtFileType: - fil := i.Info.(inode.ExtFile) - if fil.BlockStart == 0 { - return nil, errInodeOnlyFragment - } - rdr.offset = int64(fil.BlockStart) - rdr.sizes = append(rdr.sizes, fil.BlockSizes...) - if fil.Fragmented { - rdr.sizes = rdr.sizes[:len(rdr.sizes)-1] - } - default: - return nil, errInodeNotFile - } - err := rdr.readCurBlock() - if err != nil { - return nil, err - } - return &rdr, nil -} - -//removed the compression bit from a data block size -func actualDataSize(size uint32) uint32 { - return size &^ (1 << 24) -} - -func (d *dataReader) readNextBlock() error { - d.curBlock++ - if d.curBlock >= len(d.sizes) { - d.curBlock-- - return io.EOF - } - err := d.readCurBlock() - if err != nil { - d.curBlock-- - d.readCurBlock() - return err - } - return nil -} - -func (d *dataReader) readBlockAt(offset int64, size uint32) ([]byte, error) { - compressed := size&(1<<24) != (1 << 24) - size = size &^ (1 << 24) - if d.sizes[d.curBlock] == 0 { - return make([]byte, d.r.super.BlockSize), nil - } - sec := io.NewSectionReader(d.r.r, offset, int64(size)) - if compressed { - btys, err := d.r.decompressor.Decompress(sec) - if err != nil { - return nil, err - } - return btys, nil - } - var buf bytes.Buffer - _, err := io.Copy(&buf, sec) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func (d *dataReader) offsetForBlock(index int) int64 { - out := d.offset - for i := 0; i < index; i++ { - out += int64(actualDataSize(d.sizes[i])) - } - return out -} - -func (d *dataReader) readCurBlock() error { - if d.curBlock >= len(d.sizes) { - return io.EOF - } - offset := d.offsetForBlock(d.curBlock) - data, err := d.readBlockAt(offset, d.sizes[d.curBlock]) - if err != nil { - return err - } - d.curData = data - return nil -} - -func (d *dataReader) Read(p []byte) (int, error) { - if d.curData == nil { - err := d.readCurBlock() - if err != nil { - return 0, err - } - } - if d.curReadOffset+len(p) <= len(d.curData) { - for i := 0; i < len(p); i++ { - p[i] = d.curData[d.curReadOffset+i] - } - d.curReadOffset += len(p) - return len(p), nil - } - read := 0 - for read < len(p) { - if d.curReadOffset == len(d.curData) { - err := d.readNextBlock() - if err != nil { - return read, err - } - d.curReadOffset = 0 - } - for ; read < len(p); read++ { - if d.curReadOffset < len(d.curData) { - p[read] = d.curData[d.curReadOffset] - } else { - break - } - d.curReadOffset++ - } - } - if read != len(p) { - return read, errors.New("didn't read enough data") - } - return read, nil -} - -// WriteTo writes all the data in the datablock to the writer. MUST BE USED ON A FRESH DATA READER. -func (d *dataReader) WriteTo(w io.Writer) (int64, error) { - type dataCache struct { - err error - data []byte - index int - } - dataChan := make(chan *dataCache) - for i := range d.sizes { - go func(index int, c chan *dataCache) { - var cache dataCache - cache.index = index - defer func() { - c <- &cache - }() - data, err := d.readBlockAt(d.offsetForBlock(index), d.sizes[index]) - if err != nil { - cache.err = err - return - } - cache.data = data - }(i, dataChan) - } - curIndex := 0 - totalWrite := int64(0) - var backlog []*dataCache -mainLoop: - for { - if curIndex == len(d.sizes) { - return totalWrite, nil - } - if len(backlog) > 0 { - for i, cache := range backlog { - if cache.index == curIndex { - writen, err := w.Write(cache.data) - totalWrite += int64(writen) - if err != nil { - return totalWrite, err - } - if len(backlog) > 0 { - backlog[i] = backlog[len(backlog)-1] - backlog = backlog[:len(backlog)-1] - } else { - backlog = nil - } - curIndex++ - continue mainLoop - } - } - } - cache := <-dataChan - if cache.err != nil { - return totalWrite, cache.err - } - if cache.index == curIndex { - writen, err := w.Write(cache.data) - totalWrite += int64(writen) - if err != nil { - return totalWrite, err - } - curIndex++ - } else { - backlog = append(backlog, cache) - } - } -} diff --git a/reader_file.go b/reader_file.go index d0fbb20..4cb2ced 100644 --- a/reader_file.go +++ b/reader_file.go @@ -6,7 +6,7 @@ import ( "io/fs" "log" "os" - "path" + "path/filepath" "strconv" "strings" @@ -16,166 +16,152 @@ import ( //File represents a file inside a squashfs archive. type File struct { - i *inode.Inode - parent *FS + i inode.Inode + rdr io.Reader + fullRdr io.WriterTo r *Reader - reader *fileReader - name string + parent *FS + e directory.Entry dirsRead int } -//File creates a File from the FileInfo. -//*File satisfies fs.File and fs.ReadDirFile. -func (f FileInfo) File() (file *File, err error) { - file = &File{ - name: f.name, - r: f.r, - parent: f.parent, - i: f.i, - } - if file.IsRegular() { - file.reader, err = f.r.newFileReader(f.i) - } - return -} +var ( + ErrReadNotFile = errors.New("read called on non-file") +) -//File creates a File from the DirEntry. -func (d DirEntry) File() (file *File, err error) { - return d.r.newFileFromDirEntry(d.en, d.parent) -} - -func (r Reader) newFileFromDirEntry(en *directory.Entry, parent *FS) (file *File, err error) { - file = &File{ - name: en.Name, - r: &r, - parent: parent, - } - file.i, err = r.getInodeFromEntry(en) +func (r Reader) newFile(en directory.Entry) (*File, error) { + i, err := r.inodeFromDir(en) if err != nil { return nil, err } - if file.IsRegular() { - file.reader, err = r.newFileReader(file.i) + var rdr io.Reader + var full io.WriterTo + if en.Type == inode.Fil { + rdr, err = r.getData(i) + if err != nil { + return nil, err + } + full, err = r.getFullReader(i) + if err != nil { + return nil, err + } } - return + return &File{ + e: en, + i: i, + rdr: rdr, + fullRdr: full, + r: &r, + }, nil } //Stat returns the File's fs.FileInfo func (f File) Stat() (fs.FileInfo, error) { - return &FileInfo{ - i: f.i, - name: f.name, - parent: f.parent, - r: f.r, - }, nil + return newFileInfo(f.e, f.i), nil } //Read reads the data from the file. Only works if file is a normal file. func (f File) Read(p []byte) (int, error) { - if f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType { - if f.reader == nil { - return 0, fs.ErrClosed - } - return f.reader.Read(p) + if f.i.Type != inode.Fil && f.i.Type != inode.EFil { + return 0, ErrReadNotFile } - return 0, errors.New("can only read files") + if f.rdr == nil { + return 0, fs.ErrClosed + } + return f.rdr.Read(p) } //WriteTo writes all data from the file to the writer. This is multi-threaded. +//The underlying reader is seperate from the one used with Read and can be reused. func (f File) WriteTo(w io.Writer) (int64, error) { - if f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType { - if f.reader == nil { - return 0, fs.ErrClosed - } - return f.reader.WriteTo(w) - } - return 0, errors.New("can only read files") + return f.fullRdr.WriteTo(w) } //Close simply nils the underlying reader. Here mostly to satisfy fs.File func (f *File) Close() error { - f.reader = nil + f.rdr = nil return nil } //ReadDir returns n fs.DirEntry's that's contained in the File (if it's a directory). //If n <= 0 all fs.DirEntry's are returned. -func (f File) ReadDir(n int) ([]fs.DirEntry, error) { +func (f *File) ReadDir(n int) (out []fs.DirEntry, err error) { if !f.IsDir() { return nil, errors.New("File is not a directory") } - ffs, err := f.FS() + ents, err := f.r.readDirectory(f.i) if err != nil { return nil, err } - var beg, end int - if n <= 0 { - beg, end = 0, len(ffs.entries) - } else { - beg, end = f.dirsRead, f.dirsRead+n - if end > len(ffs.entries) { - end = len(ffs.entries) + start, end := 0, len(ents) + if n > 0 { + start, end = f.dirsRead, f.dirsRead+n + if end > len(f.r.e) { + end = len(f.r.e) err = io.EOF } } - out := make([]fs.DirEntry, end-beg) - for i, ent := range ffs.entries[beg:end] { - out[i] = f.r.newDirEntry(ent, ffs) + var fi FileInfo + for _, e := range ents[start:end] { + fi, err = f.r.newFileInfo(e) + if err != nil { + f.dirsRead += len(out) + return + } + out = append(out, fs.FileInfoToDirEntry(fi)) } - return out, err + f.dirsRead += len(out) + return } //FS returns the File as a FS. -func (f File) FS() (*FS, error) { +func (f *File) FS() (*FS, error) { if !f.IsDir() { return nil, errors.New("File is not a directory") } - ents, err := f.r.readDirFromInode(f.i) + ents, err := f.r.readDirectory(f.i) if err != nil { return nil, err } return &FS{ - i: f.i, - r: f.r, - parent: f.parent, - name: f.name, - entries: ents, + File: f, + e: ents, }, nil } //IsDir Yep. func (f File) IsDir() bool { - return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType -} - -func (f File) path() string { - if f.name == "/" { - return f.name - } - return f.parent.path() + "/" + f.name + return f.i.Type == inode.Dir || f.i.Type == inode.EDir } //IsRegular yep. func (f File) IsRegular() bool { - return f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType + return f.i.Type == inode.Fil || f.i.Type == inode.EFil } //IsSymlink yep. func (f File) IsSymlink() bool { - return f.i.Type == inode.SymType || f.i.Type == inode.ExtSymType + return f.i.Type == inode.Sym || f.i.Type == inode.ESym } //SymlinkPath returns the symlink's target path. Is the File isn't a symlink, returns an empty string. func (f File) SymlinkPath() string { switch f.i.Type { - case inode.SymType: - return f.i.Info.(inode.Sym).Path - case inode.ExtSymType: - return f.i.Info.(inode.ExtSym).Path + case inode.Sym: + return string(f.i.Data.(inode.Symlink).Target) + case inode.ESym: + return string(f.i.Data.(inode.ESymlink).Target) } return "" } +func (f File) path() string { + if f.parent == nil { + return f.e.Name + } + return f.parent.path() + "/" + f.e.Name +} + //GetSymlinkFile returns the File the symlink is pointing to. //If not a symlink, or the target is unobtainable (such as it being outside the archive or it's absolute) returns nil func (f File) GetSymlinkFile() *File { @@ -229,28 +215,28 @@ func (f File) ExtractSymlink(folder string) error { //ExtractWithOptions extracts the File to the given folder with the given ExtrationOptions. //If the File is a directory, it instead extracts the directory's contents to the folder. func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { - folder = path.Clean(folder) if !op.notBase { err := os.MkdirAll(folder, op.FolderPerm) if err != nil { return err } } + folder = filepath.Clean(folder) stat, err := f.Stat() if err != nil { return err } if f.IsDir() { if op.notBase { - err = os.Mkdir(folder+"/"+f.name, stat.Mode()) + err = os.Mkdir(folder+"/"+f.e.Name, stat.Mode()) if err != nil && !os.IsExist(err) { return err } } else { op.notBase = true } - var ents []fs.DirEntry - ents, err = f.ReadDir(0) + var ents []directory.Entry + ents, err = f.r.readDirectory(f.i) if err != nil { if op.Verbose { log.Println("Error while reading children of", f.path()) @@ -259,16 +245,16 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { } errChan := make(chan error) for i := 0; i < len(ents); i++ { - go func(ent *DirEntry) { - fil, goErr := ent.File() + go func(ent directory.Entry) { + fil, goErr := f.r.newFile(ent) if goErr != nil { errChan <- goErr fil.Close() return } - errChan <- fil.ExtractWithOptions(folder+"/"+f.name, op) + errChan <- fil.ExtractWithOptions(folder+"/"+f.e.Name, op) fil.Close() - }(ents[i].(*DirEntry)) + }(ents[i]) } for i := 0; i < len(ents); i++ { err = <-errChan @@ -279,12 +265,12 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { return nil } else if f.IsRegular() { var fil *os.File - fil, err = os.Create(folder + "/" + f.name) + fil, err = os.Create(folder + "/" + f.e.Name) if os.IsExist(err) { - os.Remove(folder + "/" + f.name) - fil, err = os.Create(folder + "/" + f.name) + os.Remove(folder + "/" + f.e.Name) + fil, err = os.Create(folder + "/" + f.e.Name) if err != nil { - log.Println("Error while creating", folder+"/"+f.name) + log.Println("Error while creating", folder+"/"+f.e.Name) return err } } else if err != nil { @@ -292,7 +278,7 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { } _, err = io.Copy(fil, f) if err != nil { - log.Println("Error while copying data to", folder+"/"+f.name) + log.Println("Error while copying data to", folder+"/"+f.e.Name) return err } return nil @@ -302,15 +288,15 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { fil := f.GetSymlinkFile() if fil == nil { if op.Verbose { - log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name) + log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name) } return errors.New("cannot get symlink target") } - fil.name = f.name + fil.e.Name = f.e.Name err = fil.ExtractWithOptions(folder, op) if err != nil { if op.Verbose { - log.Println("Error while extracting the symlink's file:", folder+"/"+f.name) + log.Println("Error while extracting the symlink's file:", folder+"/"+f.e.Name) } return err } @@ -319,27 +305,27 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { fil := f.GetSymlinkFile() if fil == nil { if op.Verbose { - log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name) + log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.e.Name) } return errors.New("cannot get symlink target") } - extractLoc := path.Clean(folder + "/" + path.Dir(symPath)) + extractLoc := filepath.Clean(folder + "/" + filepath.Dir(symPath)) err = fil.ExtractWithOptions(extractLoc, op) if err != nil { if op.Verbose { - log.Println("Error while extracting ", folder+"/"+f.name) + log.Println("Error while extracting ", folder+"/"+f.e.Name) } return err } } - err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name) + err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name) if os.IsExist(err) { - os.Remove(folder + "/" + f.name) - err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name) + os.Remove(folder + "/" + f.e.Name) + err = os.Symlink(f.SymlinkPath(), folder+"/"+f.e.Name) } if err != nil { if op.Verbose { - log.Println("Error while making symlink:", folder+"/"+f.name) + log.Println("Error while making symlink:", folder+"/"+f.e.Name) } return err } @@ -347,36 +333,3 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error { } return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.i.Type))) } - -//ReadDirFromInode returns a fully populated Directory from a given Inode. -//If the given inode is not a directory it returns an error. -func (r *Reader) readDirFromInode(i *inode.Inode) ([]*directory.Entry, error) { - var offset uint32 - var metaOffset uint16 - var size uint32 - switch i.Type { - case inode.DirType: - offset = i.Info.(inode.Dir).DirectoryIndex - metaOffset = i.Info.(inode.Dir).DirectoryOffset - size = uint32(i.Info.(inode.Dir).DirectorySize) - case inode.ExtDirType: - offset = i.Info.(inode.ExtDir).DirectoryIndex - metaOffset = i.Info.(inode.ExtDir).DirectoryOffset - size = i.Info.(inode.ExtDir).DirectorySize - default: - return nil, errors.New("not a directory inode") - } - br, err := r.newMetadataReader(int64(r.super.DirTableStart + uint64(offset))) - if err != nil { - return nil, err - } - _, err = br.Seek(int64(metaOffset), io.SeekStart) - if err != nil { - return nil, err - } - ents, err := directory.NewDirectory(br, size) - if err != nil { - return nil, err - } - return ents, nil -} diff --git a/reader_filereader.go b/reader_filereader.go deleted file mode 100644 index a1d3dd1..0000000 --- a/reader_filereader.go +++ /dev/null @@ -1,107 +0,0 @@ -package squashfs - -import ( - "bytes" - "errors" - "io" - - "github.com/CalebQ42/squashfs/internal/inode" -) - -//FileReader provides a io.Reader interface for files within a squashfs archive -type fileReader struct { - r *Reader - data *dataReader - in *inode.Inode - fragmentData []byte - fragged bool - fragOnly bool - read int - FileSize int //FileSize is the total size of the given file -} - -var ( - //ErrPathIsNotFile returns when trying to read from a file, but the given path is NOT a file. - errPathIsNotFile = errors.New("the given path is not a file") -) - -//ReadFile provides a squashfs.FileReader for the file at the given location. -func (r *Reader) newFileReader(in *inode.Inode) (*fileReader, error) { - var rdr fileReader - rdr.in = in - if in.Type != inode.FileType && in.Type != inode.ExtFileType { - return nil, errPathIsNotFile - } - switch in.Type { - case inode.FileType: - fil := in.Info.(inode.File) - rdr.fragged = fil.Fragmented - rdr.fragOnly = fil.BlockStart == 0 - rdr.FileSize = int(fil.Size) - case inode.ExtFileType: - fil := in.Info.(inode.ExtFile) - rdr.fragged = fil.Fragmented - rdr.fragOnly = fil.BlockStart == 0 - rdr.FileSize = int(fil.Size) - } - var err error - if rdr.fragged { - rdr.fragmentData, err = r.getFragmentDataFromInode(in) - if err != nil { - return nil, err - } - } - if !rdr.fragOnly { - rdr.data, err = r.newDataReaderFromInode(in) - if err != nil { - return nil, err - } - } - return &rdr, nil -} - -func (f *fileReader) Read(p []byte) (int, error) { - if f.fragOnly { - n, err := bytes.NewBuffer(f.fragmentData[f.read:]).Read(p) - f.read += n - if err != nil { - return n, err - } - return n, nil - } - var read int - n, err := f.data.Read(p) - read += n - if f.fragged && err == io.EOF { - if f.fragmentData == nil { - f.fragmentData, err = f.r.getFragmentDataFromInode(f.in) - if err != nil { - return read, err - } - } - n, err = bytes.NewBuffer(f.fragmentData).Read(p[read:]) - read += n - if err != nil { - return read, err - } - } else if err != nil { - return read, err - } - return read, nil -} - -func (f *fileReader) WriteTo(w io.Writer) (int64, error) { - if f.fragOnly { - n, err := w.Write(f.fragmentData) - return int64(n), err - } - if !f.fragged { - return f.data.WriteTo(w) - } - n, err := f.data.WriteTo(w) - if err != nil { - return int64(n), err - } - nn, err := w.Write(f.fragmentData) - return int64(nn) + n, err -} diff --git a/reader_fs.go b/reader_fs.go index 025661b..432aa0e 100644 --- a/reader_fs.go +++ b/reader_fs.go @@ -2,11 +2,10 @@ package squashfs import ( "bytes" - "errors" "io" "io/fs" - "os" "path" + "path/filepath" "strings" "github.com/CalebQ42/squashfs/internal/directory" @@ -16,11 +15,19 @@ import ( //FS is a fs.FS representation of a squashfs directory. //Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS type FS struct { - i *inode.Inode - r *Reader - parent *FS - name string - entries []*directory.Entry + *File + e []directory.Entry +} + +func (r Reader) newFS(e directory.Entry) (f *FS, err error) { + f = new(FS) + f.i, err = r.inodeFromDir(e) + if err != nil { + return + } + f.r = &r + f.e, err = r.readDirectory(f.i) + return } //Open opens the file at name. Returns a squashfs.File. @@ -32,59 +39,46 @@ func (f FS) Open(name string) (fs.File, error) { Err: fs.ErrInvalid, } } - name = path.Clean(strings.TrimPrefix(name, "/")) + name = filepath.Clean(name) + if name == "." || name == "" { + return f.File, nil + } split := strings.Split(name, "/") - if split[0] == ".." { - if f.parent == nil { - //This should only happen on the root FS + for i := range f.e { + if f.e[i].Name != split[0] { + continue + } + if len(split) > 1 && f.e[i].Type != inode.Dir { return nil, &fs.PathError{ Op: "open", Path: name, - //TODO: make error clearer - Err: errors.New("trying to get file outside of squashfs"), + Err: fs.ErrNotExist, } } - 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++ { - if split[0] == f.entries[i].Name { - if len(split) == 1 { - return f.r.newFileFromDirEntry(f.entries[i], &f) - } - sub, err := f.Sub(split[0]) + if len(split) > 1 { + newFS, err := f.r.newFS(f.e[i]) if err != nil { - if pathErr, ok := err.(*fs.PathError); ok { - pathErr.Op = "open" - pathErr.Path = name - return nil, err - } return nil, &fs.PathError{ Op: "open", Path: name, Err: err, } } - fil, err := sub.Open(strings.Join(split[1:], "/")) + out, err := newFS.Open(strings.Join(split[1:], "/")) if err != nil { - if pathErr, ok := err.(*fs.PathError); ok { - if pathErr.Err == fs.ErrNotExist { - continue - } - pathErr.Op = "open" - pathErr.Path = name - return nil, err - } - return nil, &fs.PathError{ - Op: "open", - Path: name, - Err: err, - } + err.(*fs.PathError).Path = name } - return fil, nil + return out, err } + out, err := f.r.newFile(f.e[i]) + if err != nil { + err = &fs.PathError{ + Op: "open", + Path: name, + Err: err, + } + } + return out, err } return nil, &fs.PathError{ Op: "open", @@ -95,6 +89,7 @@ func (f FS) Open(name string) (fs.File, error) { //Glob returns the name of the files at the given pattern. //All paths are relative to the FS. +//Uses filepath.Match to compare names. func (f FS) Glob(pattern string) (out []string, err error) { if !fs.ValidPath(pattern) { return nil, &fs.PathError{ @@ -103,24 +98,12 @@ func (f FS) Glob(pattern string) (out []string, err error) { Err: fs.ErrInvalid, } } - pattern = path.Clean(strings.TrimPrefix(pattern, "/")) + pattern = filepath.Clean(pattern) split := strings.Split(pattern, "/") - if split[0] == ".." { - if f.parent == nil { - //This should only happen on the root FS - return nil, &fs.PathError{ - Op: "readdir", - Path: pattern, - //TODO: make error clearer - Err: errors.New("trying to get file outside of squashfs"), - } - } - return f.parent.Glob(strings.Join(split[1:], "/")) - } - for i := 0; i < len(f.entries); i++ { - if match, _ := path.Match(split[0], f.entries[i].Name); match { + for i := 0; i < len(f.e); i++ { + if match, _ := path.Match(split[0], f.e[i].Name); match { if len(split) == 1 { - out = append(out, f.entries[i].Name) + out = append(out, f.e[i].Name) continue } sub, err := f.Sub(split[0]) @@ -156,7 +139,7 @@ func (f FS) Glob(pattern string) (out []string, err error) { } } for i := 0; i < len(subGlob); i++ { - subGlob[i] = f.name + "/" + subGlob[i] + subGlob[i] = f.File.e.Name + "/" + subGlob[i] } out = append(out, subGlob...) } @@ -174,28 +157,15 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) { Err: fs.ErrInvalid, } } - name = path.Clean(strings.TrimPrefix(name, "/")) + name = filepath.Clean(name) + if name == "." || name == "" { + return f.File.ReadDir(-1) + } split := strings.Split(name, "/") - if split[0] == ".." { - if f.parent == nil { - //This should only happen on the root FS - return nil, &fs.PathError{ - Op: "readdir", - Path: name, - //TODO: make error clearer - Err: errors.New("trying to get file outside of squashfs"), - } - } - return f.parent.ReadDir(strings.Join(split[1:], "/")) - } - 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++ { - if split[0] == f.entries[i].Name { + for i := 0; i < len(f.e); i++ { + if split[0] == f.e[i].Name { if len(split) == 1 { - in, err := f.r.getInodeFromEntry(f.entries[i]) + fi, err := f.r.newFile(f.e[i]) if err != nil { return nil, &fs.PathError{ Op: "readdir", @@ -203,23 +173,15 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) { Err: err, } } - ents, err := f.r.readDirFromInode(in) + out, err := fi.ReadDir(-1) if err != nil { - return nil, &fs.PathError{ + err = &fs.PathError{ Op: "readdir", Path: name, Err: err, } } - out := make([]fs.DirEntry, len(ents)) - for i, ent := range ents { - out[i] = &DirEntry{ - en: ent, - parent: &f, - r: f.r, - } - } - return out, nil + return out, err } sub, err := f.Sub(split[0]) if err != nil { @@ -294,41 +256,23 @@ func (f FS) Stat(name string) (fs.FileInfo, error) { Err: fs.ErrInvalid, } } - name = path.Clean(strings.TrimPrefix(name, "/")) + name = filepath.Clean(strings.TrimPrefix(name, "/")) + if name == "." || name == "" { + return f.File.Stat() + } split := strings.Split(name, "/") - if split[0] == ".." { - if f.parent == nil { - //This should only happen on the root FS - return nil, &fs.PathError{ - Op: "stat", - Path: name, - //TODO: make error clearer - Err: errors.New("trying to get file outside of squashfs"), - } - } - return f.parent.Stat(strings.Join(split[1:], "/")) - } - 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++ { - if split[0] == f.entries[i].Name { + for i := 0; i < len(f.e); i++ { + if split[0] == f.e[i].Name { if len(split) == 1 { - in, err := f.r.getInodeFromEntry(f.entries[i]) + in, err := f.r.newFileInfo(f.e[i]) if err != nil { - return nil, &fs.PathError{ + err = &fs.PathError{ Op: "stat", Path: name, Err: err, } } - return FileInfo{ - i: in, - parent: &f, - r: f.r, - name: f.entries[i].Name, - }, nil + return in, err } sub, err := f.Sub(split[0]) if err != nil { @@ -381,68 +325,38 @@ func (f FS) Sub(dir string) (fs.FS, error) { Err: fs.ErrInvalid, } } - dir = path.Clean(strings.TrimPrefix(dir, "/")) + dir = filepath.Clean(dir) + if dir == "." || dir == "" { + return f, nil + } split := strings.Split(dir, "/") - if split[0] == ".." { - if f.parent == nil { - //This should only happen on the root FS + for i := range f.e { + if f.e[i].Name != split[0] { + continue + } + if f.e[i].Type != inode.Dir { return nil, &fs.PathError{ Op: "sub", Path: dir, - //TODO: make error clearer - Err: errors.New("trying to get file outside of squashfs"), + Err: fs.ErrNotExist, } } - return f.parent.Sub(strings.Join(split[1:], "/")) - } - if split[0] == "." { - return f, nil - } - for i := 0; i < len(f.entries); i++ { - if split[0] == f.entries[i].Name { - if len(split) == 1 { - in, err := f.r.getInodeFromEntry(f.entries[i]) - if err != nil { - return nil, &fs.PathError{ - Op: "sub", - Path: dir, - Err: err, - } - } - ents, err := f.r.readDirFromInode(in) - if err != nil { - return nil, &fs.PathError{ - Op: "sub", - Path: dir, - Err: err, - } - } - return FS{ - i: in, - r: f.r, - parent: &f, - name: f.entries[i].Name, - entries: ents, - }, nil + newFS, err := f.r.newFS(f.e[i]) + if err != nil { + return nil, &fs.PathError{ + Op: "sub", + Path: dir, + Err: err, } - sub, err := f.Sub(strings.Join(split[1:], "/")) + } + if len(split) > 1 { + ret, err := newFS.Sub(strings.Join(split[1:], "/")) if err != nil { - if pathErr, ok := err.(*fs.PathError); ok { - if pathErr.Err == fs.ErrNotExist { - continue - } - pathErr.Op = "sub" - pathErr.Path = dir - return nil, pathErr - } - return nil, &fs.PathError{ - Op: "sub", - Path: dir, - Err: err, - } + err.(*fs.PathError).Path = dir } - return sub, nil + return ret, err } + return newFS, nil } return nil, &fs.PathError{ Op: "sub", @@ -450,61 +364,3 @@ func (f FS) Sub(dir string) (fs.FS, error) { Err: fs.ErrNotExist, } } - -func (f FS) path() string { - if f.name == "/" { - return f.name - } else if f.parent.name == "/" { - return f.name - } - return f.parent.path() + "/" + f.name -} - -//ExtractTo extracts the File to the given folder with the default options. -//It extracts the directory's contents to the folder. -func (f FS) ExtractTo(folder string) error { - return f.ExtractWithOptions(folder, DefaultOptions()) -} - -//ExtractSymlink extracts the File to the folder with the DereferenceSymlink option. -//It extracts the directory's contents to the folder. -func (f FS) ExtractSymlink(folder string) error { - return f.ExtractWithOptions(folder, ExtractionOptions{ - DereferenceSymlink: true, - FolderPerm: fs.ModePerm, - }) -} - -//ExtractWithOptions extracts the File to the given folder with the given ExtrationOptions. -//It extracts the directory's contents to the folder. -func (f FS) ExtractWithOptions(folder string, op ExtractionOptions) error { - op.notBase = true - folder = path.Clean(folder) - err := os.MkdirAll(folder, op.FolderPerm) - if err != nil { - return err - } - errChan := make(chan error) - for i := 0; i < len(f.entries); i++ { - go func(ent *DirEntry) { - fil, goErr := ent.File() - if goErr != nil { - errChan <- goErr - return - } - errChan <- fil.ExtractWithOptions(folder, op) - fil.Close() - }(&DirEntry{ - en: f.entries[i], - parent: &f, - r: f.r, - }) - } - for i := 0; i < len(f.entries); i++ { - err := <-errChan - if err != nil { - return err - } - } - return nil -} diff --git a/reader_inode.go b/reader_inode.go new file mode 100644 index 0000000..71f9582 --- /dev/null +++ b/reader_inode.go @@ -0,0 +1,134 @@ +package squashfs + +import ( + "errors" + "io" + + "github.com/CalebQ42/squashfs/internal/data" + "github.com/CalebQ42/squashfs/internal/directory" + "github.com/CalebQ42/squashfs/internal/inode" + "github.com/CalebQ42/squashfs/internal/metadata" + "github.com/CalebQ42/squashfs/internal/toreader" +) + +func (r Reader) inodeFromRef(ref uint64) (i inode.Inode, err error) { + offset, meta := (ref>>16)+r.s.InodeTableStart, ref&0xFFFF + rdr, err := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d) + if err != nil { + return + } + _, err = rdr.Read(make([]byte, meta)) + if err != nil { + return + } + return inode.Read(rdr, r.s.BlockSize) +} + +func (r Reader) inodeFromDir(e directory.Entry) (i inode.Inode, err error) { + rdr, err := metadata.NewReader(toreader.NewReader(r.r, int64(uint64(e.BlockStart)+r.s.InodeTableStart)), r.d) + if err != nil { + return + } + _, err = rdr.Read(make([]byte, e.Offset)) + if err != nil { + return + } + return inode.Read(rdr, r.s.BlockSize) +} + +func (r Reader) getData(i inode.Inode) (io.Reader, error) { + var fragOffset uint64 + var blockOffset uint32 + var blockSizes []uint32 + var fragInd uint32 + if i.Type == inode.Fil { + fragOffset = uint64(i.Data.(inode.File).Offset) + blockOffset = i.Data.(inode.File).BlockStart + blockSizes = i.Data.(inode.File).BlockSizes + fragInd = i.Data.(inode.File).FragInd + } else if i.Type == inode.EFil { + fragOffset = uint64(i.Data.(inode.EFile).Offset) + blockOffset = i.Data.(inode.EFile).BlockStart + blockSizes = i.Data.(inode.EFile).BlockSizes + fragInd = i.Data.(inode.EFile).FragInd + } else { + return nil, errors.New("getData called on non-file type") + } + rdr, err := data.NewReader(toreader.NewReader(r.r, int64(blockOffset)), r.d, blockSizes, r.s.BlockSize) + if err != nil { + return nil, err + } + if fragInd != 0xFFFFFFFF { + var fragRdr io.Reader + fragRdr, err = r.fragReader(fragInd) + if err != nil { + return nil, err + } + _, err = fragRdr.Read(make([]byte, fragOffset)) + if err != nil { + return nil, err + } + rdr.AddFragment(fragRdr) + } + return rdr, nil +} + +func (r Reader) getFullReader(i inode.Inode) (rdr *data.FullReader, err error) { + var fragOffset uint64 + var blockOffset uint32 + var blockSizes []uint32 + var fragInd uint32 + if i.Type == inode.Fil { + fragOffset = uint64(i.Data.(inode.File).Offset) + blockOffset = i.Data.(inode.File).BlockStart + blockSizes = i.Data.(inode.File).BlockSizes + fragInd = i.Data.(inode.File).FragInd + } else if i.Type == inode.EFil { + fragOffset = uint64(i.Data.(inode.EFile).Offset) + blockOffset = i.Data.(inode.EFile).BlockStart + blockSizes = i.Data.(inode.EFile).BlockSizes + fragInd = i.Data.(inode.EFile).FragInd + } else { + return nil, errors.New("getData called on non-file type") + } + rdr = data.NewFullReader(r.r, uint64(blockOffset), r.d, blockSizes, r.s.BlockSize) + if fragInd != 0xFFFFFFFF { + var fragRdr io.Reader + fragRdr, err = r.fragReader(fragInd) + if err != nil { + return nil, err + } + _, err = fragRdr.Read(make([]byte, fragOffset)) + if err != nil { + return nil, err + } + rdr.AddFragment(fragRdr) + } + return rdr, nil +} + +func (r Reader) readDirectory(i inode.Inode) ([]directory.Entry, error) { + var offset uint64 + var blockOffset uint16 + var size uint32 + if i.Type == inode.Dir { + offset = uint64(i.Data.(inode.Directory).BlockStart) + blockOffset = i.Data.(inode.Directory).Offset + size = uint32(i.Data.(inode.Directory).Size) + } else if i.Type == inode.EDir { + offset = uint64(i.Data.(inode.EDirectory).BlockStart) + blockOffset = i.Data.(inode.EDirectory).Offset + size = i.Data.(inode.EDirectory).Size + } else { + return nil, errors.New("readDirectory called on non-directory type") + } + rdr, err := metadata.NewReader(toreader.NewReader(r.r, int64(offset+r.s.DirTableStart)), r.d) + if err != nil { + return nil, err + } + _, err = rdr.Read(make([]byte, blockOffset)) + if err != nil { + return nil, err + } + return directory.ReadEntries(rdr, size) +} diff --git a/reader_test.go b/reader_test.go index 3ac0384..1c52886 100644 --- a/reader_test.go +++ b/reader_test.go @@ -36,7 +36,7 @@ func TestSquashfs(t *testing.T) { if err != nil { t.Fatal(err) } - rdr, err := NewSquashfsReader(squashFil) + rdr, err := NewReader(squashFil) if err != nil { t.Fatal(err) } @@ -56,7 +56,7 @@ func TestSquashfsFromReader(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - rdr, err := NewSquashfsReaderFromReader(resp.Body) + rdr, err := NewReaderFromReader(resp.Body) if err != nil { t.Fatal(err) } @@ -92,7 +92,7 @@ func TestAppImage(t *testing.T) { defer aiFil.Close() stat, _ := aiFil.Stat() ai := goappimage.NewAppImage(wd + "/testing/" + appImageName) - rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) + rdr, err := NewReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) if err != nil { t.Fatal(err) } @@ -164,7 +164,7 @@ func BenchmarkDragRace(b *testing.B) { } unsquashTime := time.Since(start) start = time.Now() - rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) + rdr, err := NewReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) if err != nil { b.Fatal(err) } diff --git a/superblock.go b/superblock.go index 8a46007..e0c8079 100644 --- a/superblock.go +++ b/superblock.go @@ -1,31 +1,22 @@ package squashfs -//The types of compression supported by squashfs. -const ( - GzipCompression = 1 + iota - LzmaCompression - LzoCompression - XzCompression - Lz4Compression - ZstdCompression -) +import "math" -//Superblock contains important information about a squashfs file. Located at the very front of the archive. type superblock struct { Magic uint32 InodeCount uint32 - CreationTime uint32 + ModTime uint32 BlockSize uint32 FragCount uint32 - CompressionType uint16 + CompType uint16 BlockLog uint16 Flags uint16 - IDCount uint16 - MajorVersion uint16 - MinorVersion uint16 + IdCount uint16 + VerMaj uint16 + VerMin uint16 RootInodeRef uint64 - BytesUsed uint64 - IDTableStart uint64 + Size uint64 + IdTableStart uint64 XattrTableStart uint64 InodeTableStart uint64 DirTableStart uint64 @@ -33,94 +24,53 @@ type superblock struct { ExportTableStart uint64 } -//SuperblockFlags is the series of flags describing how a squashfs archive is packed. -type SuperblockFlags struct { - //If true, inodes are stored uncompressed. - UncompressedInodes bool - //If true, data is stored uncompressed. - UncompressedData bool - check bool - //If true, fragments are stored uncompressed. - UncompressedFragments bool - //If true, ALL data is stored in sequential data blocks instead of utilizing fragments. - NoFragments bool - //If true, the last block of data will always be stored as a fragment if it's less then the block size. - AlwaysFragment bool - //If true, duplicate files are only stored once. (Currently unsupported) - RemoveDuplicates bool - //If true, the export table is populated. (Currently unsupported) - Exportable bool - //If true, the xattr table is uncompressed. (Currently unsupported) - UncompressedXattr bool - //If true, the xattr table is not populated. (Currently unsupported) - NoXattr bool - compressorOptions bool - //If true, the UID/GID table is stored uncompressed. - UncompressedIDs bool +func (s superblock) hasMagic() bool { + return s.Magic == 0x73717368 } -//DefaultFlags are the default SuperblockFlags that are used. -var DefaultFlags = SuperblockFlags{ - RemoveDuplicates: true, - Exportable: true, +func (s superblock) checkBlockLog() bool { + return s.BlockLog == uint16(math.Log2(float64(s.BlockSize))) } -//GetFlags returns a SuperblockFlags for a given superblock. -func (s *superblock) GetFlags() SuperblockFlags { - return SuperblockFlags{ - UncompressedInodes: s.Flags&0x1 == 0x1, - UncompressedData: s.Flags&0x2 == 0x2, - check: s.Flags&0x4 == 0x4, - UncompressedFragments: s.Flags&0x8 == 0x8, - NoFragments: s.Flags&0x10 == 0x10, - AlwaysFragment: s.Flags&0x20 == 0x20, - RemoveDuplicates: s.Flags&0x40 == 0x40, - Exportable: s.Flags&0x80 == 0x80, - UncompressedXattr: s.Flags&0x100 == 0x100, - NoXattr: s.Flags&0x200 == 0x200, - compressorOptions: s.Flags&0x400 == 0x400, - UncompressedIDs: s.Flags&0x800 == 0x800, - } +func (s superblock) uncompressedInodes() bool { + return s.Flags&0x1 == 0x1 } -//ToUint returns the uint16 representation of the given SuperblockFlags -func (s *SuperblockFlags) ToUint() uint16 { - var out uint16 - if s.UncompressedInodes { - out = out | 0x1 - } - if s.UncompressedData { - out = out | 0x2 - } - if s.check { - out = out | 0x4 - } - if s.UncompressedFragments { - out = out | 0x8 - } - if s.NoFragments { - out = out | 0x10 - } - if s.AlwaysFragment { - out = out | 0x20 - } - if s.RemoveDuplicates { - out = out | 0x40 - } - if s.Exportable { - out = out | 0x80 - } - if s.UncompressedXattr { - out = out | 0x100 - } - if s.NoXattr { - out = out | 0x200 - } - if s.compressorOptions { - out = out | 0x400 - } - if s.UncompressedIDs { - out = out | 0x800 - } - return out +func (s superblock) uncompressedData() bool { + return s.Flags&0x2 == 0x2 +} +func (s superblock) uncompressedFragments() bool { + return s.Flags&0x8 == 0x8 +} + +func (s superblock) noFragments() bool { + return s.Flags&0x10 == 0x10 +} + +func (s superblock) alwaysFragment() bool { + return s.Flags&0x20 == 0x20 +} + +func (s superblock) duplicates() bool { + return s.Flags&0x40 == 0x40 +} + +func (s superblock) exportable() bool { + return s.Flags&0x80 == 0x80 +} + +func (s superblock) uncompressedXattrs() bool { + return s.Flags&0x100 == 0x100 +} + +func (s superblock) noXattrs() bool { + return s.Flags&0x200 == 0x200 +} + +func (s superblock) compressionOptions() bool { + return s.Flags&0x400 == 0x400 +} + +func (s superblock) uncompressedIDs() bool { + return s.Flags&0x800 == 0x800 }