From 707391babae39aecfd14e8a560d6a0ec08c6977d Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sat, 23 Dec 2023 02:48:54 -0600 Subject: [PATCH] Initial work Create Reader Pulled back in Inode decoding and superblock New Data and Metadata readers Added getting of id, fragment, and export table data lazily Added README to squashfs/squashfs --- README.md | 4 +- go.mod | 11 ++ go.sum | 10 ++ internal/data/reader.go | 73 ++++++++++ internal/decompress/decompress.go | 5 + internal/decompress/lz4.go | 15 ++ internal/decompress/lzma.go | 18 +++ internal/decompress/lzo.go | 13 ++ internal/decompress/xz.go | 18 +++ internal/decompress/zlib.go | 18 +++ internal/decompress/zstd.go | 19 +++ internal/metadata/reader.go | 62 ++++++++ internal/toreader/toreader.go | 21 +++ squashfs/README.md | 3 + squashfs/fragment.go | 7 + squashfs/inode.go | 17 +++ squashfs/inode/dir.go | 65 +++++++++ squashfs/inode/file.go | 62 ++++++++ squashfs/inode/inode.go | 144 ++++++++++++++++++ squashfs/inode/misc.go | 45 ++++++ squashfs/inode/sym.go | 46 ++++++ squashfs/reader.go | 186 ++++++++++++++++++++++++ superblock.go => squashfs/superblock.go | 0 23 files changed, 860 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/data/reader.go create mode 100644 internal/decompress/decompress.go create mode 100644 internal/decompress/lz4.go create mode 100644 internal/decompress/lzma.go create mode 100644 internal/decompress/lzo.go create mode 100644 internal/decompress/xz.go create mode 100644 internal/decompress/zlib.go create mode 100644 internal/decompress/zstd.go create mode 100644 internal/metadata/reader.go create mode 100644 internal/toreader/toreader.go create mode 100644 squashfs/README.md create mode 100644 squashfs/fragment.go create mode 100644 squashfs/inode.go create mode 100644 squashfs/inode/dir.go create mode 100644 squashfs/inode/file.go create mode 100644 squashfs/inode/inode.go create mode 100644 squashfs/inode/misc.go create mode 100644 squashfs/inode/sym.go create mode 100644 squashfs/reader.go rename superblock.go => squashfs/superblock.go (100%) diff --git a/README.md b/README.md index eb6b0c0..2206edf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/CalebQ42/squashfs)](https://pkg.go.dev/github.com/CalebQ42/squashfs) [![Go Report Card](https://goreportcard.com/badge/github.com/CalebQ42/squashfs)](https://goreportcard.com/report/github.com/CalebQ42/squashfs) -A PURE Go library to read squashfs. There is currently no plans to add archive creation support as it will almost always be better to just call `mksquashfs`. I could see some possible use cases, but probably won't spend time on it unless it's requested (open a discussion fi you want this feature). +A PURE Go library to read squashfs. There is currently no plans to add archive creation support as it will almost always be better to just call `mksquashfs`. I could see some possible use cases, but probably won't spend time on it unless it's requested (open a discussion if you want this feature). Currently has support for reading squashfs files and extracting files and folders. @@ -15,7 +15,7 @@ Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree * No Xattr parsing. This is simply because I haven't done any research on it and how to apply these in a pure go way. * Socket files are not extracted. - * From my research, it seems like a socket file would be useless if it could be created. + * From my research, it seems like a socket file would be useless if it could be created. They are still exposed when fuse mounted. * Fifo files are ignored on `darwin` ## Issues diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39a24a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/CalebQ42/squashfs + +go 1.21.5 + +require ( + github.com/pierrec/lz4/v4 v4.1.19 + github.com/ulikunitz/xz v0.5.11 + github.com/klauspost/compress v1.17.4 + github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e + github.com/therootcompany/xz v1.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..894206e --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4= +github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs= +github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/internal/data/reader.go b/internal/data/reader.go new file mode 100644 index 0000000..e4d5c16 --- /dev/null +++ b/internal/data/reader.go @@ -0,0 +1,73 @@ +package data + +import ( + "encoding/binary" + "io" + + "github.com/CalebQ42/squashfs/internal/decompress" +) + +type Reader struct { + r io.Reader + d decompress.Decompressor + frag io.Reader + sizes []uint32 + dat []byte + curOffset uint16 + curIndex uint64 +} + +func NewReader(r io.Reader, d decompress.Decompressor, sizes []uint32) (*Reader, error) { + return &Reader{ + r: r, + d: d, + sizes: sizes, + }, nil +} + +func (r *Reader) AddFrag(fragRdr io.Reader) { + r.frag = fragRdr +} + +func (r *Reader) advance() error { + r.curOffset = 0 + defer func() { r.curIndex++ }() + var err error + if r.curIndex == uint64(len(r.sizes))-1 && r.frag != nil { + r.dat, err = io.ReadAll(r.frag) + return err + } else if r.curIndex >= uint64(len(r.sizes))-1 { + return io.EOF + } + realSize := r.sizes[r.curIndex] &^ 0x8000 + r.dat = make([]byte, realSize) + err = binary.Read(r.r, binary.LittleEndian, &r.dat) + if err != nil { + return err + } + if r.sizes[r.curIndex] != realSize { + return nil + } + r.dat, err = r.d.Decompress(r.dat) + return err +} + +func (r *Reader) Read(b []byte) (int, error) { + curRead := 0 + var toRead int + for curRead < len(b) { + if r.curOffset >= uint16(len(r.dat)) { + if err := r.advance(); err != nil { + return curRead, err + } + } + toRead = len(b) - curRead + if toRead > len(r.dat)-int(r.curOffset) { + toRead = len(r.dat) - int(r.curOffset) + } + copy(b[curRead:], r.dat[r.curOffset:int(r.curOffset)+toRead]) + r.curOffset += uint16(toRead) + curRead += toRead + } + return curRead, nil +} diff --git a/internal/decompress/decompress.go b/internal/decompress/decompress.go new file mode 100644 index 0000000..eb66221 --- /dev/null +++ b/internal/decompress/decompress.go @@ -0,0 +1,5 @@ +package decompress + +type Decompressor interface { + Decompress([]byte) ([]byte, error) +} diff --git a/internal/decompress/lz4.go b/internal/decompress/lz4.go new file mode 100644 index 0000000..e2f3bdb --- /dev/null +++ b/internal/decompress/lz4.go @@ -0,0 +1,15 @@ +package decompress + +import ( + "bytes" + "io" + + "github.com/pierrec/lz4/v4" +) + +type Lz4 struct{} + +func (l Lz4) Decompress(data []byte) ([]byte, error) { + rdr := lz4.NewReader(bytes.NewReader(data)) + return io.ReadAll(rdr) +} diff --git a/internal/decompress/lzma.go b/internal/decompress/lzma.go new file mode 100644 index 0000000..e79d0db --- /dev/null +++ b/internal/decompress/lzma.go @@ -0,0 +1,18 @@ +package decompress + +import ( + "bytes" + "io" + + "github.com/ulikunitz/xz/lzma" +) + +type Lzma struct{} + +func (l Lzma) Decompress(data []byte) ([]byte, error) { + rdr, err := lzma.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + return io.ReadAll(rdr) +} diff --git a/internal/decompress/lzo.go b/internal/decompress/lzo.go new file mode 100644 index 0000000..f5783b4 --- /dev/null +++ b/internal/decompress/lzo.go @@ -0,0 +1,13 @@ +package decompress + +import ( + "bytes" + + "github.com/rasky/go-lzo" +) + +type Lzo struct{} + +func (l Lzo) Decompress(data []byte) ([]byte, error) { + return lzo.Decompress1X(bytes.NewReader(data), len(data), 0) +} diff --git a/internal/decompress/xz.go b/internal/decompress/xz.go new file mode 100644 index 0000000..02191db --- /dev/null +++ b/internal/decompress/xz.go @@ -0,0 +1,18 @@ +package decompress + +import ( + "bytes" + "io" + + "github.com/therootcompany/xz" +) + +type Xz struct{} + +func (x Xz) Decompress(data []byte) ([]byte, error) { + rdr, err := xz.NewReader(bytes.NewReader(data), 0) + if err != nil { + return nil, err + } + return io.ReadAll(rdr) +} diff --git a/internal/decompress/zlib.go b/internal/decompress/zlib.go new file mode 100644 index 0000000..3da3415 --- /dev/null +++ b/internal/decompress/zlib.go @@ -0,0 +1,18 @@ +package decompress + +import ( + "bytes" + "compress/zlib" + "io" +) + +type Zlib struct{} + +func (z Zlib) Decompress(data []byte) ([]byte, error) { + rdr, err := zlib.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer rdr.Close() + return io.ReadAll(rdr) +} diff --git a/internal/decompress/zstd.go b/internal/decompress/zstd.go new file mode 100644 index 0000000..ff24013 --- /dev/null +++ b/internal/decompress/zstd.go @@ -0,0 +1,19 @@ +package decompress + +import ( + "bytes" + "io" + + "github.com/klauspost/compress/zstd" +) + +type Zstd struct{} + +func (z Zstd) Decompress(data []byte) ([]byte, error) { + rdr, err := zstd.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer rdr.Close() + return io.ReadAll(rdr) +} diff --git a/internal/metadata/reader.go b/internal/metadata/reader.go new file mode 100644 index 0000000..58baa51 --- /dev/null +++ b/internal/metadata/reader.go @@ -0,0 +1,62 @@ +package metadata + +import ( + "encoding/binary" + "io" + + "github.com/CalebQ42/squashfs/internal/decompress" +) + +type Reader struct { + r io.Reader + d decompress.Decompressor + dat []byte + curOffset uint16 +} + +func NewReader(r io.Reader, d decompress.Decompressor) *Reader { + return &Reader{ + r: r, + d: d, + } +} + +func (r *Reader) advance() error { + r.curOffset = 0 + var size uint16 + err := binary.Read(r.r, binary.LittleEndian, &size) + if err != nil { + return err + } + realSize := size &^ 0x8000 + r.dat = make([]byte, realSize) + err = binary.Read(r.r, binary.LittleEndian, &r.dat) + if err != nil { + return err + } + if size != realSize { + return nil + } + r.dat, err = r.d.Decompress(r.dat) + return err +} + +func (r *Reader) Read(b []byte) (int, error) { + curRead := 0 + var toRead int + for curRead < len(b) { + if r.curOffset >= uint16(len(r.dat)) { + if err := r.advance(); err != nil { + return curRead, err + } + } + toRead = len(b) - curRead + if toRead > len(r.dat)-int(r.curOffset) { + toRead = len(r.dat) - int(r.curOffset) + } + copy(b[curRead:], r.dat[r.curOffset:int(r.curOffset)+toRead]) + r.curOffset += uint16(toRead) + curRead += toRead + } + return curRead, nil +} diff --git a/internal/toreader/toreader.go b/internal/toreader/toreader.go new file mode 100644 index 0000000..9600bd5 --- /dev/null +++ b/internal/toreader/toreader.go @@ -0,0 +1,21 @@ +package toreader + +import "io" + +type Reader struct { + r io.ReaderAt + offset int64 +} + +func NewReader(r io.ReaderAt, start int64) *Reader { + return &Reader{ + r: r, + offset: start, + } +} + +func (r *Reader) Read(b []byte) (int, error) { + n, err := r.r.ReadAt(b, r.offset) + r.offset += int64(n) + return n, err +} diff --git a/squashfs/README.md b/squashfs/README.md new file mode 100644 index 0000000..33eb8eb --- /dev/null +++ b/squashfs/README.md @@ -0,0 +1,3 @@ +# Lower-Level Squashfs + +This library is a lower level version of the main [squashfs](https://github.com/CalebQ42/squashfs) library that doesn't try to be easy to use and exposes a lot of information that is not necesary for must use cases. diff --git a/squashfs/fragment.go b/squashfs/fragment.go new file mode 100644 index 0000000..91003e0 --- /dev/null +++ b/squashfs/fragment.go @@ -0,0 +1,7 @@ +package squashfs + +type fragEntry struct { + start uint64 + size uint32 + _ uint32 +} diff --git a/squashfs/inode.go b/squashfs/inode.go new file mode 100644 index 0000000..531c0e1 --- /dev/null +++ b/squashfs/inode.go @@ -0,0 +1,17 @@ +package squashfs + +import ( + "github.com/CalebQ42/squashfs/internal/metadata" + "github.com/CalebQ42/squashfs/internal/toreader" + "github.com/CalebQ42/squashfs/squashfs/inode" +) + +func (r *Reader) inodeFromRef(ref uint64) (*inode.Inode, error) { + offset, meta := (ref>>16)+r.sup.InodeTableStart, ref&0xFFFF + rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d) + _, err := rdr.Read(make([]byte, meta)) + if err != nil { + return nil, err + } + return inode.Read(rdr, r.sup.BlockSize) +} diff --git a/squashfs/inode/dir.go b/squashfs/inode/dir.go new file mode 100644 index 0000000..bffe973 --- /dev/null +++ b/squashfs/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/squashfs/inode/file.go b/squashfs/inode/file.go new file mode 100644 index 0000000..1b4d461 --- /dev/null +++ b/squashfs/inode/file.go @@ -0,0 +1,62 @@ +package inode + +import ( + "encoding/binary" + "io" + "math" +) + +type fileInit struct { + BlockStart uint32 + FragInd uint32 + FragOffset uint32 + Size uint32 +} + +type File struct { + fileInit + BlockSizes []uint32 +} + +type eFileInit struct { + BlockStart uint64 + Size uint64 + Sparse uint64 + LinkCount uint32 + FragInd uint32 + FragOffset 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 + } + toRead := int(math.Floor(float64(f.Size) / float64(blockSize))) + if f.FragInd == 0xFFFFFFFF && f.Size%blockSize > 0 { + toRead++ + } + f.BlockSizes = make([]uint32, toRead) + 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 + } + toRead := int(math.Floor(float64(f.Size) / float64(blockSize))) + if f.FragInd == 0xFFFFFFFF && f.Size%uint64(blockSize) > 0 { + toRead++ + } + f.BlockSizes = make([]uint32, toRead) + err = binary.Read(r, binary.LittleEndian, &f.BlockSizes) + return +} diff --git a/squashfs/inode/inode.go b/squashfs/inode/inode.go new file mode 100644 index 0000000..21f0a2e --- /dev/null +++ b/squashfs/inode/inode.go @@ -0,0 +1,144 @@ +package inode + +import ( + "encoding/binary" + "errors" + "io" + "io/fs" + "strconv" +) + +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) { + i = new(Inode) + 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 " + strconv.Itoa(int(i.Type))) + } + return +} + +func (i Inode) Mode() (out fs.FileMode) { + out = fs.FileMode(i.Perm) + switch i.Data.(type) { + case Directory: + out |= fs.ModeDir + case EDirectory: + out |= fs.ModeDir + case Symlink: + out |= fs.ModeSymlink + case ESymlink: + out |= fs.ModeSymlink + case Device: + out |= fs.ModeDevice + case EDevice: + out |= fs.ModeDevice + case IPC: + out |= fs.ModeNamedPipe + case EIPC: + out |= fs.ModeNamedPipe + } + return +} + +func (i Inode) LinkCount() uint32 { + switch i.Data.(type) { + case EFile: + return i.Data.(EFile).LinkCount + case Directory: + return i.Data.(Directory).LinkCount + case EDirectory: + return i.Data.(EDirectory).LinkCount + case Device: + return i.Data.(Device).LinkCount + case EDevice: + return i.Data.(EDevice).LinkCount + case IPC: + return i.Data.(IPC).LinkCount + case EIPC: + return i.Data.(EIPC).LinkCount + case Symlink: + return i.Data.(Symlink).LinkCount + case ESymlink: + return i.Data.(ESymlink).LinkCount + default: + return 0 + } +} + +func (i Inode) Size() uint64 { + switch i.Data.(type) { + case File: + return uint64(i.Data.(File).Size) + case EFile: + return i.Data.(EFile).Size + // case Directory: + // return uint64(i.Data.(Directory).Size) + // case EDirectory: + // return uint64(i.Data.(EDirectory).Size) + default: + return 0 + } +} diff --git a/squashfs/inode/misc.go b/squashfs/inode/misc.go new file mode 100644 index 0000000..8ba0b61 --- /dev/null +++ b/squashfs/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/squashfs/inode/sym.go b/squashfs/inode/sym.go new file mode 100644 index 0000000..43659bb --- /dev/null +++ b/squashfs/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/squashfs/reader.go b/squashfs/reader.go new file mode 100644 index 0000000..c4d8fbe --- /dev/null +++ b/squashfs/reader.go @@ -0,0 +1,186 @@ +package squashfs + +import ( + "encoding/binary" + "errors" + "io" + "math" + "time" + + "github.com/CalebQ42/squashfs/internal/decompress" + "github.com/CalebQ42/squashfs/internal/toreader" +) + +// The types of compression supported by squashfs +const ( + ZlibCompression = uint16(iota + 1) + LZMACompression + LZOCompression + XZCompression + LZ4Compression + ZSTDCompression +) + +var ( + ErrorMagic = errors.New("magic incorrect. probably not reading squashfs archive or archive is corrupted") + ErrorLog = errors.New("block log is incorrect. possible corrupted archive") + ErrorVersion = errors.New("squashfs version of archive is not 4.0. may be corrupted") + ErrorNotExportable = errors.New("archive does not have an export table") +) + +type Reader struct { + r io.ReaderAt + d decompress.Decompressor + fragTable []fragEntry + idTable []uint32 + exportTable []uint64 + sup superblock +} + +func NewReader(r io.ReaderAt) (rdr *Reader, err error) { + rdr = new(Reader) + rdr.r = r + err = binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &rdr.sup) + if err != nil { + return nil, errors.Join(errors.New("failed to read superblock"), err) + } + if !rdr.sup.checkMagic() { + return nil, ErrorMagic + } + if !rdr.sup.checkBlockLog() { + return nil, ErrorLog + } + if !rdr.sup.checkVersion() { + return nil, ErrorVersion + } + switch rdr.sup.CompType { + case ZlibCompression: + rdr.d = decompress.Zlib{} + case LZMACompression: + rdr.d = decompress.Lzma{} + case LZOCompression: + rdr.d = decompress.Lzo{} + case XZCompression: + rdr.d = decompress.Xz{} + case LZ4Compression: + rdr.d = decompress.Lz4{} + case ZSTDCompression: + rdr.d = &decompress.Zstd{} + default: + return nil, errors.New("invalid compression type. possible corrupted archive") + } + return +} + +// Returns the last time the archive was modified. +func (r *Reader) ModTime() time.Time { + return time.Unix(int64(r.sup.ModTime), 0) +} + +// Get a uid/gid at the given index. Lazily populates the reader's id table as necessary. +func (r *Reader) id(i uint16) (uint32, error) { + if len(r.idTable) > int(i) { + return r.idTable[i], nil + } else if i >= r.sup.IdCount { + return 0, errors.New("id out of bounds") + } + // Populate the id table as needed + blockNum := uint16(math.Ceil(float64(i) / 2048)) + blocksRead := len(r.idTable) / 2048 + blocksToRead := int(blockNum) - blocksRead + + var offset uint64 + var idsToRead uint16 + var idsTmp []uint32 + var err error + for i := blocksRead; i < int(blockNum)+blocksToRead; i++ { + err = binary.Read(toreader.NewReader(r.r, int64(r.sup.IdTableStart)+int64(8*i)), binary.LittleEndian, &offset) + if err != nil { + return 0, err + } + idsToRead = r.sup.IdCount - uint16(len(r.idTable)) + if idsToRead > 2048 { + idsToRead = 2048 + } + idsTmp = make([]uint32, idsToRead) + err = binary.Read(toreader.NewReader(r.r, int64(offset)), binary.LittleEndian, &idsTmp) + if err != nil { + return 0, err + } + r.idTable = append(r.idTable, idsTmp...) + } + return r.idTable[i], nil +} + +// Get a fragment entry at the given index. Lazily populates the reader's fragment table as necessary. +func (r *Reader) fragEntry(i uint32) (fragEntry, error) { + if len(r.fragTable) > int(i) { + return r.fragTable[i], nil + } else if i >= r.sup.FragCount { + return fragEntry{}, errors.New("fragment out of bounds") + } + // Populate the fragment table as needed + blockNum := uint32(math.Ceil(float64(i) / 512)) + blocksRead := len(r.fragTable) / 512 + blocksToRead := int(blockNum) - blocksRead + + var offset uint64 + var fragsToRead uint32 + var fragsTmp []fragEntry + var err error + for i := blocksRead; i < int(blockNum)+blocksToRead; i++ { + err = binary.Read(toreader.NewReader(r.r, int64(r.sup.FragTableStart)+int64(8*i)), binary.LittleEndian, &offset) + if err != nil { + return fragEntry{}, err + } + fragsToRead = r.sup.FragCount - uint32(len(r.fragTable)) + if fragsToRead > 512 { + fragsToRead = 512 + } + fragsTmp = make([]fragEntry, fragsToRead) + err = binary.Read(toreader.NewReader(r.r, int64(offset)), binary.LittleEndian, &fragsTmp) + if err != nil { + return fragEntry{}, err + } + r.fragTable = append(r.fragTable, fragsTmp...) + } + return r.fragTable[i], nil +} + +// Get an inode reference at the given index. Lazily populates the reader's export table as necessary. +func (r *Reader) inodeRef(i uint32) (uint64, error) { + if !r.sup.exportable() { + return 0, ErrorNotExportable + } + if len(r.exportTable) > int(i) { + return r.exportTable[i], nil + } else if i >= r.sup.InodeCount { + return 0, errors.New("inode out of bounds") + } + // Populate the export table as neede + blockNum := uint32(math.Ceil(float64(i) / 1024)) + blocksRead := len(r.exportTable) / 1024 + blocksToRead := int(blockNum) - blocksRead + + var offset uint64 + var refsToRead uint32 + var refsTmp []uint64 + var err error + for i := blocksRead; i < int(blockNum)+blocksToRead; i++ { + err = binary.Read(toreader.NewReader(r.r, int64(r.sup.ExportTableStart)+int64(8*i)), binary.LittleEndian, &offset) + if err != nil { + return 0, err + } + refsToRead = r.sup.InodeCount - uint32(len(r.exportTable)) + if refsToRead > 1024 { + refsToRead = 1024 + } + refsTmp = make([]uint64, refsToRead) + err = binary.Read(toreader.NewReader(r.r, int64(offset)), binary.LittleEndian, &refsTmp) + if err != nil { + return 0, err + } + r.exportTable = append(r.exportTable, refsTmp...) + } + return r.exportTable[i], nil +} diff --git a/superblock.go b/squashfs/superblock.go similarity index 100% rename from superblock.go rename to squashfs/superblock.go