go-unarr/unarr.go

287 lines
5.6 KiB
Go

// Package unarr is a decompression library for RAR, TAR, ZIP and 7z archives.
package unarr
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
"time"
"unsafe"
"github.com/gen2brain/go-unarr/unarrc"
)
var (
ErrOpenFile = errors.New("unarr: open file failed")
ErrOpenMemory = errors.New("unarr: open memory failed")
ErrOpenArchive = errors.New("unarr: no valid RAR, ZIP, 7Z or TAR archive")
ErrEntry = errors.New("unarr: failed to parse entry")
ErrEntryAt = errors.New("unarr: failed to parse entry at")
ErrEntryFor = errors.New("unarr: failed to parse entry for")
ErrSeek = errors.New("unarr: seek failed")
ErrRead = errors.New("unarr: read failure")
)
// Archive represents unarr archive
type Archive struct {
// C stream struct
stream *unarrc.Stream
// C archive struct
archive *unarrc.Archive
}
// NewArchive returns new unarr Archive
func NewArchive(path string) (a *Archive, err error) {
a = new(Archive)
a.stream = unarrc.OpenFile(path)
if a.stream == nil {
err = ErrOpenFile
return
}
err = a.open()
return
}
// NewArchiveFromMemory returns new unarr Archive from byte slice
func NewArchiveFromMemory(b []byte) (a *Archive, err error) {
a = new(Archive)
a.stream = unarrc.OpenMemory(unsafe.Pointer(&b[0]), uint(len(b)))
if a.stream == nil {
err = ErrOpenMemory
return
}
err = a.open()
return
}
// NewArchiveFromReader returns new unarr Archive from io.Reader
func NewArchiveFromReader(r io.Reader) (a *Archive, err error) {
b, e := io.ReadAll(r)
if e != nil {
err = e
return
}
a, err = NewArchiveFromMemory(b)
return
}
// open opens archive
func (a *Archive) open() (err error) {
a.archive = unarrc.OpenRarArchive(a.stream)
if a.archive == nil {
a.archive = unarrc.OpenZipArchive(a.stream, false)
}
if a.archive == nil {
a.archive = unarrc.Open7zArchive(a.stream)
}
if a.archive == nil {
a.archive = unarrc.OpenTarArchive(a.stream)
}
if a.archive == nil {
unarrc.Close(a.stream)
err = ErrOpenArchive
}
return
}
// Entry reads the next archive entry.
//
// io.EOF is returned when there is no more to be read from the archive.
func (a *Archive) Entry() error {
r := unarrc.ParseEntry(a.archive)
if !r {
e := unarrc.AtEof(a.archive)
if e {
return io.EOF
}
return ErrEntry
}
return nil
}
// EntryAt reads the archive entry at the given offset
func (a *Archive) EntryAt(off int64) error {
r := unarrc.ParseEntryAt(a.archive, off)
if !r {
return ErrEntryAt
}
return nil
}
// EntryFor reads the (first) archive entry associated with the given name
func (a *Archive) EntryFor(name string) error {
r := unarrc.ParseEntryFor(a.archive, name)
if !r {
return ErrEntryFor
}
return nil
}
// Read tries to read 'b' bytes into buffer, advancing the read offset pointer.
//
// Returns the actual number of bytes read.
func (a *Archive) Read(b []byte) (n int, err error) {
r := unarrc.EntryUncompress(a.archive, unsafe.Pointer(&b[0]), uint(len(b)))
n = len(b)
if !r || n == 0 {
err = io.EOF
}
return
}
// Seek moves the read offset pointer interpreted according to whence.
//
// Returns the new offset.
func (a *Archive) Seek(offset int64, whence int) (int64, error) {
r := unarrc.Seek(a.stream, offset, whence)
if !r {
return 0, ErrSeek
}
return int64(unarrc.Tell(a.stream)), nil
}
// Close closes the underlying unarr archive
func (a *Archive) Close() (err error) {
unarrc.CloseArchive(a.archive)
unarrc.Close(a.stream)
return
}
// Size returns the total size of uncompressed data of the current entry
func (a *Archive) Size() int {
return int(unarrc.EntryGetSize(a.archive))
}
// Offset returns the stream offset of the current entry, for use with EntryAt
func (a *Archive) Offset() int64 {
return int64(unarrc.EntryGetOffset(a.archive))
}
// Name returns the name of the current entry as UTF-8 string
func (a *Archive) Name() string {
return toValidName(unarrc.EntryGetName(a.archive))
}
// RawName returns the name of the current entry as raw string
func (a *Archive) RawName() string {
return unarrc.EntryGetRawName(a.archive)
}
// ModTime returns the stored modification time of the current entry
func (a *Archive) ModTime() time.Time {
filetime := int64(unarrc.EntryGetFiletime(a.archive))
return time.Unix((filetime/10000000)-11644473600, 0)
}
// ReadAll reads current entry and returns data
func (a *Archive) ReadAll() ([]byte, error) {
var err error
var n int
size := a.Size()
b := make([]byte, size)
for size > 0 {
n, err = a.Read(b)
if err != nil {
if err != io.EOF {
return nil, err
}
}
size -= n
}
if size > 0 {
return nil, ErrRead
}
return b, nil
}
// Extract extracts archive to destination path
func (a *Archive) Extract(path string) (contents []string, err error) {
for {
e := a.Entry()
if e != nil {
if e == io.EOF {
break
}
err = e
return
}
name := a.Name()
contents = append(contents, name)
data, e := a.ReadAll()
if e != nil {
err = e
return
}
dirname := filepath.Join(path, filepath.Dir(name))
_ = os.MkdirAll(dirname, 0755)
e = os.WriteFile(filepath.Join(dirname, filepath.Base(name)), data, 0644)
if e != nil {
err = e
return
}
}
return
}
// List lists the contents of archive
func (a *Archive) List() (contents []string, err error) {
for {
e := a.Entry()
if e != nil {
if e == io.EOF {
break
}
err = e
return
}
name := a.Name()
contents = append(contents, name)
}
return
}
func toValidName(name string) string {
p := filepath.Clean(name)
if strings.HasPrefix(p, "/") {
p = p[len("/"):]
}
for strings.HasPrefix(p, "../") {
p = p[len("../"):]
}
return p
}