package archiver import ( "archive/tar" "archive/zip" "bytes" "compress/bzip2" "compress/gzip" "github.com/ulikunitz/xz" "io" "os" "path/filepath" ) var magicByteLookup = map[Type][][]byte{ Tar: { {0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30}, {0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00}, }, TarGzip: { {0x1F, 0x8B}, }, TarXz: { {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, }, TarBzip: { {0x42, 0x5A, 0x68}, }, Zip: { {0x50, 0x4B, 0x03, 0x04}, {0x50, 0x4B, 0x05, 0x06}, {0x50, 0x4B, 0x07, 0x08}, }, } // OpenFS opens an archive file from a given fs.FS and returns an Archive struct. // It takes fs.FS, and a path to the archive file as its parameters. // The function returns an Archive struct and an error, if any. func OpenFS(filesystem Filesystem, path string) (*Archive, error) { return openArchive(filesystem, path) } // Open opens an archive file and returns an Archive struct. // It takes a path to the archive file as its parameter. // The function returns an Archive struct and an error, if any. func Open(path string) (*Archive, error) { archiveFolderPath, err := filepath.Abs(filepath.Dir(path)) if err != nil { return nil, err } return openArchive(WrapPath(archiveFolderPath), path) } // Extract opens an archive file and extracts the contents. // It takes a path, and ExtractOptions for its parameters. // The function returns an error, if any. func Extract(path string, options ExtractOptions) error { archiveFile, err := Open(path) if err != nil { return err } defer archiveFile.Close() if err := archiveFile.Extract(options); err != nil { return err } if err := archiveFile.Close(); err != nil { return err } return nil } // ExtractFile opens an archive file and extracts a specific file within the archive. // It takes a path, a file path, and ExtractFileOptions for its parameters. // The function returns an error, if any. func ExtractFile(path string, file string, options ExtractFileOptions) error { archive, err := Open(path) if err != nil { return err } defer archive.Close() archiveFile, err := archive.GetFile(file) if err != nil { return err } if err := archiveFile.Extract(options); err != nil { return err } if err := archive.Close(); err != nil { return err } return nil } func openArchive(filesystem Filesystem, path string) (*Archive, error) { archive := new(Archive) archive.files = make(map[string]*File) var file *bytes.Reader if filesystem.billyFS != nil { billyFS := filesystem.billyFS archiveFile, err := billyFS.Open(path) if err != nil { return nil, err } fileData, err := io.ReadAll(archiveFile) if err != nil { return nil, err } file = bytes.NewReader(fileData) } else if filesystem.fs != nil { genericFS := filesystem.fs archiveFile, err := genericFS.Open(path) if err != nil { return nil, err } fileData, err := io.ReadAll(archiveFile) if err != nil { return nil, err } file = bytes.NewReader(fileData) } else if len(filesystem.path) != 0 { archiveFile, err := os.Open(path) if err != nil { return nil, err } fileData, err := io.ReadAll(archiveFile) if err != nil { return nil, err } file = bytes.NewReader(fileData) } if file == nil { return nil, ErrArchiveFileNotFound } magicBytes := make([]byte, 512) if _, err := file.Read(magicBytes); err != nil { return nil, err } if _, err := file.Seek(0, io.SeekStart); err != nil { return nil, err } var foundArchiveType Type for archiveT, fileBytes := range magicByteLookup { if len(foundArchiveType) != 0 { break } for _, b := range fileBytes { var magicByteWithOffset []byte if archiveT == Tar { magicByteWithOffset = magicBytes[257 : 257+len(b)] } else { magicByteWithOffset = magicBytes[0:len(b)] } if bytes.Equal(b, magicByteWithOffset) { foundArchiveType = archiveT break } switch foundArchiveType { case TarGzip: gzipReader, err := gzip.NewReader(bytes.NewReader(b)) if err != nil { return nil, err } fileMagicBytes := make([]byte, 512) if _, err := gzipReader.Read(fileMagicBytes); err != nil { return nil, err } if !bytes.Equal(b, fileMagicBytes[257:257+len(b)]) { return nil, ErrArchiveTypeNotSupported } if err := gzipReader.Close(); err != nil { return nil, err } case TarXz: xzReader, err := xz.NewReader(bytes.NewReader(b)) if err != nil { return nil, err } fileMagicBytes := make([]byte, 512) if _, err := xzReader.Read(fileMagicBytes); err != nil { return nil, err } if !bytes.Equal(b, fileMagicBytes[257:257+len(b)]) { return nil, ErrArchiveTypeNotSupported } case TarBzip: bzReader := bzip2.NewReader(bytes.NewReader(b)) fileMagicBytes := make([]byte, 512) if _, err := bzReader.Read(fileMagicBytes); err != nil { return nil, err } if !bytes.Equal(b, fileMagicBytes[257:257+len(b)]) { return nil, ErrArchiveTypeNotSupported } } } } if len(foundArchiveType) == 0 { return nil, ErrArchiveTypeNotSupported } switch foundArchiveType { case TarGzip: gzipReader, err := gzip.NewReader(file) if err != nil { return nil, err } tarReader := tar.NewReader(gzipReader) for { header, err := tarReader.Next() if err != nil && err != io.EOF { return nil, err } else if err == io.EOF { if _, err := file.Seek(0, io.SeekStart); err != nil { return nil, err } break } _, fileName := filepath.Split(header.Name) archive.files[header.Name] = &File{FileName: fileName, Path: header.Name, archive: archive} } if err := gzipReader.Reset(file); err != nil { return nil, err } archive.reader = gzipReader archive.tarReader = tar.NewReader(gzipReader) case TarBzip: bzipReader := bzip2.NewReader(file) tarReader := tar.NewReader(bzipReader) for { header, err := tarReader.Next() if err != nil && err != io.EOF { return nil, err } else if err == io.EOF { if _, err := file.Seek(0, io.SeekStart); err != nil { return nil, err } break } _, fileName := filepath.Split(header.Name) archive.files[header.Name] = &File{FileName: fileName, Path: header.Name, archive: archive} } bzipReader = bzip2.NewReader(file) archive.reader = bzipReader archive.tarReader = tar.NewReader(bzipReader) case TarXz: xzReader, err := xz.NewReader(file) if err != nil { return nil, err } tarReader := tar.NewReader(xzReader) for { header, err := tarReader.Next() if err != nil && err != io.EOF { return nil, err } else if err == io.EOF { if _, err := file.Seek(0, io.SeekStart); err != nil { return nil, err } break } _, fileName := filepath.Split(header.Name) archive.files[header.Name] = &File{FileName: fileName, Path: header.Name, archive: archive} } xzReader, err = xz.NewReader(file) if err != nil { return nil, err } archive.reader = xzReader archive.tarReader = tar.NewReader(xzReader) case Tar: tarReader := tar.NewReader(file) for { header, err := tarReader.Next() if err != nil && err != io.EOF { return nil, err } else if err == io.EOF { if _, err := file.Seek(0, io.SeekStart); err != nil { return nil, err } break } _, fileName := filepath.Split(header.Name) archive.files[header.Name] = &File{FileName: fileName, Path: header.Name, archive: archive} } archive.reader = file archive.tarReader = tar.NewReader(file) case Zip: reader, err := zip.NewReader(file, file.Size()) if err != nil { return nil, err } archive.reader = reader for _, file := range reader.File { _, fileName := filepath.Split(file.Name) archive.files[file.Name] = &File{FileName: fileName, Path: file.Name, archive: archive} } default: return nil, ErrArchiveTypeNotSupported } archive.Type = foundArchiveType archive.Path = path archive.archiveFile = file return archive, nil }