diff --git a/direntry_fileinfo.go b/direntry_fileinfo.go new file mode 100644 index 0000000..a8ab09f --- /dev/null +++ b/direntry_fileinfo.go @@ -0,0 +1,137 @@ +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/file.go b/file.go index 6b391d2..581aaee 100644 --- a/file.go +++ b/file.go @@ -2,522 +2,352 @@ package squashfs import ( "errors" - "fmt" "io" + "io/fs" + "log" "os" "path" + "strconv" "strings" - "time" "github.com/CalebQ42/squashfs/internal/directory" "github.com/CalebQ42/squashfs/internal/inode" ) -//TODO: implement fs.FS, fs.ReadDirFile, fs.ReadFileFS, fs.StatFS, fs.SubFS with 1.16 - -var ( - //ErrNotDirectory is returned when you're trying to do directory things with a non-directory - errNotDirectory = errors.New("File is not a directory") - //ErrNotFile is returned when you're trying to do file things with a directory - errNotFile = errors.New("File is not a file") - //ErrNotReading is returned when running functions that are only meant to be used when reading a squashfs - errNotReading = errors.New("Function only supported when reading a squashfs") - //ErrBrokenSymlink is returned when using ExtractWithOptions with the unbreakSymlink set to true, but the symlink's file cannot be extracted. - ErrBrokenSymlink = errors.New("Extracted symlink is probably broken") -) - -//File is the main way to interact with files within squashfs, or when putting files into a squashfs. -//File can be either a file or folder. When reading from a squashfs, it reads from the datablocks. -//When writing, this holds the information on WHERE the file will be placed inside the archive. -// -//If copying data from a squashfs, the returned reader from io.Sys() implements io.WriterTo which -//will be significantly faster then calling Read directly. -//Ex: use io.Sys().(io.Reader) for io.Copy instead of using the File directly. -// -//Implements os.FileInfo and io.Reader +//File represents a file inside a squashfs archive. type File struct { - reader io.Reader - Parent *File - r *Reader //Underlying reader. When writing, will probably be an os.File. When reading this is kept nil UNTIL reading to save memory. - in *inode.Inode - name string - dir string - filType int //The file's type, using inode types. - + i *inode.Inode + parent *FS + r *Reader + reader *fileReader + name string + dirsRead int } -//get a File from a directory.entry -func (r *Reader) newFileFromDirEntry(entry *directory.Entry) (fil *File, err error) { - fil = new(File) - fil.in, err = r.getInodeFromEntry(entry) +//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 } - fil.name = entry.Name - fil.r = r - fil.filType = fil.in.Type - return -} - -//Name is the file's name -func (f *File) Name() string { - return f.name -} - -//Size is the complete size of the file. Zero if it's not a file. -func (f *File) Size() int64 { - switch f.filType { - case inode.FileType: - return int64(f.in.Info.(inode.File).Size) - case inode.ExtFileType: - return int64(f.in.Info.(inode.ExtFile).Size) - default: - return 0 - } -} - -//ModTime is the time of last modification. -func (f *File) ModTime() time.Time { - return time.Unix(int64(f.in.Header.ModifiedTime), 0) -} - -//Sys returns the underlying reader. If the reader isn't initialized, it will initialize it. -//If called on something other then a file, returns nil. -func (f *File) Sys() interface{} { - if !f.IsFile() { - return nil - } - if f.reader == nil && f.r != nil { - var err error - f.reader, err = f.r.newFileReader(f.in) - if err != nil { - return nil - } - } - return f.reader -} - -//TODO: Implement below when 1.16 drops to satisfy fs.File - -//Stat simply returns the file. It's simply here to satisfy fs.File -// func (f *File) Stat() (fs.FileInfo, error) { -// return f, nil -// } - -//Close does nothing. It's simply here to satisfy fs.File -//TODO: add actual implementation -// func (f *File) Close() error { -// return nil -// } - -//GetChildren returns a *squashfs.File slice of every direct child of the directory. If the File is not a directory, will return ErrNotDirectory -func (f *File) GetChildren() (children []*File, err error) { - children = make([]*File, 0) - if f.r == nil { - return nil, errNotReading - } - if !f.IsDir() { - return nil, errNotDirectory - } - dir, err := f.r.readDirFromInode(f.in) - if err != nil { - return - } - var fil *File - for _, entry := range dir.Entries { - fil, err = f.r.newFileFromDirEntry(&entry) - if err != nil { - return - } - fil.Parent = f - if f.name != "" { - fil.dir = f.Path() - } - children = append(children, fil) + if file.IsRegular() { + file.reader, err = r.newFileReader(file.i) } return } -//GetChildrenRecursively returns ALL children. Goes down ALL folder paths. -func (f *File) GetChildrenRecursively() (children []*File, err error) { - children = make([]*File, 0) - if f.r == nil { - return nil, errNotReading - } - if !f.IsDir() { - return nil, errNotDirectory - } - children, err = f.GetChildren() - if err != nil { - return - } - var childFolders []*File - for _, child := range children { - if child.IsDir() { - childFolders = append(childFolders, child) - } - } - for _, folds := range childFolders { - var childs []*File - childs, err = folds.GetChildrenRecursively() - if err != nil { - fmt.Println(err) - return - } - children = append(children, childs...) - } - 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 } -//Path returns the path of the file within the archive. -func (f *File) Path() string { - if f.name == "" { - return f.dir +//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 f.dir + "/" + f.name + return 0, errors.New("Can only read files") } -//GetFileAtPath tries to return the File at the given path, relative to the file. -//Returns nil if called on something other then a folder, OR if the path goes oustide the archive. -//Allows wildcards supported by path.Match (namely * and ?) and will return the FIRST file that matches. -func (f *File) GetFileAtPath(dirPath string) *File { - dirPath = path.Clean(dirPath) - dirPath = strings.TrimPrefix(dirPath, "/") - if dirPath == "" || dirPath == "." { - return f - } - if dirPath != "." && !f.IsDir() { - return nil - } - split := strings.Split(dirPath, "/") - if split[0] == ".." && f.name == "" { - return nil - } else if split[0] == ".." { - if f.Parent != nil { - return f.Parent.GetFileAtPath(strings.Join(split[1:], "/")) - } - return nil - } - children, err := f.GetChildren() - if err != nil { - return nil - } - for _, child := range children { - eq, _ := path.Match(split[0], child.name) - if eq { - return child.GetFileAtPath(strings.Join(split[1:], "/")) +//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 } -//TODO: add with 1.16 -//Open is the same as GetFileAtPath to implement fs.FS -// func (f *File) Open(name string) (fs.File, error) { -// tmp := f.GetFileAtPath(name) -// if tmp == nil { -// return tmp, fs.ErrNotExist -// } -// return tmp, nil -// } - -//IsDir returns if the file is a directory. -func (f *File) IsDir() bool { - return f.filType == inode.DirType || f.filType == inode.ExtDirType -} - -//IsSymlink returns if the file is a symlink. -func (f *File) IsSymlink() bool { - return f.filType == inode.SymType || f.filType == inode.ExtSymType -} - -//IsFile returns if the file is a file. -func (f *File) IsFile() bool { - return f.filType == inode.FileType || f.filType == inode.ExtFileType -} - -//SymlinkPath returns the path the symlink is pointing to. If the file ISN'T a symlink, will return an empty string. -//If a path begins with "/" then the symlink is pointing to an absolute path (starting from root, and not a file inside the archive) -func (f *File) SymlinkPath() string { - switch f.filType { - case inode.SymType: - return f.in.Info.(inode.Sym).Path - case inode.ExtSymType: - return f.in.Info.(inode.ExtSym).Path - default: - return "" +//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") } -} - -//GetSymlinkFile tries to return the squashfs.File associated with the symlink. If the file isn't a symlink -//or the symlink points to a location outside the archive, nil is returned. -func (f *File) GetSymlinkFile() *File { - if !f.IsSymlink() { - return nil - } - if strings.HasSuffix(f.SymlinkPath(), "/") { - return nil - } - return f.Parent.GetFileAtPath(f.SymlinkPath()) -} - -//GetSymlinkFileRecursive tries to return the squasfs.File associated with the symlink. It will recursively -//try to get the symlink's file. This will return either a non-symlink File, or nil. -func (f *File) GetSymlinkFileRecursive() *File { - if !f.IsSymlink() { - return nil - } - if strings.HasSuffix(f.SymlinkPath(), "/") { - return nil - } - sym := f - for { - sym = sym.GetSymlinkFile() - if sym == nil { - return nil - } - if !sym.IsSymlink() { - return sym - } - } -} - -//Mode returns the os.FileMode of the File. Sets mode bits for directories and symlinks. -func (f *File) Mode() os.FileMode { - mode := os.FileMode(f.in.Header.Permissions) - switch { - case f.IsDir(): - mode = mode | os.ModeDir - case f.IsSymlink(): - mode = mode | os.ModeSymlink - } - return mode -} - -//ExtractTo extracts the file to the given path. This is the same as ExtractWithOptions(path, false, false, os.ModePerm, false). -//Will NOT try to keep symlinks valid, folders extracted will have the permissions set by the squashfs, but the folder to make path will have full permissions (777). -// -//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned. -func (f *File) ExtractTo(path string) []error { - return f.ExtractWithOptions(path, false, false, os.ModePerm, false) -} - -//ExtractSymlink is similar to ExtractTo, but when it extracts a symlink, it instead extracts the file associated with the symlink in it's place. -//This is the same as ExtractWithOptions(path, true, false, os.ModePerm, false) -func (f *File) ExtractSymlink(path string) []error { - return f.ExtractWithOptions(path, true, false, os.ModePerm, false) -} - -//ExtractWithOptions will extract the file to the given path, while allowing customization on how it works. ExtractTo is the "default" options. -//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned. -//Should only return multiple errors if extracting a folder. -// -//If dereferenceSymlink is set, instead of extracting a symlink, it will extract the file the symlink is pointed to in it's place. -//If both dereferenceSymlink and unbreakSymlink is set, dereferenceSymlink takes precendence. -// -//If unbreakSymlink is set, it will also try to extract the symlink's associated file. WARNING: the symlink's file may have to go up the directory to work. -//If unbreakSymlink is set and the file cannot be extracted, a ErrBrokenSymlink will be appended to the returned error slice. -// -//folderPerm only applies to the folders created to get to path. Folders from the archive are given the correct permissions defined by the archive. -func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlink bool, folderPerm os.FileMode, verbose bool) (errs []error) { - errs = make([]error, 0) - err := os.MkdirAll(path, folderPerm) + ffs, err := f.FS() if err != nil { - return []error{err} + return nil, err } - switch { - case f.IsDir(): - if f.name != "" { - //TODO: check if folder is present, and if so, try to set it's permission - err = os.Mkdir(path+"/"+f.name, os.ModePerm) - if err != nil { - if verbose { - fmt.Println("Error while making: ", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return - } - var fil *os.File - fil, err = os.Open(path + "/" + f.name) - if err != nil { - if verbose { - fmt.Println("Error while opening:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return - } - fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID])) - //don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0 - // if err != nil { - // if verbose { - // fmt.Println("Error while changing owner:", path+"/"+f.Name) - // fmt.Println(err) - // } - // errs = append(errs, err) - // } - err = fil.Chmod(f.Mode()) - if err != nil { - if verbose { - fmt.Println("Error while changing owner:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, 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 } - var children []*File - children, err = f.GetChildren() + } + 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{ + entries: ents, + parent: f.parent, + r: f.r, + name: f.name, + }, 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 { - if verbose { - fmt.Println("Error getting children for:", f.Path()) - fmt.Println(err) + return err + } + } + stat, err := f.Stat() + if f.IsDir() { + if op.notBase { + err = os.Mkdir(folder+"/"+f.name, stat.Mode()) + if err != nil && !os.IsExist(err) { + return err } - errs = append(errs, err) - return + } else { + op.notBase = true } - finishChan := make(chan []error) - for _, child := range children { - go func(child *File) { - if f.name == "" { - finishChan <- child.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose) - } else { - finishChan <- child.ExtractWithOptions(path+"/"+f.name, dereferenceSymlink, unbreakSymlink, folderPerm, verbose) + 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 } - }(child) + errChan <- fil.ExtractWithOptions(folder+"/"+f.name, op) + fil.Close() + return + }(ents[i].(*DirEntry)) } - for range children { - errs = append(errs, (<-finishChan)...) + for i := 0; i < len(ents); i++ { + err = <-errChan + if err != nil { + return err + } } - return - case f.IsFile(): + return nil + } else if f.IsRegular() { var fil *os.File - fil, err = os.Create(path + "/" + f.name) + fil, err = os.Create(folder + "/" + f.name) if os.IsExist(err) { - err = os.Remove(path + "/" + f.name) + os.Remove(folder + "/" + f.name) + fil, err = os.Create(folder + "/" + f.name) if err != nil { - if verbose { - fmt.Println("Error while making:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return - } - fil, err = os.Create(path + "/" + f.name) - if err != nil { - if verbose { - fmt.Println("Error while making:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return + log.Println("Error while creating", folder+"/"+f.name) + return err } } else if err != nil { - if verbose { - fmt.Println("Error while making:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return - } //Since we will be reading from the file - _, err = io.Copy(fil, f.Sys().(io.Reader)) - if err != nil { - if verbose { - fmt.Println("Error while Copying data to:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) - return + return err } - fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID])) - //don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0 - // if err != nil { - // if verbose { - // fmt.Println("Error while changing owner:", path+"/"+f.Name) - // fmt.Println(err) - // } - // errs = append(errs, err) - // return - // } - err = fil.Chmod(f.Mode()) + _, err = io.Copy(fil, f) if err != nil { - if verbose { - fmt.Println("Error while setting permissions for:", path+"/"+f.name) - fmt.Println(err) - } - errs = append(errs, err) + log.Println("Error while copying data to", folder+"/"+f.name) + return err } - return - case f.IsSymlink(): + return nil + } else if f.IsSymlink() { symPath := f.SymlinkPath() - if dereferenceSymlink { + if op.DereferenceSymlink { fil := f.GetSymlinkFile() if fil == nil { - if verbose { - fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name) + if op.Verbose { + log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name) } - return + return errors.New("Cannot get symlink target") } fil.name = f.name - extracSymErrs := fil.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose) - if len(extracSymErrs) > 0 { - if verbose { - fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name) - fmt.Println(extracSymErrs) + err = fil.ExtractWithOptions(folder, op) + if err != nil { + if op.Verbose { + log.Println("Error while extracting the symlink's file:", folder+"/"+f.name) } - errs = append(errs, extracSymErrs...) + return err } - return - } else if unbreakSymlink { + return nil + } else if op.UnbreakSymlink { fil := f.GetSymlinkFile() - if fil != nil { - symPath = path + "/" + symPath - paths := strings.Split(symPath, "/") - extracSymErrs := fil.ExtractWithOptions(strings.Join(paths[:len(paths)-1], "/"), dereferenceSymlink, unbreakSymlink, folderPerm, verbose) - if len(extracSymErrs) > 0 { - if verbose { - fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name) - fmt.Println(extracSymErrs) - } - errs = append(errs, extracSymErrs...) + if fil == nil { + if op.Verbose { + log.Println("Symlink path(", symPath, ") is unobtainable:", folder+"/"+f.name) } - } else { - if verbose { - fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+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 + return err } } - err = os.Symlink(f.SymlinkPath(), path+"/"+f.name) + 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 verbose { - fmt.Println("Error while making symlink:", path+"/"+f.name) - fmt.Println(err) + if op.Verbose { + log.Println("Error while making symlink:", folder+"/"+f.name) } - errs = append(errs, err) + return err } + return nil } - return -} - -//Read from the file. Doesn't do anything fancy, just pases it to the underlying io.Reader. If a directory, return io.EOF. -func (f *File) Read(p []byte) (int, error) { - if !f.IsFile() { - return 0, io.EOF - } - var err error - if f.reader == nil && f.r != nil { - f.reader, err = f.r.newFileReader(f.in) - if err != nil { - return 0, err - } - } - return f.reader.Read(p) + 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.Directory, error) { +func (r *Reader) readDirFromInode(i *inode.Inode) ([]*directory.Entry, error) { var offset uint32 var metaOffset uint16 var size uint32 @@ -541,26 +371,9 @@ func (r *Reader) readDirFromInode(i *inode.Inode) (*directory.Directory, error) if err != nil { return nil, err } - dir, err := directory.NewDirectory(br, size) + ents, err := directory.NewDirectory(br, size) if err != nil { - return dir, err + return nil, err } - return dir, 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.Header.InodeOffset))) - if err != nil { - return nil, err - } - _, err = br.Seek(int64(en.Offset), 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 + return ents, nil } diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..8488017 --- /dev/null +++ b/fs.go @@ -0,0 +1,489 @@ +package squashfs + +import ( + "bytes" + "errors" + "io" + "io/fs" + "os" + "path" + "strings" + + "github.com/CalebQ42/squashfs/internal/directory" +) + +//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 { + r *Reader + parent *FS + name string + entries []*directory.Entry +} + +//Open opens the file at name. Returns a squashfs.File. +func (f FS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: fs.ErrInvalid, + } + } + name = path.Clean(strings.TrimPrefix(name, "/")) + split := strings.Split(name, "/") + if split[0] == ".." { + if f.parent == nil { + //This should only happen on the root FS + return nil, &fs.PathError{ + Op: "open", + Path: name, + //TODO: make error clearer + Err: errors.New("Trying to get file outside of squashfs"), + } + } + return f.parent.Open(strings.Join(split[1:], "/")) + } + for i := 0; i < len(f.entries); i++ { + if match, _ := path.Match(split[0], f.entries[i].Name); match { + if len(split) == 1 { + return f.r.newFileFromDirEntry(f.entries[i], &f) + } + sub, err := f.Sub(split[0]) + 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:], "/")) + 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, + } + } + return fil, nil + } + } + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: fs.ErrNotExist, + } +} + +//Glob returns the name of the files at the given pattern. +//All paths are relative to the FS. +func (f FS) Glob(pattern string) (out []string, err error) { + if !fs.ValidPath(pattern) { + return nil, &fs.PathError{ + Op: "glob", + Path: pattern, + Err: fs.ErrInvalid, + } + } + pattern = path.Clean(strings.TrimPrefix(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 { + if len(split) == 1 { + out = append(out, f.entries[i].Name) + continue + } + sub, err := f.Sub(split[0]) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "glob" + pathErr.Path = pattern + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "glob", + Path: pattern, + Err: err, + } + } + subGlob, err := sub.(FS).Glob(strings.Join(split[1:], "/")) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "glob" + pathErr.Path = pattern + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "glob", + Path: pattern, + Err: err, + } + } + for i := 0; i < len(subGlob); i++ { + subGlob[i] = f.name + "/" + subGlob[i] + } + out = append(out, subGlob...) + } + } + return +} + +//ReadDir returns all the DirEntry returns all DirEntry's for the directory at name. +//If name is not a directory, returns an error. +func (f FS) ReadDir(name string) ([]fs.DirEntry, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: fs.ErrInvalid, + } + } + name = path.Clean(strings.TrimPrefix(name, "/")) + 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:], "/")) + } + for i := 0; i < len(f.entries); i++ { + if match, _ := path.Match(split[0], f.entries[i].Name); match { + if len(split) == 1 { + in, err := f.r.getInodeFromEntry(f.entries[i]) + if err != nil { + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: err, + } + } + ents, err := f.r.readDirFromInode(in) + if err != nil { + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: err, + } + } + out := make([]fs.DirEntry, len(f.entries)) + for i, ent := range ents { + out[i] = &DirEntry{ + en: ent, + parent: &f, + r: f.r, + } + } + return out, nil + } + sub, err := f.Sub(split[0]) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "readir" + pathErr.Path = name + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: err, + } + } + redDir, err := sub.(FS).ReadDir(strings.Join(split[1:], "/")) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "readdir" + pathErr.Path = name + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: err, + } + } + return redDir, nil + } + } + return nil, &fs.PathError{ + Op: "readdir", + Path: name, + Err: fs.ErrNotExist, + } +} + +//ReadFile returns the data (in []byte) for the file at name. +func (f FS) ReadFile(name string) ([]byte, error) { + fil, err := f.Open(name) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + pathErr.Op = "readfile" + pathErr.Path = name + return nil, pathErr + } + } + var buf bytes.Buffer + _, err = io.Copy(&buf, fil) + if err != nil { + return nil, &fs.PathError{ + Op: "readfile", + Path: name, + Err: err, + } + } + return buf.Bytes(), nil +} + +//Stat returns the fs.FileInfo for the file at name. +func (f FS) Stat(name string) (fs.FileInfo, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: fs.ErrInvalid, + } + } + name = path.Clean(strings.TrimPrefix(name, "/")) + 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:], "/")) + } + for i := 0; i < len(f.entries); i++ { + if match, _ := path.Match(split[0], f.entries[i].Name); match { + if len(split) == 1 { + in, err := f.r.getInodeFromEntry(f.entries[i]) + if err != nil { + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: err, + } + } + return FileInfo{ + i: in, + parent: &f, + r: f.r, + name: f.entries[i].Name, + }, nil + } + sub, err := f.Sub(split[0]) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "stat" + pathErr.Path = name + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: err, + } + } + stat, err := sub.(FS).Stat(strings.Join(split[1:], "/")) + if err != nil { + if pathErr, ok := err.(*fs.PathError); ok { + if pathErr.Err == fs.ErrNotExist { + continue + } + pathErr.Op = "stat" + pathErr.Path = name + return nil, pathErr + } + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: err, + } + } + return stat, nil + } + } + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: fs.ErrNotExist, + } +} + +//Sub returns the FS at dir +func (f FS) Sub(dir string) (fs.FS, error) { + if !fs.ValidPath(dir) { + return nil, &fs.PathError{ + Op: "sub", + Path: dir, + Err: fs.ErrInvalid, + } + } + dir = path.Clean(strings.TrimPrefix(dir, "/")) + split := strings.Split(dir, "/") + if split[0] == ".." { + if f.parent == nil { + //This should only happen on the root FS + return nil, &fs.PathError{ + Op: "sub", + Path: dir, + //TODO: make error clearer + Err: errors.New("Trying to get file outside of squashfs"), + } + } + return f.parent.Sub(strings.Join(split[1:], "/")) + } + for i := 0; i < len(f.entries); i++ { + if match, _ := path.Match(split[0], f.entries[i].Name); match { + 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{ + r: f.r, + parent: &f, + name: f.entries[i].Name, + entries: ents, + }, nil + } + sub, err := f.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, + } + } + return sub, nil + } + } + return nil, &fs.PathError{ + Op: "sub", + Path: dir, + Err: fs.ErrNotExist, + } +} + +func (f FS) path() string { + 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() + return + }(&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/internal/directory/directory.go b/internal/directory/directory.go index 44da60d..b7afee1 100644 --- a/internal/directory/directory.go +++ b/internal/directory/directory.go @@ -23,38 +23,33 @@ type EntryRaw struct { //Entry is an entry in a directory. type Entry struct { - *Header - Name string - EntryRaw + Name string + InodeOffset uint32 + InodeBlockOffset uint16 + Type uint16 } //NewEntry creates a new directory entry -func NewEntry(rdr io.Reader) (Entry, error) { - var entry Entry - err := binary.Read(rdr, binary.LittleEndian, &entry.EntryRaw) +func NewEntry(rdr io.Reader) (*Entry, error) { + var raw EntryRaw + err := binary.Read(rdr, binary.LittleEndian, &raw) if err != nil { - return Entry{}, err + return nil, err } - tmp := make([]byte, entry.EntryRaw.NameSize+1) + tmp := make([]byte, raw.NameSize+1) err = binary.Read(rdr, binary.LittleEndian, &tmp) if err != nil { - return Entry{}, err + return nil, err } - entry.Name = string(tmp) - return entry, err -} - -//Directory is an entry in the directory table of a squashfs. -//Will only have multiple headers if there are more then 256 entries -type Directory struct { - Headers []Header - Entries []Entry + 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) (*Directory, error) { - var dir Directory - var err error +func NewDirectory(base io.Reader, size uint32) (entries []*Entry, err error) { tmp := make([]byte, size) base.Read(tmp) rdr := bytes.NewBuffer(tmp) @@ -62,6 +57,7 @@ func NewDirectory(base io.Reader, size uint32) (*Directory, error) { var hdr Header err = binary.Read(rdr, binary.LittleEndian, &hdr) if err == io.ErrUnexpectedEOF { + err = nil break } else if err != nil { return nil, err @@ -71,24 +67,21 @@ func NewDirectory(base io.Reader, size uint32) (*Directory, error) { if hdr.Count%256 > 0 { headers++ } - dir.Headers = append(dir.Headers, hdr) for i := uint32(0); i < hdr.Count; i++ { if i != 0 && i%256 == 0 { - var newHdr Header - err = binary.Read(rdr, binary.LittleEndian, &newHdr) + err = binary.Read(rdr, binary.LittleEndian, &hdr) if err != nil { return nil, err } - dir.Headers = append(dir.Headers, newHdr) } - var ent Entry + var ent *Entry ent, err = NewEntry(rdr) if err != nil { return nil, err } - ent.Header = &dir.Headers[len(dir.Headers)-1] - dir.Entries = append(dir.Entries, ent) + ent.InodeOffset = hdr.InodeOffset + entries = append(entries, ent) } } - return &dir, nil + return } diff --git a/internal/inode/inodetypes.go b/internal/inode/inodetypes.go index 0d00582..ddcba7b 100644 --- a/internal/inode/inodetypes.go +++ b/internal/inode/inodetypes.go @@ -25,7 +25,7 @@ const ( //Header is the common header for all inodes type Header struct { - InodeType uint16 + Type uint16 Permissions uint16 UID uint16 GID uint16 diff --git a/internal/inode/process.go b/internal/inode/process.go index 8e42f89..7a7da6c 100644 --- a/internal/inode/process.go +++ b/internal/inode/process.go @@ -2,7 +2,9 @@ package inode import ( "encoding/binary" + "errors" "io" + "strconv" ) //Inode holds an inode. Header is the header that's common for all inodes. @@ -10,121 +12,117 @@ import ( //Info holds the actual Inode. Due to each inode type being a different type, it's store as an interface{} type Inode struct { Info interface{} //Info is the parsed specific data. It's type is defined by Type. - Type int //Type the inode type defined in the header. Here so it's easy to access Header } //ProcessInode tries to read an inode from the BlockReader func ProcessInode(br io.Reader, blockSize uint32) (*Inode, error) { - var head Header - err := binary.Read(br, binary.LittleEndian, &head) + var in Inode + err := binary.Read(br, binary.LittleEndian, &in.Header) if err != nil { return nil, err } - var info interface{} - switch head.InodeType { + switch in.Type { case DirType: var inode Dir err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case FileType: var inode File inode, err = NewFile(br, blockSize) if err != nil { return nil, err } - info = inode + in.Info = inode case SymType: var inode Sym inode, err = NewSymlink(br) if err != nil { return nil, err } - info = inode + in.Info = inode case BlockDevType: var inode Device err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case CharDevType: var inode Device err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case FifoType: var inode IPC err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case SocketType: var inode IPC err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtDirType: var inode ExtDir inode, err = NewExtendedDirectory(br) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtFileType: var inode ExtFile inode, err = NewExtendedFile(br, blockSize) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtSymType: var inode ExtSym inode, err = NewExtendedSymlink(br) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtBlockDeviceType: var inode ExtDevice err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtCharDeviceType: var inode ExtDevice err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtFifoType: var inode ExtIPC err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode case ExtSocketType: var inode ExtIPC err = binary.Read(br, binary.LittleEndian, &inode) if err != nil { return nil, err } - info = inode + in.Info = inode + default: + return nil, errors.New("Unsupported inode type: " + strconv.Itoa(int(in.Type))) } - return &Inode{ - Type: int(head.InodeType), - Header: head, - Info: info, - }, nil + return &in, nil } diff --git a/reader.go b/reader.go index 81d41c9..6dac7a0 100644 --- a/reader.go +++ b/reader.go @@ -30,7 +30,8 @@ var ( //Reader processes and reads a squashfs archive. type Reader struct { - r io.ReaderAt + FS + r *io.SectionReader decompressor compression.Decompressor root *File fragOffsets []uint64 @@ -42,24 +43,25 @@ type Reader struct { //NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { var rdr Reader - rdr.r = r - err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super) + err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super) if err != nil { return nil, err } + rdr.r = io.NewSectionReader(r, 0, int64(rdr.super.BytesUsed)) if rdr.super.Magic != magic { return nil, errNoMagic } if rdr.super.BlockLog != uint16(math.Log2(float64(rdr.super.BlockSize))) { return nil, errors.New("BlockSize and BlockLog doesn't match. The archive is probably corrupt") } + rdr.r.Seek(96, io.SeekStart) hasUnsupportedOptions := false rdr.flags = rdr.super.GetFlags() if rdr.flags.compressorOptions { switch rdr.super.CompressionType { case GzipCompression: var gzip *compression.Gzip - gzip, err = compression.NewGzipCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8)) + gzip, err = compression.NewGzipCompressorWithOptions(rdr.r) if err != nil { return nil, err } @@ -69,7 +71,7 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { rdr.decompressor = gzip case XzCompression: var xz *compression.Xz - xz, err = compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8)) + xz, err = compression.NewXzCompressorWithOptions(rdr.r) if err != nil { return nil, err } @@ -79,14 +81,14 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { rdr.decompressor = xz case Lz4Compression: var lz4 *compression.Lz4 - lz4, err = compression.NewLz4CompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8)) + lz4, err = compression.NewLz4CompressorWithOptions(rdr.r) if err != nil { return nil, err } rdr.decompressor = lz4 case ZstdCompression: var zstd *compression.Zstd - zstd, err = compression.NewZstdCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 4)) + zstd, err = compression.NewZstdCompressorWithOptions(rdr.r) if err != nil { return nil, err } @@ -126,13 +128,14 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { } unread := rdr.super.IDCount blockOffsets := make([]uint64, int(math.Ceil(float64(rdr.super.IDCount)/2048))) + rdr.r.Seek(int64(rdr.super.IDTableStart), io.SeekStart) for i := range blockOffsets { - secRdr := io.NewSectionReader(r, int64(rdr.super.IDTableStart)+(8*int64(i)), 8) - err = binary.Read(secRdr, binary.LittleEndian, &blockOffsets[i]) + err = binary.Read(rdr.r, binary.LittleEndian, &blockOffsets[i]) if err != nil { return nil, err } - idRdr, err := rdr.newMetadataReader(int64(blockOffsets[i])) + var idRdr *metadataReader + idRdr, err = rdr.newMetadataReader(int64(blockOffsets[i])) if err != nil { return nil, err } @@ -147,6 +150,23 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { } unread -= read } + metaRdr, err := rdr.newMetadataReaderFromInodeRef(rdr.super.RootInodeRef) + if err != nil { + return nil, err + } + i, err := inode.ProcessInode(metaRdr, rdr.super.BlockSize) + if err != nil { + return nil, err + } + entries, err := rdr.readDirFromInode(i) + if err != nil { + return nil, err + } + rdr.FS = FS{ + r: &rdr, + name: "/", + entries: entries, + } if hasUnsupportedOptions { return &rdr, ErrOptions } @@ -157,119 +177,3 @@ func NewSquashfsReader(r io.ReaderAt) (*Reader, error) { func (r *Reader) ModTime() time.Time { return time.Unix(int64(r.super.CreationTime), 0) } - -//ExtractTo tries to extract ALL files to the given path. This is the same as getting the root folder and extracting that. -func (r *Reader) ExtractTo(path string) []error { - if r.root == nil { - _, err := r.GetRootFolder() - if err != nil { - return []error{err} - } - } - return r.root.ExtractTo(path) -} - -//GetRootFolder returns a squashfs.File that references the root directory of the squashfs archive. -func (r *Reader) GetRootFolder() (*File, error) { - if r.root != nil { - return r.root, nil - } - mr, err := r.newMetadataReaderFromInodeRef(r.super.RootInodeRef) - if err != nil { - return nil, err - } - var root File - root.in, err = inode.ProcessInode(mr, r.super.BlockSize) - if err != nil { - return nil, err - } - root.dir = "/" - root.filType = root.in.Type - root.r = r - r.root = &root - return r.root, nil -} - -//GetAllFiles returns a slice of ALL files and folders contained in the squashfs. -func (r *Reader) GetAllFiles() (fils []*File, err error) { - if r.root == nil { - _, err := r.GetRootFolder() - if err != nil { - return nil, err - } - } - return r.root.GetChildrenRecursively() -} - -//FindFile returns the first file (in the same order as Reader.GetAllFiles) that the given function returns true for. Returns nil if nothing is found. -func (r *Reader) FindFile(query func(*File) bool) *File { - if r.root == nil { - _, err := r.GetRootFolder() - if err != nil { - return nil - } - } - fils, err := r.root.GetChildren() - if err != nil { - return nil - } - var childrenDirs []*File - for _, fil := range fils { - if query(fil) { - return fil - } - if fil.IsDir() { - childrenDirs = append(childrenDirs, fil) - } - } - for len(childrenDirs) != 0 { - var tmp []*File - for _, dirs := range childrenDirs { - chil, err := dirs.GetChildren() - if err != nil { - return nil - } - for _, child := range chil { - if query(child) { - return child - } - if child.IsDir() { - tmp = append(tmp, child) - } - } - } - childrenDirs = tmp - } - return nil -} - -//FindAll returns all files where the given function returns true. -func (r *Reader) FindAll(query func(*File) bool) (all []*File) { - if r.root == nil { - _, err := r.GetRootFolder() - if err != nil { - return nil - } - } - fils, err := r.root.GetChildrenRecursively() - if err != nil { - return nil - } - for _, fil := range fils { - if query(fil) { - all = append(all, fil) - } - } - return -} - -//GetFileAtPath will return the file at the given path. If the file cannot be found, will return nil. -func (r *Reader) GetFileAtPath(filepath string) *File { - if r.root == nil { - _, err := r.GetRootFolder() - if err != nil { - return nil - } - } - return r.root.GetFileAtPath(filepath) -} diff --git a/reader_test.go b/reader_test.go index 8480e0a..b701fd7 100644 --- a/reader_test.go +++ b/reader_test.go @@ -3,7 +3,6 @@ package squashfs import ( "fmt" "io" - "math" "net/http" "os" "os/exec" @@ -16,7 +15,7 @@ import ( const ( downloadURL = "https://github.com/srevinsaju/Firefox-Appimage/releases/download/firefox-v84.0.r20201221152838/firefox-84.0.r20201221152838-x86_64.AppImage" - appImageName = "Ultimaker_Cura-4.8.0.AppImage" + appImageName = "firefox-84.0.r20201221152838-x86_64.AppImage" squashfsName = "balenaEtcher-1.5.113-x64.AppImage.sfs" ) @@ -34,18 +33,18 @@ func TestSquashfs(t *testing.T) { t.Fatal(err) } fmt.Println("stuff", rdr.super.CompressionType) - fil := rdr.GetFileAtPath("*.desktop") - if fil == nil { - t.Fatal("Can't find desktop fil") - } - errs := fil.ExtractTo(wd + "/testing") - if len(errs) > 0 { - t.Fatal(errs) - } - errs = rdr.ExtractTo(wd + "/testing/" + squashfsName + ".d") - if len(errs) > 0 { - t.Fatal(errs) - } + // fil := rdr.GetFileAtPath("*.desktop") + // if fil == nil { + // t.Fatal("Can't find desktop fil") + // } + // errs := fil.ExtractTo(wd + "/testing") + // if len(errs) > 0 { + // t.Fatal(errs) + // } + // errs = rdr.ExtractTo(wd + "/testing/" + squashfsName + ".d") + // if len(errs) > 0 { + // t.Fatal(errs) + // } t.Fatal("No Problems") } @@ -75,9 +74,9 @@ func TestAppImage(t *testing.T) { if err != nil { t.Fatal(err) } - fmt.Println(rdr.super.BlockLog, strconv.FormatInt(int64(rdr.super.BlockSize), 2)) - fmt.Println(math.Log2(float64(rdr.super.BlockSize))) - t.Fatal("No problemo!") + os.RemoveAll(wd + "/testing/firefox") + err = rdr.ExtractTo(wd + "/testing/firefox") + t.Fatal(err) } func TestUnsquashfs(t *testing.T) { @@ -147,14 +146,14 @@ func BenchmarkDragRace(b *testing.B) { if err != nil { b.Fatal(err) } - errs := rdr.ExtractTo(wd + "/testing/firefox") - if len(errs) > 0 { - b.Fatal(errs) + err = rdr.ExtractTo(wd + "/testing/firefox") + if err != nil { + b.Fatal(err) } libTime := time.Since(start) b.Log("Unsqushfs:", unsquashTime.Round(time.Millisecond)) b.Log("Library:", libTime.Round(time.Millisecond)) - b.Log("unsquashfs is " + strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64) + "x faster") + b.Log("unsquashfs is", strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64)+"x faster") } func downloadTestAppImage(dir string) error { diff --git a/writer.go b/writer.go index 5fd70ce..7a7bbb9 100644 --- a/writer.go +++ b/writer.go @@ -20,6 +20,7 @@ type Writer struct { compressor compression.Compressor structure map[string][]*fileHolder symlinkTable map[string]string //[oldpath]newpath + folders []string uidGUIDTable []int compressionType int //BlockSize is how large the data blocks are. Can be between 4096 (4KB) and 1048576 (1 MB). @@ -49,12 +50,15 @@ func NewWriterWithOptions(compressionType int, allowErrors bool) (*Writer, error return nil, errors.New("Incorrect compression type") } if compressionType == 3 { - return nil, errors.New("LZO compression is not (currently) supported") + return nil, errors.New("Lzo compression is not (currently) supported") } return &Writer{ structure: map[string][]*fileHolder{ "/": make([]*fileHolder, 0), }, + folders: []string{ + "/", + }, symlinkTable: make(map[string]string), compressionType: compressionType, allowErrors: allowErrors, @@ -78,7 +82,7 @@ type fileHolder struct { symlink bool } -//AddFile attempts to add an os.File to the archive at it's root. +//AddFile attempts to add an os.File to the archive's root directory. func (w *Writer) AddFile(file *os.File) error { return w.AddFileToFolder("/", file) } @@ -126,6 +130,7 @@ func (w *Writer) AddFileTo(filepath string, file *os.File) error { sort.Ints(w.uidGUIDTable) } if holder.symlink { + holder.reader = file target, err := os.Readlink(file.Name()) if err != nil { return err @@ -156,9 +161,15 @@ func (w *Writer) AddFileTo(filepath string, file *os.File) error { dirsAdded = append(dirsAdded, holder.path+"/"+holder.name) } } - } else if !stat.Mode().IsRegular() { + } else if stat.Mode().IsRegular() { + holder.reader = file + } else { return errors.New("Unsupported file type " + file.Name()) } + if _, ok := w.structure[holder.path]; ok { + w.folders = append(w.folders, holder.path) + sort.Strings(w.folders) + } w.structure[holder.path] = append(w.structure[holder.path], &holder) return nil } @@ -179,6 +190,10 @@ func (w *Writer) AddReaderTo(filepath string, reader io.Reader, size uint64) err holder.name = path.Base(filepath) holder.size = size holder.reader = reader + if _, ok := w.structure[holder.path]; ok { + w.folders = append(w.folders, holder.path) + sort.Strings(w.folders) + } w.structure[holder.path] = append(w.structure[holder.path], &holder) return nil } diff --git a/writer_write.go b/writer_write.go index db1a4e9..17b56ab 100644 --- a/writer_write.go +++ b/writer_write.go @@ -42,12 +42,13 @@ func (w *Writer) WriteTo(write io.Writer) (int64, error) { InodeCount: w.countInodes(), CreationTime: uint32(time.Now().Unix()), BlockSize: w.BlockSize, - BlockLog: uint16(math.Log2(float64(w.BlockSize))), CompressionType: uint16(w.compressionType), + BlockLog: uint16(math.Log2(float64(w.BlockSize))), Flags: w.Flags.ToUint(), IDCount: uint16(len(w.uidGUIDTable)), MajorVersion: 4, MinorVersion: 0, } + _ = super return 0, errors.New("I SAID DON'T") }