package squashfs import ( "errors" "io" "io/fs" "log" "os" "os/exec" "path/filepath" "runtime" "strconv" "sync" squashfslow "github.com/CalebQ42/squashfs/low" "github.com/CalebQ42/squashfs/low/data" "github.com/CalebQ42/squashfs/low/inode" ) // File represents a file inside a squashfs archive. type File struct { full data.FullReader rdr data.Reader rdrInit bool parent FS r *Reader Low squashfslow.FileBase dirsRead int } // Creates a new *File from the given *squashfs.Base func (r *Reader) FileFromBase(b squashfslow.FileBase, parent FS) File { return File{ Low: b, parent: parent, r: r, } } func (f File) FS() (FS, error) { if !f.IsDir() { return FS{}, errors.New("not a directory") } d, err := f.Low.ToDir(f.r.Low) if err != nil { return FS{}, err } return FS{LowDir: d, parent: &f.parent, r: f.r}, nil } // Closes the underlying readers. // Further calls to Read and WriteTo will re-create the readers. // Never returns an error. func (f *File) Close() error { f.rdr.Close() f.full.Close() return nil } // Returns the file the symlink points to. // If the file isn't a symlink, or points to a file outside the archive, returns nil. func (f File) GetSymlinkFile() fs.File { if !f.IsSymlink() { return nil } if filepath.IsAbs(f.SymlinkPath()) { return nil } fil, err := f.parent.Open(f.SymlinkPath()) if err != nil { return nil } return fil } // Returns whether the file is a directory. func (f File) IsDir() bool { return f.Low.IsDir() } // Returns whether the file is a regular file. func (f File) IsRegular() bool { return f.Low.IsRegular() } // Returns whether the file is a symlink. func (f File) IsSymlink() bool { return f.Low.Inode.Type == inode.Sym || f.Low.Inode.Type == inode.ESym } func (f File) Mode() fs.FileMode { return f.Low.Inode.Mode() } // Read reads the data from the file. Only works if file is a normal file. func (f *File) Read(b []byte) (int, error) { if !f.IsRegular() { return 0, errors.New("file is not a regular file") } if !f.rdrInit { err := f.initializeReaders() if err != nil { return 0, err } } return f.rdr.Read(b) } // 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") } d, err := f.Low.ToDir(f.r.Low) if err != nil { return nil, err } start, end := 0, len(d.Entries) if n > 0 { start, end = f.dirsRead, f.dirsRead+n if end > len(d.Entries) { end = len(d.Entries) err = io.EOF } } var out []fs.DirEntry var fi FileInfo for _, e := range d.Entries[start:end] { fi, err = f.r.newFileInfo(e) if err != nil { f.dirsRead += len(out) return out, err } out = append(out, fs.FileInfoToDirEntry(fi)) } f.dirsRead += len(out) return out, err } // Returns the file's fs.FileInfo func (f File) Stat() (fs.FileInfo, error) { uid, err := f.Low.Uid(&f.r.Low) if err != nil { return nil, err } gid, err := f.Low.Gid(&f.r.Low) if err != nil { return nil, err } return newFileInfo(f.Low.Name, uid, gid, &f.Low.Inode), nil } // 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.Low.Inode.Type { case inode.Sym: return string(f.Low.Inode.Data.(inode.Symlink).Target) case inode.ESym: return string(f.Low.Inode.Data.(inode.ESymlink).Target) } return "" } // Writes all data from the file to the given writer in a multi-threaded manner. // The underlying reader is separate func (f *File) WriteTo(w io.Writer) (int64, error) { if !f.IsRegular() { return 0, errors.New("file is not a regular file") } if !f.rdrInit { err := f.initializeReaders() if err != nil { return 0, err } } return f.full.WriteTo(w) } func (f *File) initializeReaders() error { var err error f.rdr, f.full, err = f.Low.GetRegFileReaders(f.r.Low) if err == nil { f.rdrInit = true } else { f.rdr.Close() f.full.Close() } return err } func (f File) deviceDevices() (maj uint32, min uint32) { var dev uint32 switch f.Low.Inode.Type { case inode.Char, inode.Block: dev = f.Low.Inode.Data.(inode.Device).Dev case inode.EChar, inode.EBlock: dev = f.Low.Inode.Data.(inode.EDevice).Dev } return dev >> 8, dev & 0x000FF } func (f File) path() string { if f.parent.LowDir.Name == "" { return f.Low.Name } return filepath.Join(f.parent.path(), f.Low.Name) } // Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Uses default extraction options. func (f File) Extract(folder string) error { return f.ExtractWithOptions(folder, DefaultOptions()) } // Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder. // Allows setting various extraction options via ExtractionOptions. func (f File) ExtractWithOptions(path string, op *ExtractionOptions) error { if op.dispatcher == nil { op.fullRdrPool = sync.Pool{ New: func() any { return &data.BlockResults{} }, } op.dispatcher = make(chan struct{}, op.ExtractionRoutines) for range op.ExtractionRoutines { op.dispatcher <- struct{}{} } if op.LogOutput != nil { log.SetOutput(op.LogOutput) } err := os.MkdirAll(path, 0777) if err != nil { if op.Verbose { log.Println("Failed to create initial directory", path) } return err } } switch f.Low.Inode.Type { case inode.Dir, inode.EDir: <-op.dispatcher d, err := f.Low.ToDir(f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to create squashfs.Directory for", path) } op.dispatcher <- struct{}{} return errors.Join(errors.New("failed to create squashfs.Directory: "+path), err) } errChan := make(chan error, len(d.Entries)) for i := range d.Entries { b, err := f.r.Low.BaseFromEntry(d.Entries[i]) if err != nil { if op.Verbose { log.Println("Failed to get squashfs.Base from entry for", path) } return errors.Join(errors.New("failed to get base from entry: "+path), err) } go func(b squashfslow.FileBase, path string) { if b.IsDir() { <-op.dispatcher extDir := filepath.Join(path, b.Name) err = os.Mkdir(extDir, 0777) if err != nil { if op.Verbose { log.Println("Failed to create directory", path) } op.dispatcher <- struct{}{} errChan <- errors.Join(errors.New("failed to create directory: "+path), err) return } fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)) op.dispatcher <- struct{}{} err = fil.ExtractWithOptions(extDir, op) if err != nil { if op.Verbose { log.Println("Failed to extract directory", path) } errChan <- errors.Join(errors.New("failed to extract directory: "+path), err) return } errChan <- nil } else { fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)) err = fil.ExtractWithOptions(path, op) fil.Close() errChan <- err } }(b, path) } op.dispatcher <- struct{}{} var errCache []error for range d.Entries { err := <-errChan if err != nil { errCache = append(errCache, err) } } if len(errCache) > 0 { return errors.Join(errors.New("failed to extract folder: "+path), errors.Join(errCache...)) } case inode.Fil, inode.EFil: <-op.dispatcher path = filepath.Join(path, f.Low.Name) outFil, err := os.Create(path) if err != nil { if op.Verbose { log.Println("Failed to create file", path) } op.dispatcher <- struct{}{} return errors.Join(errors.New("failed to create file: "+path), err) } defer outFil.Close() full, err := f.Low.GetFullReader(&f.r.Low) defer full.Close() if err != nil { if op.Verbose { log.Println("Failed to create full reader for", path) } op.dispatcher <- struct{}{} return errors.Join(errors.New("failed to create full reader: "+path), err) } full.SetDispatcherPool(op.dispatcher, &op.fullRdrPool) op.dispatcher <- struct{}{} _, err = full.WriteTo(outFil) if err != nil { if op.Verbose { log.Println("Failed to write file", path) } return errors.Join(errors.New("failed to write file: "+path), err) } case inode.Sym, inode.ESym: <-op.dispatcher defer func() { op.dispatcher <- struct{}{} }() symPath := f.SymlinkPath() if op.DereferenceSymlink { filTmp := f.GetSymlinkFile() if filTmp == nil { if op.Verbose { log.Println("Failed to get symlink's file:", f.path()) } return errors.New("failed to get symlink's file") } fil := filTmp.(*File) fil.Low.Name = f.Low.Name err := fil.ExtractWithOptions(path, op) if err != nil { if op.Verbose { log.Println("Failed to extract symlink's file:", filepath.Join(path, f.Low.Name)) } return errors.Join(errors.New("failed to extract symlink's file: "+path), err) } } else { if op.UnbreakSymlink { filTmp := f.GetSymlinkFile() if filTmp == nil { if op.Verbose { log.Println("Failed to get symlink's file:", f.path()) } return errors.New("failed to get symlink's file") } extractLoc := filepath.Join(path, filepath.Dir(symPath)) fil := filTmp.(*File) err := fil.ExtractWithOptions(extractLoc, op) if err != nil { if op.Verbose { log.Println("Error while extracting", fil.path(), "to make sure symlink at", f.path(), "is unbroken") } return errors.Join(errors.New("failed to extract symlink's file: "+extractLoc), err) } } path = filepath.Join(path, f.Low.Name) err := os.Symlink(f.SymlinkPath(), path) if err != nil { if op.Verbose { log.Println("Failed to create symlink:", path) } return errors.Join(errors.New("failed to create symlink: "+path), err) } } case inode.Char, inode.EChar, inode.Block, inode.EBlock, inode.Fifo, inode.EFifo: <-op.dispatcher defer func() { op.dispatcher <- struct{}{} }() if runtime.GOOS == "windows" { if op.Verbose { log.Println(f.path(), "ignored. A device link and can't be created on Windows.") } return nil } _, err := exec.LookPath("mknod") if err != nil { if op.Verbose { log.Println("mknot command not found, cannot create device link for", f.path()) } return errors.Join(errors.New("mknot command not found"), err) } path = filepath.Join(path, f.Low.Name) var typ string switch f.Low.Inode.Type { case inode.Char, inode.EChar: typ = "c" case inode.Block, inode.EBlock: typ = "b" default: //Fifo IPC if runtime.GOOS == "darwin" { if op.Verbose { log.Println(f.path(), "ignored. A Fifo file and can't be created on Darwin.") } return nil } typ = "p" } cmd := exec.Command("mknod", path, typ) if typ != "p" { maj, min := f.deviceDevices() cmd.Args = append(cmd.Args, strconv.Itoa(int(maj)), strconv.Itoa(int(min))) } if op.Verbose { cmd.Stdout = op.LogOutput cmd.Stderr = op.LogOutput } err = cmd.Run() if err != nil { if op.Verbose { log.Println("Error while running mknod for", path) } return errors.Join(errors.New("error while running mknod for "+path), err) } case inode.Sock, inode.ESock: if op.Verbose { log.Println(f.path(), "ignored since it's a socket file.") } return nil default: return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.Low.Inode.Type))) } if op.Verbose { log.Println(f.path(), "extracted to", path) } if op.IgnorePerm { return nil } uid, err := f.Low.Uid(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to get uid for", path) log.Println(err) } return nil } gid, err := f.Low.Gid(&f.r.Low) if err != nil { if op.Verbose { log.Println("Failed to get gid for", path) log.Println(err) } return nil } os.Chmod(path, f.Mode()) os.Chown(path, int(uid), int(gid)) return nil }