Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
7.3 KiB
306 lines
7.3 KiB
package rardecode |
|
|
|
import ( |
|
"bufio" |
|
"bytes" |
|
"errors" |
|
"fmt" |
|
"io" |
|
"os" |
|
"path/filepath" |
|
"regexp" |
|
"strconv" |
|
"strings" |
|
) |
|
|
|
const ( |
|
maxSfxSize = 0x100000 // maximum number of bytes to read when searching for RAR signature |
|
sigPrefix = "Rar!\x1A\x07" |
|
|
|
fileFmt15 = iota + 1 // Version 1.5 archive file format |
|
fileFmt50 // Version 5.0 archive file format |
|
) |
|
|
|
var ( |
|
errNoSig = errors.New("rardecode: RAR signature not found") |
|
errVerMismatch = errors.New("rardecode: volume version mistmatch") |
|
errCorruptHeader = errors.New("rardecode: corrupt block header") |
|
errCorruptFileHeader = errors.New("rardecode: corrupt file header") |
|
errBadHeaderCrc = errors.New("rardecode: bad header crc") |
|
errUnknownArc = errors.New("rardecode: unknown archive version") |
|
errUnknownDecoder = errors.New("rardecode: unknown decoder version") |
|
errUnsupportedDecoder = errors.New("rardecode: unsupported decoder version") |
|
errArchiveContinues = errors.New("rardecode: archive continues in next volume") |
|
errArchiveEnd = errors.New("rardecode: archive end reached") |
|
errDecoderOutOfData = errors.New("rardecode: decoder expected more data than is in packed file") |
|
|
|
reDigits = regexp.MustCompile(`\d+`) |
|
) |
|
|
|
type readBuf []byte |
|
|
|
func (b *readBuf) byte() byte { |
|
v := (*b)[0] |
|
*b = (*b)[1:] |
|
return v |
|
} |
|
|
|
func (b *readBuf) uint16() uint16 { |
|
v := uint16((*b)[0]) | uint16((*b)[1])<<8 |
|
*b = (*b)[2:] |
|
return v |
|
} |
|
|
|
func (b *readBuf) uint32() uint32 { |
|
v := uint32((*b)[0]) | uint32((*b)[1])<<8 | uint32((*b)[2])<<16 | uint32((*b)[3])<<24 |
|
*b = (*b)[4:] |
|
return v |
|
} |
|
|
|
func (b *readBuf) bytes(n int) []byte { |
|
v := (*b)[:n] |
|
*b = (*b)[n:] |
|
return v |
|
} |
|
|
|
func (b *readBuf) uvarint() uint64 { |
|
var x uint64 |
|
var s uint |
|
for i, n := range *b { |
|
if n < 0x80 { |
|
*b = (*b)[i+1:] |
|
return x | uint64(n)<<s |
|
} |
|
x |= uint64(n&0x7f) << s |
|
s += 7 |
|
|
|
} |
|
// if we run out of bytes, just return 0 |
|
*b = (*b)[len(*b):] |
|
return 0 |
|
} |
|
|
|
// readFull wraps io.ReadFull to return io.ErrUnexpectedEOF instead |
|
// of io.EOF when 0 bytes are read. |
|
func readFull(r io.Reader, buf []byte) error { |
|
_, err := io.ReadFull(r, buf) |
|
if err == io.EOF { |
|
return io.ErrUnexpectedEOF |
|
} |
|
return err |
|
} |
|
|
|
// findSig searches for the RAR signature and version at the beginning of a file. |
|
// It searches no more than maxSfxSize bytes. |
|
func findSig(br *bufio.Reader) (int, error) { |
|
for n := 0; n <= maxSfxSize; { |
|
b, err := br.ReadSlice(sigPrefix[0]) |
|
n += len(b) |
|
if err == bufio.ErrBufferFull { |
|
continue |
|
} else if err != nil { |
|
if err == io.EOF { |
|
err = errNoSig |
|
} |
|
return 0, err |
|
} |
|
|
|
b, err = br.Peek(len(sigPrefix[1:]) + 2) |
|
if err != nil { |
|
if err == io.EOF { |
|
err = errNoSig |
|
} |
|
return 0, err |
|
} |
|
if !bytes.HasPrefix(b, []byte(sigPrefix[1:])) { |
|
continue |
|
} |
|
b = b[len(sigPrefix)-1:] |
|
|
|
var ver int |
|
switch { |
|
case b[0] == 0: |
|
ver = fileFmt15 |
|
case b[0] == 1 && b[1] == 0: |
|
ver = fileFmt50 |
|
default: |
|
continue |
|
} |
|
_, _ = br.ReadSlice('\x00') |
|
|
|
return ver, nil |
|
} |
|
return 0, errNoSig |
|
} |
|
|
|
// volume extends a fileBlockReader to be used across multiple |
|
// files in a multi-volume archive |
|
type volume struct { |
|
fileBlockReader |
|
f *os.File // current file handle |
|
br *bufio.Reader // buffered reader for current volume file |
|
dir string // volume directory |
|
file string // current volume file |
|
num int // volume number |
|
old bool // uses old naming scheme |
|
} |
|
|
|
// nextVolName updates name to the next filename in the archive. |
|
func (v *volume) nextVolName() { |
|
if v.num == 0 { |
|
// check file extensions |
|
i := strings.LastIndex(v.file, ".") |
|
if i < 0 { |
|
// no file extension, add one |
|
i = len(v.file) |
|
v.file += ".rar" |
|
} else { |
|
ext := strings.ToLower(v.file[i+1:]) |
|
// replace with .rar for empty extensions & self extracting archives |
|
if ext == "" || ext == "exe" || ext == "sfx" { |
|
v.file = v.file[:i+1] + "rar" |
|
} |
|
} |
|
if a, ok := v.fileBlockReader.(*archive15); ok { |
|
v.old = a.old |
|
} |
|
// new naming scheme must have volume number in filename |
|
if !v.old && reDigits.FindStringIndex(v.file) == nil { |
|
v.old = true |
|
} |
|
// For old style naming if 2nd and 3rd character of file extension is not a digit replace |
|
// with "00" and ignore any trailing characters. |
|
if v.old && (len(v.file) < i+4 || v.file[i+2] < '0' || v.file[i+2] > '9' || v.file[i+3] < '0' || v.file[i+3] > '9') { |
|
v.file = v.file[:i+2] + "00" |
|
return |
|
} |
|
} |
|
// new style volume naming |
|
if !v.old { |
|
// find all numbers in volume name |
|
m := reDigits.FindAllStringIndex(v.file, -1) |
|
if l := len(m); l > 1 { |
|
// More than 1 match so assume name.part###of###.rar style. |
|
// Take the last 2 matches where the first is the volume number. |
|
m = m[l-2 : l] |
|
if strings.Contains(v.file[m[0][1]:m[1][0]], ".") || !strings.Contains(v.file[:m[0][0]], ".") { |
|
// Didn't match above style as volume had '.' between the two numbers or didnt have a '.' |
|
// before the first match. Use the second number as volume number. |
|
m = m[1:] |
|
} |
|
} |
|
// extract and increment volume number |
|
lo, hi := m[0][0], m[0][1] |
|
n, err := strconv.Atoi(v.file[lo:hi]) |
|
if err != nil { |
|
n = 0 |
|
} else { |
|
n++ |
|
} |
|
// volume number must use at least the same number of characters as previous volume |
|
vol := fmt.Sprintf("%0"+fmt.Sprint(hi-lo)+"d", n) |
|
v.file = v.file[:lo] + vol + v.file[hi:] |
|
return |
|
} |
|
// old style volume naming |
|
i := strings.LastIndex(v.file, ".") |
|
// get file extension |
|
b := []byte(v.file[i+1:]) |
|
// start incrementing volume number digits from rightmost |
|
for j := 2; j >= 0; j-- { |
|
if b[j] != '9' { |
|
b[j]++ |
|
break |
|
} |
|
// digit overflow |
|
if j == 0 { |
|
// last character before '.' |
|
b[j] = 'A' |
|
} else { |
|
// set to '0' and loop to next character |
|
b[j] = '0' |
|
} |
|
} |
|
v.file = v.file[:i+1] + string(b) |
|
} |
|
|
|
func (v *volume) next() (*fileBlockHeader, error) { |
|
for { |
|
var atEOF bool |
|
|
|
h, err := v.fileBlockReader.next() |
|
switch err { |
|
case errArchiveContinues: |
|
case io.EOF: |
|
// Read all of volume without finding an end block. The only way |
|
// to tell if the archive continues is to try to open the next volume. |
|
atEOF = true |
|
default: |
|
return h, err |
|
} |
|
|
|
v.f.Close() |
|
v.nextVolName() |
|
v.f, err = os.Open(v.dir + v.file) // Open next volume file |
|
if err != nil { |
|
if atEOF && os.IsNotExist(err) { |
|
// volume not found so assume that the archive has ended |
|
return nil, io.EOF |
|
} |
|
return nil, err |
|
} |
|
v.num++ |
|
v.br.Reset(v.f) |
|
ver, err := findSig(v.br) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if v.version() != ver { |
|
return nil, errVerMismatch |
|
} |
|
v.reset() // reset encryption |
|
} |
|
} |
|
|
|
func (v *volume) Close() error { |
|
// may be nil if os.Open fails in next() |
|
if v.f == nil { |
|
return nil |
|
} |
|
return v.f.Close() |
|
} |
|
|
|
func openVolume(name, password string) (*volume, error) { |
|
var err error |
|
v := new(volume) |
|
v.dir, v.file = filepath.Split(name) |
|
v.f, err = os.Open(name) |
|
if err != nil { |
|
return nil, err |
|
} |
|
v.br = bufio.NewReader(v.f) |
|
v.fileBlockReader, err = newFileBlockReader(v.br, password) |
|
if err != nil { |
|
v.f.Close() |
|
return nil, err |
|
} |
|
return v, nil |
|
} |
|
|
|
func newFileBlockReader(br *bufio.Reader, pass string) (fileBlockReader, error) { |
|
runes := []rune(pass) |
|
if len(runes) > maxPassword { |
|
pass = string(runes[:maxPassword]) |
|
} |
|
ver, err := findSig(br) |
|
if err != nil { |
|
return nil, err |
|
} |
|
switch ver { |
|
case fileFmt15: |
|
return newArchive15(br, pass), nil |
|
case fileFmt50: |
|
return newArchive50(br, pass), nil |
|
} |
|
return nil, errUnknownArc |
|
}
|
|
|