Files
squashfs/low/reader.go
T
Will Murphy 0905141013 fix: prevent index out of range on long frag tables
Previously, reading fragment 512 would panic with index out of range.
Fix that panic by introducing an abstraction over reading blocks of
items, caching the intermediate result, and returning an item at a
particular index. The primary goal of this abstraction is to make edge
cases like requesting items on page boundaries easy to unit test for.

Additionally, fix unit tests by making t.Fatal calls protected by
nil checks on the error values.
2025-06-06 15:46:22 -05:00

201 lines
5.9 KiB
Go

package squashfslow
import (
"encoding/binary"
"errors"
"io"
"math"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/inode"
)
// 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
Root Directory
fragTable []fragEntry
idTable []uint32
exportTable []uint64
Superblock superblock
}
func NewReader(r io.ReaderAt) (rdr Reader, err error) {
rdr.r = r
err = binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &rdr.Superblock)
if err != nil {
return rdr, errors.Join(errors.New("failed to read superblock"), err)
}
if !rdr.Superblock.ValidMagic() {
return rdr, ErrorMagic
}
if !rdr.Superblock.ValidBlockLog() {
return rdr, ErrorLog
}
if !rdr.Superblock.ValidVersion() {
return rdr, ErrorVersion
}
switch rdr.Superblock.CompType {
case ZlibCompression:
rdr.d = decompress.NewZlib()
case LZMACompression:
rdr.d, err = decompress.NewLzma()
if err != nil {
return rdr, err
}
case LZOCompression:
rdr.d, err = decompress.NewLzo()
if err != nil {
return rdr, err
}
case XZCompression:
rdr.d = decompress.NewXz()
case LZ4Compression:
rdr.d = decompress.NewLz4()
case ZSTDCompression:
rdr.d = decompress.NewZstd()
default:
return rdr, errors.New("invalid compression type. possible corrupted archive")
}
rdr.Root, err = rdr.directoryFromRef(rdr.Superblock.RootInodeRef, "")
if err != nil {
return rdr, errors.Join(errors.New("failed to read root directory"), err)
}
return
}
// 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.Superblock.IdCount {
return 0, errors.New("id out of bounds")
}
// Populate the id table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/2048)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.idTable) / 2048
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var idsToRead uint16
var idsTmp []uint32
var err error
var rdr metadata.Reader
// We can *maybe* have a slight speed increase by manually decoding instead of using reflection via binary.Read
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.IdTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
idsToRead = min(r.Superblock.IdCount-uint16(len(r.idTable)), 2048)
idsTmp = make([]uint32, idsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(&rdr, binary.LittleEndian, &idsTmp)
rdr.Close()
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) {
return readPagedItems(int(i), 512, &r.fragTable, int(r.Superblock.FragCount),
func(startBlock, fragsToRead int) ([]fragEntry, error) {
// get the offset of the next block of fragments
var offset uint64
err := binary.Read(toreader.NewReader(r.r, int64(r.Superblock.FragTableStart)+int64(8*startBlock)), binary.LittleEndian, &offset)
if err != nil {
return nil, err
}
fragsTmp := make([]fragEntry, fragsToRead)
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
defer rdr.Close()
err = binary.Read(rdr, binary.LittleEndian, &fragsTmp)
if err != nil {
return nil, err
}
return fragsTmp, 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.Superblock.Exportable() {
return 0, ErrorNotExportable
}
if len(r.exportTable) > int(i) {
return r.exportTable[i], nil
} else if i >= r.Superblock.InodeCount {
return 0, errors.New("inode out of bounds")
}
// Populate the export table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/1024)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.exportTable) / 1024
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var refsToRead uint32
var refsTmp []uint64
var err error
var rdr metadata.Reader
// We can *maybe* have a slight speed increase by manually decoding instead of using reflection via binary.Read
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.ExportTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
refsToRead = min(r.Superblock.InodeCount-uint32(len(r.exportTable)), 1024)
refsTmp = make([]uint64, refsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(&rdr, binary.LittleEndian, &refsTmp)
rdr.Close()
if err != nil {
return 0, err
}
r.exportTable = append(r.exportTable, refsTmp...)
}
return r.exportTable[i], nil
}
func (r Reader) Inode(i uint32) (inode.Inode, error) {
ref, err := r.inodeRef(i)
if err != nil {
return inode.Inode{}, err
}
return r.InodeFromRef(ref)
}