diff --git a/direntry_fileinfo.go b/direntry_fileinfo.go index 9e2100e..a8ab09f 100644 --- a/direntry_fileinfo.go +++ b/direntry_fileinfo.go @@ -9,6 +9,7 @@ import ( "github.com/CalebQ42/squashfs/internal/inode" ) +//DirEntry is a child of a directory. type DirEntry struct { en *directory.Entry parent *FS @@ -23,14 +24,17 @@ func (r *Reader) newDirEntry(en *directory.Entry, parent *FS) *DirEntry { } } +//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: @@ -42,6 +46,7 @@ func (d DirEntry) Type() fs.FileMode { } } +//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 { @@ -72,6 +77,7 @@ func (r *Reader) getInodeFromEntry(en *directory.Entry) (*inode.Inode, error) { return i, nil } +//FileInfo is a fs.FileInfo for a file. type FileInfo struct { i *inode.Inode parent *FS @@ -79,10 +85,12 @@ type FileInfo struct { 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: @@ -93,6 +101,7 @@ func (f FileInfo) Size() int64 { 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 { @@ -108,19 +117,21 @@ func (f FileInfo) Mode() fs.FileMode { 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{} { - return &File{ - name: f.name, - i: f.i, - r: f.r, - parent: f.parent, + fil, err := f.File() + if err != nil { + return nil } + return fil } diff --git a/file.go b/file.go index 1a82475..581aaee 100644 --- a/file.go +++ b/file.go @@ -1,516 +1,379 @@ -// package squashfs +package squashfs -// import ( -// "errors" -// "fmt" -// "io" -// "os" -// "path" -// "strings" -// "time" +import ( + "errors" + "io" + "io/fs" + "log" + "os" + "path" + "strconv" + "strings" -// "github.com/CalebQ42/squashfs/internal/directory" -// "github.com/CalebQ42/squashfs/internal/inode" -// ) + "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 +//File represents a file inside a squashfs archive. +type File struct { + i *inode.Inode + parent *FS + r *Reader + reader *fileReader + name string + dirsRead int +} -// 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 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 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 -// 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. +//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 +} -// //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) -// if err != nil { -// return nil, err -// } -// fil.name = entry.Name -// fil.r = r -// fil.filType = fil.in.Type -// 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 +} -// //Name is the file's name -// func (f *File) Name() string { -// return f.name -// } +//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") +} -// //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 -// } -// } +//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") +} -// //ModTime is the time of last modification. -// func (f *File) ModTime() time.Time { -// return time.Unix(int64(f.in.Header.ModifiedTime), 0) -// } +//Close simply nils the underlying reader. Here mostly to satisfy fs.File +func (f *File) Close() error { + f.reader = nil + return nil +} -// //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 -// } +//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 +} -// //TODO: Implement below when 1.16 drops to satisfy fs.File +//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 +} -// //Stat simply returns the file. It's simply here to satisfy fs.File -// // func (f *File) Stat() (fs.FileInfo, error) { -// // return f, nil -// // } +//IsDir Yep. +func (f File) IsDir() bool { + return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType +} -// //Close does nothing. It's simply here to satisfy fs.File -// //TODO: add actual implementation -// // func (f *File) Close() error { -// // return nil -// // } +func (f File) path() string { + if f.name == "/" { + return f.name + } + return f.parent.path() + "/" + f.name +} -// //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) -// } -// return -// } +//IsRegular yep. +func (f File) IsRegular() bool { + return f.i.Type == inode.FileType || f.i.Type == inode.ExtFileType +} -// //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 -// } +//IsSymlink yep. +func (f File) IsSymlink() bool { + return f.i.Type == inode.SymType || f.i.Type == inode.ExtSymType +} -// //Path returns the path of the file within the archive. -// func (f *File) Path() string { -// if f.name == "" { -// return f.dir -// } -// return f.dir + "/" + f.name -// } +//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 "" +} -// //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:], "/")) -// } -// } -// return nil -// } +//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) +} -// //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 -// // } +//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 +} -// //IsDir returns if the file is a directory. -// func (f *File) IsDir() bool { -// return f.filType == inode.DirType || f.filType == inode.ExtDirType -// } +//DefaultOptions is the default ExtractionOptions. +func DefaultOptions() ExtractionOptions { + return ExtractionOptions{ + DereferenceSymlink: false, + UnbreakSymlink: false, + Verbose: false, + FolderPerm: fs.ModePerm, + } +} -// //IsSymlink returns if the file is a symlink. -// func (f *File) IsSymlink() bool { -// return f.filType == inode.SymType || f.filType == inode.ExtSymType -// } +//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()) +} -// //IsFile returns if the file is a file. -// func (f *File) IsFile() bool { -// return f.filType == inode.FileType || f.filType == inode.ExtFileType -// } +//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, + }) +} -// //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 "" -// } -// } +//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 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() + return + }(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))) +} -// //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) -// if err != nil { -// return []error{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 children []*File -// children, err = f.GetChildren() -// if err != nil { -// if verbose { -// fmt.Println("Error getting children for:", f.Path()) -// fmt.Println(err) -// } -// errs = append(errs, err) -// return -// } -// 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) -// } -// }(child) -// } -// for range children { -// errs = append(errs, (<-finishChan)...) -// } -// return -// case f.IsFile(): -// var fil *os.File -// fil, err = os.Create(path + "/" + f.name) -// if os.IsExist(err) { -// err = os.Remove(path + "/" + 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 -// } -// } 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 -// } -// 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()) -// if err != nil { -// if verbose { -// fmt.Println("Error while setting permissions for:", path+"/"+f.name) -// fmt.Println(err) -// } -// errs = append(errs, err) -// } -// return -// case f.IsSymlink(): -// symPath := f.SymlinkPath() -// if dereferenceSymlink { -// fil := f.GetSymlinkFile() -// if fil == nil { -// if verbose { -// fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name) -// } -// return -// } -// 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) -// } -// errs = append(errs, extracSymErrs...) -// } -// return -// } else if 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...) -// } -// } else { -// if verbose { -// fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name) -// } -// return -// } -// } -// err = os.Symlink(f.SymlinkPath(), path+"/"+f.name) -// if err != nil { -// if verbose { -// fmt.Println("Error while making symlink:", path+"/"+f.name) -// fmt.Println(err) -// } -// errs = append(errs, err) -// } -// } -// 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) -// } +//ReadDirFromInode returns a fully populated Directory from a given Inode. +//If the given inode is not a directory it returns an error. +func (r *Reader) readDirFromInode(i *inode.Inode) ([]*directory.Entry, error) { + var offset uint32 + var metaOffset uint16 + var size uint32 + switch i.Type { + case inode.DirType: + offset = i.Info.(inode.Dir).DirectoryIndex + metaOffset = i.Info.(inode.Dir).DirectoryOffset + size = uint32(i.Info.(inode.Dir).DirectorySize) + case inode.ExtDirType: + offset = i.Info.(inode.ExtDir).DirectoryIndex + metaOffset = i.Info.(inode.ExtDir).DirectoryOffset + size = i.Info.(inode.ExtDir).DirectorySize + default: + return nil, errors.New("Not a directory inode") + } + br, err := r.newMetadataReader(int64(r.super.DirTableStart + uint64(offset))) + if err != nil { + return nil, err + } + _, err = br.Seek(int64(metaOffset), io.SeekStart) + if err != nil { + return nil, err + } + ents, err := directory.NewDirectory(br, size) + if err != nil { + return nil, err + } + return ents, nil +} diff --git a/file2.go b/file2.go deleted file mode 100644 index 7f8bec2..0000000 --- a/file2.go +++ /dev/null @@ -1,157 +0,0 @@ -package squashfs - -import ( - "errors" - "io" - "io/fs" - - "github.com/CalebQ42/squashfs/internal/directory" - "github.com/CalebQ42/squashfs/internal/inode" -) - -type File struct { - i *inode.Inode - parent *FS - r *Reader - reader *fileReader - name string - dirsRead int -} - -func (f FileInfo) File() (file *File, err error) { - file = &File{ - name: f.name, - r: f.r, - parent: f.parent, - i: f.i, - } - file.reader, err = f.r.newFileReader(f.i) - return -} - -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 - } - file.reader, err = r.newFileReader(file.i) - return -} - -func (f *File) Stat() (fs.FileInfo, error) { - return &FileInfo{ - i: f.i, - name: f.name, - parent: f.parent, - r: f.r, - }, nil -} - -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") -} - -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") -} - -func (f *File) Close() error { - f.reader = nil - return nil -} - -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 -} - -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, - }, nil -} - -func (f File) IsDir() bool { - return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType -} - -func (f File) Path() - -//ReadDirFromInode returns a fully populated Directory from a given Inode. -//If the given inode is not a directory it returns an error. -func (r *Reader) readDirFromInode(i *inode.Inode) ([]*directory.Entry, error) { - var offset uint32 - var metaOffset uint16 - var size uint32 - switch i.Type { - case inode.DirType: - offset = i.Info.(inode.Dir).DirectoryIndex - metaOffset = i.Info.(inode.Dir).DirectoryOffset - size = uint32(i.Info.(inode.Dir).DirectorySize) - case inode.ExtDirType: - offset = i.Info.(inode.ExtDir).DirectoryIndex - metaOffset = i.Info.(inode.ExtDir).DirectoryOffset - size = i.Info.(inode.ExtDir).DirectorySize - default: - return nil, errors.New("Not a directory inode") - } - br, err := r.newMetadataReader(int64(r.super.DirTableStart + uint64(offset))) - if err != nil { - return nil, err - } - _, err = br.Seek(int64(metaOffset), io.SeekStart) - if err != nil { - return nil, err - } - ents, err := directory.NewDirectory(br, size) - if err != nil { - return nil, err - } - return ents, nil -} diff --git a/fs.go b/fs.go index 88a2a41..8488017 100644 --- a/fs.go +++ b/fs.go @@ -1,19 +1,27 @@ 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{ @@ -24,6 +32,18 @@ func (f FS) Open(name string) (fs.File, error) { } 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 { @@ -68,22 +88,402 @@ func (f FS) Open(name string) (fs.File, error) { } } -func (f FS) Glob(pattern string) ([]string, error) { - return nil, nil +//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 } -func (f FS) ReadDir(name string) ([]DirEntry, error) { - return nil, nil +//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) { - return nil, nil + 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 } -func (f FS) Stat(name string) ([]byte, error) { - return nil, 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) { - return nil, nil + 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 67311ea..b7afee1 100644 --- a/internal/directory/directory.go +++ b/internal/directory/directory.go @@ -25,7 +25,7 @@ type EntryRaw struct { type Entry struct { Name string InodeOffset uint32 - InodeBlockOffset int16 + InodeBlockOffset uint16 Type uint16 } @@ -42,7 +42,7 @@ func NewEntry(rdr io.Reader) (*Entry, error) { return nil, err } return &Entry{ - InodeBlockOffset: raw.InodeOffset, + InodeBlockOffset: raw.Offset, Type: raw.Type, Name: string(tmp), }, nil 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 5b7be30..6dac7a0 100644 --- a/reader.go +++ b/reader.go @@ -8,6 +8,7 @@ import ( "time" "github.com/CalebQ42/squashfs/internal/compression" + "github.com/CalebQ42/squashfs/internal/inode" ) const ( @@ -29,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 @@ -41,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 } @@ -68,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 } @@ -78,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 } @@ -125,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 } @@ -146,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 } @@ -156,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 d0a8e07..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" ) @@ -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) { @@ -131,7 +130,7 @@ func BenchmarkDragRace(b *testing.B) { } else if err != nil { b.Fatal(err) } - // stat, _ := aiFil.Stat() + stat, _ := aiFil.Stat() ai := goappimage.NewAppImage(wd + "/testing/" + appImageName) os.RemoveAll(wd + "/testing/unsquashFirefox") os.RemoveAll(wd + "/testing/firefox") @@ -143,18 +142,18 @@ func BenchmarkDragRace(b *testing.B) { } unsquashTime := time.Since(start) start = time.Now() - // rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) - // if err != nil { - // b.Fatal(err) - // } - // errs := rdr.ExtractTo(wd + "/testing/firefox") - // if len(errs) > 0 { - // b.Fatal(errs) - // } + rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset)) + if err != nil { + b.Fatal(err) + } + 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 {