From 3f1b2a8d1edb9eef313f04f1c4cf721baa5fe1e7 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 29 Jan 2021 12:55:57 -0600 Subject: [PATCH 1/3] Restructure for 1.16 io/fs interface --- direntry_fileinfo.go | 126 ++++ file.go | 1036 +++++++++++++++---------------- file2.go | 157 +++++ fs.go | 89 +++ internal/directory/directory.go | 51 +- reader.go | 193 +++--- reader_test.go | 42 +- 7 files changed, 1004 insertions(+), 690 deletions(-) create mode 100644 direntry_fileinfo.go create mode 100644 file2.go create mode 100644 fs.go diff --git a/direntry_fileinfo.go b/direntry_fileinfo.go new file mode 100644 index 0000000..9e2100e --- /dev/null +++ b/direntry_fileinfo.go @@ -0,0 +1,126 @@ +package squashfs + +import ( + "io" + "io/fs" + "time" + + "github.com/CalebQ42/squashfs/internal/directory" + "github.com/CalebQ42/squashfs/internal/inode" +) + +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, + } +} + +func (d DirEntry) Name() string { + return d.en.Name +} + +func (d DirEntry) IsDir() bool { + return d.en.Type == inode.DirType +} + +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 + } +} + +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 +} + +type FileInfo struct { + i *inode.Inode + parent *FS + r *Reader + name string +} + +func (f FileInfo) Name() string { + return f.name +} + +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 +} + +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 +} + +func (f FileInfo) ModTime() time.Time { + return time.Unix(int64(f.i.ModifiedTime), 0) +} + +func (f FileInfo) IsDir() bool { + return f.i.Type == inode.DirType || f.i.Type == inode.ExtDirType +} + +func (f FileInfo) Sys() interface{} { + return &File{ + name: f.name, + i: f.i, + r: f.r, + parent: f.parent, + } +} diff --git a/file.go b/file.go index 6b391d2..1a82475 100644 --- a/file.go +++ b/file.go @@ -1,566 +1,516 @@ -package squashfs +// package squashfs -import ( - "errors" - "fmt" - "io" - "os" - "path" - "strings" - "time" +// import ( +// "errors" +// "fmt" +// "io" +// "os" +// "path" +// "strings" +// "time" - "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 +// //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") -) +// 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 -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 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. -} - -//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 -} - -//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 { +// //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 +// } + +// //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) +// } +// 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 +// } + +// //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 +// } + +// //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 // } -//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 -} +// //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 +// // } -//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 -} - -//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 -} - -//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 -} - -//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 // } -//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 +// } -//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 +// } -//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 "" +// } +// } -//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 "" - } -} +// //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()) +// } -//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 +// } +// } +// } -//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 +// } -//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) +// } -//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) +// } -//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 +// } -//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.Directory, 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 - } - dir, err := directory.NewDirectory(br, size) - if err != nil { - return dir, 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 -} +// //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) +// } diff --git a/file2.go b/file2.go new file mode 100644 index 0000000..7f8bec2 --- /dev/null +++ b/file2.go @@ -0,0 +1,157 @@ +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 new file mode 100644 index 0000000..88a2a41 --- /dev/null +++ b/fs.go @@ -0,0 +1,89 @@ +package squashfs + +import ( + "io/fs" + "path" + "strings" + + "github.com/CalebQ42/squashfs/internal/directory" +) + +type FS struct { + r *Reader + parent *FS + entries []*directory.Entry +} + +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, "/") + 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, + } +} + +func (f FS) Glob(pattern string) ([]string, error) { + return nil, nil +} + +func (f FS) ReadDir(name string) ([]DirEntry, error) { + return nil, nil +} + +func (f FS) ReadFile(name string) ([]byte, error) { + return nil, nil +} + +func (f FS) Stat(name string) ([]byte, error) { + return nil, nil +} + +func (f FS) Sub(dir string) (fs.FS, error) { + return nil, nil +} diff --git a/internal/directory/directory.go b/internal/directory/directory.go index 44da60d..67311ea 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 int16 + 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.InodeOffset, + 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/reader.go b/reader.go index 81d41c9..5b7be30 100644 --- a/reader.go +++ b/reader.go @@ -8,7 +8,6 @@ import ( "time" "github.com/CalebQ42/squashfs/internal/compression" - "github.com/CalebQ42/squashfs/internal/inode" ) const ( @@ -159,117 +158,117 @@ func (r *Reader) ModTime() time.Time { } //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) -} +// 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 - } + // 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 + // 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() -} +// 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 -} +// 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 -} +// 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) -} +// 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..d0a8e07 100644 --- a/reader_test.go +++ b/reader_test.go @@ -34,18 +34,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") } @@ -131,7 +131,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,14 +143,14 @@ 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) + // } + // errs := rdr.ExtractTo(wd + "/testing/firefox") + // if len(errs) > 0 { + // b.Fatal(errs) + // } libTime := time.Since(start) b.Log("Unsqushfs:", unsquashTime.Round(time.Millisecond)) b.Log("Library:", libTime.Round(time.Millisecond)) From d89153c3e231488d96b9548e041a29de5588c800 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sat, 30 Jan 2021 06:30:00 -0600 Subject: [PATCH 2/3] Finished io/FS interface --- direntry_fileinfo.go | 21 +- file.go | 847 +++++++++++++------------------- file2.go | 157 ------ fs.go | 416 +++++++++++++++- internal/directory/directory.go | 4 +- internal/inode/inodetypes.go | 2 +- internal/inode/process.go | 46 +- reader.go | 157 ++---- reader_test.go | 29 +- 9 files changed, 849 insertions(+), 830 deletions(-) delete mode 100644 file2.go 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 { From 07962426b2128202b111299b7ee7304c01c6426f Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Wed, 3 Feb 2021 14:03:21 -0600 Subject: [PATCH 3/3] Minor work on the Writer --- writer.go | 26 ++++++++++++++++++++------ writer_write.go | 22 +++++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/writer.go b/writer.go index 6521210..ec70b0c 100644 --- a/writer.go +++ b/writer.go @@ -19,6 +19,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). @@ -44,12 +45,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, @@ -71,7 +75,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) } @@ -95,8 +99,7 @@ func (w *Writer) AddFileTo(filepath string, file *os.File) error { return errors.New("File already exists at " + filepath) } var holder fileHolder - holder.path, holder.name = path.Split(filepath) - holder.reader = file + holder.path, holder.name = path.Dir(filepath), path.Base(filepath) stat, err := file.Stat() if err != nil { return err @@ -118,6 +121,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 @@ -148,9 +152,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 } @@ -167,8 +177,12 @@ func (w *Writer) AddReaderTo(filepath string, reader io.Reader) error { return errors.New("File already exists at " + filepath) } var holder fileHolder - holder.path, holder.name = path.Split(filepath) + holder.path, holder.name = path.Dir(filepath), path.Base(filepath) 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 0ee8bab..9cbe922 100644 --- a/writer_write.go +++ b/writer_write.go @@ -4,25 +4,41 @@ import ( "errors" "io" "math" + "time" ) +func (w Writer) countInodes() (out uint32) { + out++ // for the root indode + for _, fold := range w.folders { + out += uint32(len(w.structure[fold])) + } + return +} + //WriteTo attempts to write the archive to the given io.Writer. -func (w *Writer) WriteTo(write io.Writer) (int64, error) { +func (w *Writer) WriteTo(write io.WriteSeeker) (int64, error) { if w.BlockSize > 1048576 { w.BlockSize = 1048576 } else if w.BlockSize < 4096 { w.BlockSize = 4096 } + w.Flags.Duplicates = false + w.Flags.Exportable = false + w.Flags.NoXattr = true //TODO: set forced Flag values - _ = superblock{ + //TODO: make sure we aren't missing folders + super := superblock{ Magic: magic, + 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") }