Files
squashfs/reader_file.go
T
2021-12-02 13:43:18 +01:00

383 lines
9.5 KiB
Go

package squashfs
import (
"errors"
"io"
"io/fs"
"log"
"os"
"path"
"strconv"
"strings"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
//File represents a file inside a squashfs archive.
type File struct {
i *inode.Inode
parent *FS
r *Reader
reader *fileReader
name string
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
}
//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)
if err != nil {
return nil, err
}
if file.IsRegular() {
file.reader, err = r.newFileReader(file.i)
}
return
}
//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
}
//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)
}
return 0, errors.New("can only read files")
}
//WriteTo writes all data from the file to the writer. This is multi-threaded.
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")
}
//Close simply nils the underlying reader. Here mostly to satisfy fs.File
func (f *File) Close() error {
f.reader = 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) {
if !f.IsDir() {
return nil, errors.New("File is not a directory")
}
ffs, err := f.FS()
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)
err = io.EOF
}
}
out := make([]fs.DirEntry, end-beg)
for i, ent := range ffs.entries[beg:end] {
out[i] = f.r.newDirEntry(ent, ffs)
}
return out, err
}
//FS returns the File as a FS.
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)
if err != nil {
return nil, err
}
return &FS{
i: f.i,
r: f.r,
parent: f.parent,
name: f.name,
entries: 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
}
//IsRegular yep.
func (f File) IsRegular() bool {
return f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType
}
//IsSymlink yep.
func (f File) IsSymlink() bool {
return f.i.Type == inode.SymType || f.i.Type == inode.ExtSymType
}
//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
}
return ""
}
//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 {
if !f.IsSymlink() {
return nil
}
if strings.HasPrefix(f.SymlinkPath(), "/") {
return nil
}
sym, err := f.parent.Open(f.SymlinkPath())
if err != nil {
return nil
}
return sym.(*File)
}
//ExtractionOptions are available options on how to extract.
type ExtractionOptions struct {
notBase bool
DereferenceSymlink bool //Replace symlinks with the target file
UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink
Verbose bool //Prints extra info to log on an error
FolderPerm fs.FileMode //The permissions used when creating the extraction folder
}
//DefaultOptions is the default ExtractionOptions.
func DefaultOptions() ExtractionOptions {
return ExtractionOptions{
DereferenceSymlink: false,
UnbreakSymlink: false,
Verbose: false,
FolderPerm: fs.ModePerm,
}
}
//ExtractTo extracts the File to the given folder with the default options.
//If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) ExtractTo(folder string) error {
return f.ExtractWithOptions(folder, DefaultOptions())
}
//ExtractSymlink extracts the File to the folder with the DereferenceSymlink option.
//If the File is a directory, it instead extracts the directory's contents to the folder.
func (f File) 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.
//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
}
}
stat, err := f.Stat()
if err != nil {
return err
}
if f.IsDir() {
if op.notBase {
err = os.Mkdir(folder+"/"+f.name, stat.Mode())
if err != nil && !os.IsExist(err) {
return err
}
} else {
op.notBase = true
}
var ents []fs.DirEntry
ents, err = f.ReadDir(0)
if err != nil {
if op.Verbose {
log.Println("Error while reading children of", f.path())
}
return err
}
errChan := make(chan error)
for i := 0; i < len(ents); i++ {
go func(ent *DirEntry) {
fil, goErr := ent.File()
if goErr != nil {
errChan <- goErr
fil.Close()
return
}
errChan <- fil.ExtractWithOptions(folder+"/"+f.name, op)
fil.Close()
}(ents[i].(*DirEntry))
}
for i := 0; i < len(ents); i++ {
err = <-errChan
if err != nil {
return err
}
}
return nil
} else if f.IsRegular() {
var fil *os.File
fil, err = os.Create(folder + "/" + f.name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.name)
fil, err = os.Create(folder + "/" + f.name)
if err != nil {
log.Println("Error while creating", folder+"/"+f.name)
return err
}
} else if err != nil {
return err
}
_, err = io.Copy(fil, f)
if err != nil {
log.Println("Error while copying data to", folder+"/"+f.name)
return err
}
return nil
} else if f.IsSymlink() {
symPath := f.SymlinkPath()
if op.DereferenceSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name)
}
return errors.New("cannot get symlink target")
}
fil.name = f.name
err = fil.ExtractWithOptions(folder, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting the symlink's file:", folder+"/"+f.name)
}
return err
}
return nil
} else if op.UnbreakSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if op.Verbose {
log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name)
}
return errors.New("cannot get symlink target")
}
extractLoc := path.Clean(folder + "/" + path.Dir(symPath))
err = fil.ExtractWithOptions(extractLoc, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting ", folder+"/"+f.name)
}
return err
}
}
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name)
if os.IsExist(err) {
os.Remove(folder + "/" + f.name)
err = os.Symlink(f.SymlinkPath(), folder+"/"+f.name)
}
if err != nil {
if op.Verbose {
log.Println("Error while making symlink:", folder+"/"+f.name)
}
return err
}
return nil
}
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
}