feat: vendor bodgit/sevenzip package to remove go4.org dependency

Vendored the sevenzip package to eliminate dependency chain:
- sevenzip -> go4.org -> 25+ Google/Cloud/telemetry packages

Changes:
- Added internal/sevenzip/ with full package source
- Inlined go4.org/readerutil into multireaderat.go
- Updated all internal imports to use bookhoard/internal/sevenzip
- Preserved .cb7 comic archive support

This reduces bloat by ~4.9 MB and removes unused telemetry
dependencies while maintaining all functionality.
This commit is contained in:
2026-02-22 16:29:21 -05:00
parent c6cf038c8e
commit 46ae45ee92
26 changed files with 4063 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2020, Matt Dainty
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+69
View File
@@ -0,0 +1,69 @@
package aes7z
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"sync"
lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
type cacheKey struct {
password string
cycles int
salt string // []byte isn't comparable
}
const cacheSize = 10
//nolint:gochecknoglobals
var once = sync.OnceValues(func() (*lru.Cache[cacheKey, []byte], error) {
return lru.New[cacheKey, []byte](cacheSize)
})
func calculateKey(password string, cycles int, salt []byte) ([]byte, error) {
cache, err := once()
if err != nil {
return nil, fmt.Errorf("aes7z: error creating cache: %w", err)
}
ck := cacheKey{
password: password,
cycles: cycles,
salt: hex.EncodeToString(salt),
}
if key, ok := cache.Get(ck); ok {
return key, nil
}
b := bytes.NewBuffer(salt)
// Convert password to UTF-16LE
utf16le := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
t := transform.NewWriter(b, utf16le.NewEncoder())
_, _ = t.Write([]byte(password))
key := make([]byte, sha256.Size)
if cycles == 0x3f {
copy(key, b.Bytes())
} else {
h := sha256.New()
for i := uint64(0); i < 1<<cycles; i++ {
// These will never error
_, _ = h.Write(b.Bytes())
_ = binary.Write(h, binary.LittleEndian, i)
}
copy(key, h.Sum(nil))
}
_ = cache.Add(ck, key)
return key, nil
}
+126
View File
@@ -0,0 +1,126 @@
// Package aes7z implements the 7-zip AES decryption.
package aes7z
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"io"
)
var (
errAlreadyClosed = errors.New("aes7z: already closed")
errNeedOneReader = errors.New("aes7z: need exactly one reader")
errInsufficientProperties = errors.New("aes7z: not enough properties")
errNoPasswordSet = errors.New("aes7z: no password set")
errUnsupportedMethod = errors.New("aes7z: unsupported compression method")
)
type readCloser struct {
rc io.ReadCloser
salt, iv []byte
cycles int
cbc cipher.BlockMode
buf bytes.Buffer
}
func (rc *readCloser) Close() error {
if rc.rc == nil {
return errAlreadyClosed
}
if err := rc.rc.Close(); err != nil {
return fmt.Errorf("aes7z: error closing: %w", err)
}
rc.rc = nil
return nil
}
func (rc *readCloser) Password(p string) error {
key, err := calculateKey(p, rc.cycles, rc.salt)
if err != nil {
return err
}
block, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("aes7z: error creating cipher: %w", err)
}
rc.cbc = cipher.NewCBCDecrypter(block, rc.iv)
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.rc == nil {
return 0, errAlreadyClosed
}
if rc.cbc == nil {
return 0, errNoPasswordSet
}
var block [aes.BlockSize]byte
for rc.buf.Len() < len(p) {
if _, err := io.ReadFull(rc.rc, block[:]); err != nil {
if errors.Is(err, io.EOF) {
break
}
return 0, fmt.Errorf("aes7z: error reading block: %w", err)
}
rc.cbc.CryptBlocks(block[:], block[:])
_, _ = rc.buf.Write(block[:])
}
n, err := rc.buf.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("aes7z: error reading: %w", err)
}
return n, err
}
// NewReader returns a new AES-256-CBC & SHA-256 io.ReadCloser. The Password
// method must be called before attempting to call Read so that the block
// cipher is correctly initialised.
func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
// Need at least two bytes initially
if len(p) < 2 {
return nil, errInsufficientProperties
}
if p[0]&0xc0 == 0 {
return nil, errUnsupportedMethod
}
rc := new(readCloser)
salt := p[0]>>7&1 + p[1]>>4
iv := p[0]>>6&1 + p[1]&0x0f
if len(p) != int(2+salt+iv) {
return nil, errInsufficientProperties
}
rc.salt = p[2 : 2+salt]
rc.iv = make([]byte, aes.BlockSize)
copy(rc.iv, p[2+salt:])
rc.cycles = int(p[0] & 0x3f)
rc.rc = readers[0]
return rc, nil
}
+232
View File
@@ -0,0 +1,232 @@
// Package bcj2 implements the BCJ2 filter for x86 binaries.
package bcj2
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"bookhoard/internal/sevenzip/internal/util"
)
type readCloser struct {
main util.ReadCloser
call io.ReadCloser
jump io.ReadCloser
rd util.ReadCloser
nrange uint
code uint
sd [256 + 2]uint
previous byte
written uint32
buf *bytes.Buffer
}
const (
numMoveBits = 5
numbitModelTotalBits = 11
bitModelTotal uint = 1 << numbitModelTotalBits
numTopBits = 24
topValue uint = 1 << numTopBits
)
var (
errAlreadyClosed = errors.New("bcj2: already closed")
errNeedFourReaders = errors.New("bcj2: need exactly four readers")
)
func isJcc(b0, b1 byte) bool {
return b0 == 0x0f && (b1&0xf0) == 0x80
}
func isJ(b0, b1 byte) bool {
return (b1&0xfe) == 0xe8 || isJcc(b0, b1)
}
func index(b0, b1 byte) int {
switch b1 {
case 0xe8:
return int(b0)
case 0xe9:
return 256
default:
return 257
}
}
// NewReader returns a new BCJ2 io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 4 {
return nil, errNeedFourReaders
}
rc := &readCloser{
main: util.ByteReadCloser(readers[0]),
call: readers[1],
jump: readers[2],
rd: util.ByteReadCloser(readers[3]),
nrange: 0xffffffff,
buf: new(bytes.Buffer),
}
rc.buf.Grow(1 << 16)
b := make([]byte, 5)
if _, err := io.ReadFull(rc.rd, b); err != nil {
if !errors.Is(err, io.EOF) {
err = fmt.Errorf("bcj2: error reading initial state: %w", err)
}
return nil, err
}
for _, x := range b {
rc.code = (rc.code << 8) | uint(x)
}
for i := range rc.sd {
rc.sd[i] = bitModelTotal >> 1
}
return rc, nil
}
func (rc *readCloser) Close() error {
if rc.main == nil || rc.call == nil || rc.jump == nil || rc.rd == nil {
return errAlreadyClosed
}
if err := errors.Join(rc.main.Close(), rc.call.Close(), rc.jump.Close(), rc.rd.Close()); err != nil {
return fmt.Errorf("bcj2: error closing: %w", err)
}
rc.main, rc.call, rc.jump, rc.rd = nil, nil, nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.main == nil || rc.call == nil || rc.jump == nil || rc.rd == nil {
return 0, errAlreadyClosed
}
if err := rc.read(); err != nil && !errors.Is(err, io.EOF) {
return 0, err
}
n, err := rc.buf.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("bcj2: error reading: %w", err)
}
return n, err
}
func (rc *readCloser) update() error {
if rc.nrange < topValue {
b, err := rc.rd.ReadByte()
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("bcj2: error reading byte: %w", err)
}
rc.code = (rc.code << 8) | uint(b)
rc.nrange <<= 8
}
return nil
}
func (rc *readCloser) decode(i int) (bool, error) {
newBound := (rc.nrange >> numbitModelTotalBits) * rc.sd[i]
if rc.code < newBound {
rc.nrange = newBound
rc.sd[i] += (bitModelTotal - rc.sd[i]) >> numMoveBits
if err := rc.update(); err != nil {
return false, err
}
return false, nil
}
rc.nrange -= newBound
rc.code -= newBound
rc.sd[i] -= rc.sd[i] >> numMoveBits
if err := rc.update(); err != nil {
return false, err
}
return true, nil
}
//nolint:cyclop,funlen
func (rc *readCloser) read() error {
var (
b byte
err error
)
for {
if b, err = rc.main.ReadByte(); err != nil {
if !errors.Is(err, io.EOF) {
err = fmt.Errorf("bcj2: error reading byte: %w", err)
}
return err
}
rc.written++
_ = rc.buf.WriteByte(b)
if isJ(rc.previous, b) {
break
}
rc.previous = b
if rc.buf.Len() == rc.buf.Cap() {
return nil
}
}
bit, err := rc.decode(index(rc.previous, b))
if err != nil {
return err
}
//nolint:nestif
if bit {
var r io.Reader
if b == 0xe8 {
r = rc.call
} else {
r = rc.jump
}
var dest uint32
if err = binary.Read(r, binary.BigEndian, &dest); err != nil {
if !errors.Is(err, io.EOF) {
err = fmt.Errorf("bcj2: error reading uint32: %w", err)
}
return err
}
dest -= rc.written + 4
_ = binary.Write(rc.buf, binary.LittleEndian, dest)
rc.previous = byte(dest >> 24)
rc.written += 4
} else {
rc.previous = b
}
return nil
}
+55
View File
@@ -0,0 +1,55 @@
package bra
import (
"encoding/binary"
"io"
)
const armAlignment = 4
type arm struct {
ip uint32
}
func (c *arm) Size() int { return armAlignment }
func (c *arm) Convert(b []byte, encoding bool) int {
if len(b) < c.Size() {
return 0
}
if c.ip == 0 {
c.ip += armAlignment
}
var i int
for i = 0; i < len(b) & ^(armAlignment-1); i += armAlignment {
v := binary.LittleEndian.Uint32(b[i:])
c.ip += uint32(armAlignment)
if b[i+3] == 0xeb {
v <<= 2
if encoding {
v += c.ip
} else {
v -= c.ip
}
v >>= 2
v &= 0x00ffffff
v |= 0xeb000000
}
binary.LittleEndian.PutUint32(b[i:], v)
}
return i
}
// NewARMReader returns a new ARM io.ReadCloser.
func NewARMReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
return newReader(readers, new(arm))
}
+104
View File
@@ -0,0 +1,104 @@
package bra
import (
"encoding/binary"
"io"
)
const bcjLookAhead = 4
type bcj struct {
ip, state uint32
}
func (c *bcj) Size() int { return bcjLookAhead + 1 }
func test86MSByte(b byte) bool {
return (b+1)&0xfe == 0
}
//nolint:cyclop,funlen,gocognit
func (c *bcj) Convert(b []byte, encoding bool) int {
if len(b) < c.Size() {
return 0
}
var (
pos uint32
mask = c.state & 7
)
for {
p := pos
for ; int(p) < len(b)-bcjLookAhead; p++ {
if b[p]&0xfe == 0xe8 {
break
}
}
d := p - pos
pos = p
if int(p) >= len(b)-bcjLookAhead {
if d > 2 {
c.state = 0
} else {
c.state = mask >> d
}
c.ip += pos
return int(pos)
}
if d > 2 {
mask = 0
} else {
mask >>= d
if mask != 0 && (mask > 4 || mask == 3 || test86MSByte(b[p+(mask>>1)+1])) {
mask = (mask >> 1) | 4
pos++
continue
}
}
//nolint:nestif
if test86MSByte(b[p+4]) {
v := binary.LittleEndian.Uint32(b[p+1:])
cur := c.ip + uint32(c.Size()) + pos //nolint:gosec
pos += uint32(c.Size()) //nolint:gosec
if encoding {
v += cur
} else {
v -= cur
}
if mask != 0 {
sh := mask & 6 << 2
if test86MSByte(byte(v >> sh)) {
v ^= (uint32(0x100) << sh) - 1
if encoding {
v += cur
} else {
v -= cur
}
}
mask = 0
}
binary.LittleEndian.PutUint32(b[p+1:], v)
b[p+4] = 0 - b[p+4]&1
} else {
mask = (mask >> 1) | 4
pos++
}
}
}
// NewBCJReader returns a new BCJ io.ReadCloser.
func NewBCJReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
return newReader(readers, new(bcj))
}
+7
View File
@@ -0,0 +1,7 @@
// Package bra implements the branch rewriting filter for binaries.
package bra
type converter interface {
Size() int
Convert(b []byte, encoding bool) int
}
+48
View File
@@ -0,0 +1,48 @@
package bra
import (
"encoding/binary"
"io"
)
const ppcAlignment = 4
type ppc struct {
ip uint32
}
func (c *ppc) Size() int { return ppcAlignment }
func (c *ppc) Convert(b []byte, encoding bool) int {
if len(b) < c.Size() {
return 0
}
var i int
for i = 0; i < len(b) & ^(ppcAlignment-1); i += ppcAlignment {
v := binary.BigEndian.Uint32(b[i:])
if b[i+0]&0xfc == 0x48 && b[i+3]&3 == 1 {
if encoding {
v += c.ip
} else {
v -= c.ip
}
v &= 0x03ffffff
v |= 0x48000000
}
c.ip += uint32(ppcAlignment)
binary.BigEndian.PutUint32(b[i:], v)
}
return i
}
// NewPPCReader returns a new PPC io.ReadCloser.
func NewPPCReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
return newReader(readers, new(ppc))
}
+72
View File
@@ -0,0 +1,72 @@
package bra
import (
"bytes"
"errors"
"fmt"
"io"
)
type readCloser struct {
rc io.ReadCloser
buf bytes.Buffer
n int
conv converter
}
var (
errAlreadyClosed = errors.New("bra: already closed")
errNeedOneReader = errors.New("bra: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.rc == nil {
return errAlreadyClosed
}
if err := rc.rc.Close(); err != nil {
return fmt.Errorf("bra: error closing: %w", err)
}
rc.rc = nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.rc == nil {
return 0, errAlreadyClosed
}
if _, err := io.CopyN(&rc.buf, rc.rc, int64(max(len(p), rc.conv.Size())-rc.buf.Len())); err != nil {
if !errors.Is(err, io.EOF) {
return 0, fmt.Errorf("bra: error buffering: %w", err)
}
if rc.buf.Len() < rc.conv.Size() {
rc.n = rc.buf.Len()
}
}
rc.n += rc.conv.Convert(rc.buf.Bytes()[rc.n:], false)
n, err := rc.buf.Read(p[:min(rc.n, len(p))])
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("bra: error reading: %w", err)
}
rc.n -= n
return n, err
}
func newReader(readers []io.ReadCloser, conv converter) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
return &readCloser{
rc: readers[0],
conv: conv,
}, nil
}
+53
View File
@@ -0,0 +1,53 @@
package bra
import (
"encoding/binary"
"io"
)
const sparcAlignment = 4
type sparc struct {
ip uint32
}
func (c *sparc) Size() int { return sparcAlignment }
func (c *sparc) Convert(b []byte, encoding bool) int {
if len(b) < c.Size() {
return 0
}
var i int
for i = 0; i < len(b) & ^(sparcAlignment-1); i += sparcAlignment {
v := binary.BigEndian.Uint32(b[i:])
if (b[i+0] == 0x40 && b[i+1]&0xc0 == 0) || (b[i+0] == 0x7f && b[i+1] >= 0xc0) {
v <<= 2
if encoding {
v += c.ip
} else {
v -= c.ip
}
v &= 0x01ffffff
v -= uint32(1) << 24
v ^= 0xff000000
v >>= 2
v |= 0x40000000
}
c.ip += uint32(sparcAlignment)
binary.BigEndian.PutUint32(b[i:], v)
}
return i
}
// NewSPARCReader returns a new SPARC io.ReadCloser.
func NewSPARCReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
return newReader(readers, new(sparc))
}
+113
View File
@@ -0,0 +1,113 @@
// Package brotli implements the Brotli decompressor.
package brotli
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"github.com/andybalholm/brotli"
"github.com/bodgit/plumbing"
)
type readCloser struct {
c io.Closer
r *brotli.Reader
}
const (
frameMagic uint32 = 0x184d2a50
frameSize uint32 = 8
brotliMagic uint16 = 0x5242 // 'B', 'R'
)
var (
//nolint:gochecknoglobals
brotliReaderPool sync.Pool
errAlreadyClosed = errors.New("brotli: already closed")
errNeedOneReader = errors.New("brotli: need exactly one reader")
)
// This isn't part of the Brotli format but is prepended by the 7-zip implementation.
type headerFrame struct {
FrameMagic uint32
FrameSize uint32
CompressedSize uint32
BrotliMagic uint16
UncompressedSize uint16 // * 64 KB
}
func (rc *readCloser) Close() error {
if rc.c == nil || rc.r == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("brotli: error closing: %w", err)
}
brotliReaderPool.Put(rc.r)
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("brotli: error reading: %w", err)
}
return n, err
}
// NewReader returns a new Brotli io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
hr, b := new(headerFrame), new(bytes.Buffer)
b.Grow(binary.Size(hr))
// The 7-Zip Brotli compressor adds a 16 byte frame to the beginning of
// the data which will confuse a pure Brotli implementation. Read it
// but keep a copy so we can add it back if it doesn't look right
if err := binary.Read(io.TeeReader(readers[0], b), binary.LittleEndian, hr); err != nil {
if !errors.Is(err, io.EOF) {
err = fmt.Errorf("brotli: error reading frame: %w", err)
}
return nil, err
}
var reader io.ReadCloser
// If the header looks right, continue reading from that point
// onwards, otherwise prepend it again and hope for the best
if hr.FrameMagic == frameMagic && hr.FrameSize == frameSize && hr.BrotliMagic == brotliMagic {
reader = readers[0]
} else {
reader = plumbing.MultiReadCloser(io.NopCloser(b), readers[0])
}
r, ok := brotliReaderPool.Get().(*brotli.Reader)
if ok {
_ = r.Reset(reader)
} else {
r = brotli.NewReader(reader)
}
return &readCloser{
c: readers[0],
r: r,
}, nil
}
@@ -0,0 +1,58 @@
// Package bzip2 implements the Bzip2 decompressor.
package bzip2
import (
"compress/bzip2"
"errors"
"fmt"
"io"
)
type readCloser struct {
c io.Closer
r io.Reader
}
var (
errAlreadyClosed = errors.New("bzip2: already closed")
errNeedOneReader = errors.New("bzip2: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.c == nil || rc.r == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("bzip2: error closing: %w", err)
}
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("bzip2: error reading: %w", err)
}
return n, err
}
// NewReader returns a new bzip2 io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
return &readCloser{
c: readers[0],
r: bzip2.NewReader(readers[0]),
}, nil
}
@@ -0,0 +1,77 @@
// Package deflate implements the Deflate decompressor.
package deflate
import (
"errors"
"fmt"
"io"
"sync"
"bookhoard/internal/sevenzip/internal/util"
"github.com/klauspost/compress/flate"
)
type readCloser struct {
c io.Closer
fr io.ReadCloser
}
var (
//nolint:gochecknoglobals
flateReaderPool sync.Pool
errAlreadyClosed = errors.New("deflate: already closed")
errNeedOneReader = errors.New("deflate: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.c == nil || rc.fr == nil {
return errAlreadyClosed
}
if err := errors.Join(rc.fr.Close(), rc.c.Close()); err != nil {
return fmt.Errorf("deflate: error closing: %w", err)
}
flateReaderPool.Put(rc.fr)
rc.c, rc.fr = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.c == nil || rc.fr == nil {
return 0, errAlreadyClosed
}
n, err := rc.fr.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("deflate: error reading: %w", err)
}
return n, err
}
// NewReader returns a new DEFLATE io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
fr, ok := flateReaderPool.Get().(io.ReadCloser)
if ok {
frf, ok := fr.(flate.Resetter)
if ok {
if err := frf.Reset(util.ByteReadCloser(readers[0]), nil); err != nil {
return nil, fmt.Errorf("deflate: error resetting: %w", err)
}
}
} else {
fr = flate.NewReader(util.ByteReadCloser(readers[0]))
}
return &readCloser{
c: readers[0],
fr: fr,
}, nil
}
@@ -0,0 +1,89 @@
// Package delta implements the Delta filter.
package delta
import (
"errors"
"fmt"
"io"
)
type readCloser struct {
rc io.ReadCloser
state [stateSize]byte
delta int
}
const (
stateSize = 256
)
var (
errAlreadyClosed = errors.New("delta: already closed")
errNeedOneReader = errors.New("delta: need exactly one reader")
errInsufficientProperties = errors.New("delta: not enough properties")
)
func (rc *readCloser) Close() error {
if rc.rc == nil {
return errAlreadyClosed
}
if err := rc.rc.Close(); err != nil {
return fmt.Errorf("delta: error closing: %w", err)
}
rc.rc = nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.rc == nil {
return 0, errAlreadyClosed
}
n, err := rc.rc.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
return n, fmt.Errorf("delta: error reading: %w", err)
}
var (
buffer [stateSize]byte
j int
)
copy(buffer[:], rc.state[:rc.delta])
for i := 0; i < n; {
for j = 0; j < rc.delta && i < n; i++ {
p[i] = buffer[j] + p[i]
buffer[j] = p[i]
j++
}
}
if j == rc.delta {
j = 0
}
copy(rc.state[:], buffer[j:rc.delta])
copy(rc.state[rc.delta-j:], buffer[:j])
return n, nil
}
// NewReader returns a new Delta io.ReadCloser.
func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
if len(p) != 1 {
return nil, errInsufficientProperties
}
return &readCloser{
rc: readers[0],
delta: int(p[0] + 1),
}, nil
}
+71
View File
@@ -0,0 +1,71 @@
// Package lz4 implements the LZ4 decompressor.
package lz4
import (
"errors"
"fmt"
"io"
"sync"
lz4 "github.com/pierrec/lz4/v4"
)
type readCloser struct {
c io.Closer
r *lz4.Reader
}
var (
//nolint:gochecknoglobals
lz4ReaderPool sync.Pool
errAlreadyClosed = errors.New("lz4: already closed")
errNeedOneReader = errors.New("lz4: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.c == nil || rc.r == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("lz4: error closing: %w", err)
}
lz4ReaderPool.Put(rc.r)
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("lz4: error reading: %w", err)
}
return n, err
}
// NewReader returns a new LZ4 io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
r, ok := lz4ReaderPool.Get().(*lz4.Reader)
if ok {
r.Reset(readers[0])
} else {
r = lz4.NewReader(readers[0])
}
return &readCloser{
c: readers[0],
r: r,
}, nil
}
+112
View File
@@ -0,0 +1,112 @@
// Package lzma implements the LZMA decompressor.
package lzma
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/ulikunitz/xz/lzma"
)
type readCloser struct {
c io.Closer
r io.Reader
}
var (
errAlreadyClosed = errors.New("lzma: already closed")
errNeedOneReader = errors.New("lzma: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.c == nil || rc.r == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("lzma: error closing: %w", err)
}
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("lzma: error reading: %w", err)
}
return n, err
}
// NewReader returns a new LZMA io.ReadCloser.
func NewReader(p []byte, s uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
h := bytes.NewBuffer(p)
_ = binary.Write(h, binary.LittleEndian, s)
lr, err := lzma.NewReader(multiReader(h, readers[0]))
if err != nil {
return nil, fmt.Errorf("lzma: error creating reader: %w", err)
}
return &readCloser{
c: readers[0],
r: lr,
}, nil
}
func multiReader(b *bytes.Buffer, rc io.ReadCloser) io.Reader {
mr := io.MultiReader(b, rc)
if br, ok := rc.(io.ByteReader); ok {
return &multiByteReader{
b: b,
br: br,
mr: mr,
}
}
return mr
}
type multiByteReader struct {
b *bytes.Buffer
br io.ByteReader
mr io.Reader
}
func (m *multiByteReader) ReadByte() (b byte, err error) {
if m.b.Len() > 0 {
b, err = m.b.ReadByte()
} else {
b, err = m.br.ReadByte()
}
if err != nil {
err = fmt.Errorf("lzma: error multi byte reading: %w", err)
}
return b, err
}
func (m *multiByteReader) Read(p []byte) (int, error) {
n, err := m.mr.Read(p)
if err != nil {
err = fmt.Errorf("lzma: error multi reading: %w", err)
}
return n, err
}
@@ -0,0 +1,77 @@
// Package lzma2 implements the LZMA2 decompressor.
package lzma2
import (
"errors"
"fmt"
"io"
"github.com/ulikunitz/xz/lzma"
)
type readCloser struct {
c io.Closer
r io.Reader
}
var (
errAlreadyClosed = errors.New("lzma2: already closed")
errNeedOneReader = errors.New("lzma2: need exactly one reader")
errInsufficientProperties = errors.New("lzma2: not enough properties")
)
func (rc *readCloser) Close() error {
if rc.c == nil || rc.r == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("lzma2: error closing: %w", err)
}
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("lzma2: error reading: %w", err)
}
return n, err
}
// NewReader returns a new LZMA2 io.ReadCloser.
func NewReader(p []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
if len(p) != 1 {
return nil, errInsufficientProperties
}
config := lzma.Reader2Config{
DictCap: (2 | (int(p[0]) & 1)) << (p[0]/2 + 11), // This gem came from Lzma2Dec.c
}
if err := config.Verify(); err != nil {
return nil, fmt.Errorf("lzma2: error verifying config: %w", err)
}
lr, err := config.NewReader2(readers[0])
if err != nil {
return nil, fmt.Errorf("lzma2: error creating reader: %w", err)
}
return &readCloser{
c: readers[0],
r: lr,
}, nil
}
+138
View File
@@ -0,0 +1,138 @@
// Package pool implements the reader pooling.
package pool
import (
"container/list"
"runtime"
"sort"
"sync"
"bookhoard/internal/sevenzip/internal/util"
)
// Pooler is the interface implemented by a pool.
type Pooler interface {
Get(offset int64) (util.SizeReadSeekCloser, bool)
Put(offset int64, rc util.SizeReadSeekCloser) (bool, error)
}
// Constructor is the function prototype used to instantiate a pool.
type Constructor func() (Pooler, error)
type noopPool struct{}
// NewNoopPool returns a Pooler that doesn't actually pool anything.
func NewNoopPool() (Pooler, error) {
return new(noopPool), nil
}
func (noopPool) Get(_ int64) (util.SizeReadSeekCloser, bool) {
return nil, false
}
func (noopPool) Put(_ int64, rc util.SizeReadSeekCloser) (bool, error) {
return false, rc.Close() //nolint:wrapcheck
}
type pool struct {
mutex sync.Mutex
size int
evictList *list.List
items map[int64]*list.Element
}
type entry struct {
key int64
value util.SizeReadSeekCloser
}
// NewPool returns a Pooler that uses a LRU strategy to maintain a fixed pool
// of util.SizeReadSeekCloser's keyed by their stream offset.
func NewPool() (Pooler, error) {
return &pool{
size: runtime.NumCPU(),
evictList: list.New(),
items: make(map[int64]*list.Element),
}, nil
}
func (p *pool) Get(offset int64) (util.SizeReadSeekCloser, bool) {
p.mutex.Lock()
defer p.mutex.Unlock()
if ent, ok := p.items[offset]; ok {
_ = p.removeElement(ent, false)
return ent.Value.(*entry).value, true //nolint:forcetypeassert
}
// Sort keys in descending order
keys := p.keys()
sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] })
for _, k := range keys {
// First key less than offset is the closest
if k < offset {
ent := p.items[k]
_ = p.removeElement(ent, false)
return ent.Value.(*entry).value, true //nolint:forcetypeassert
}
}
return nil, false
}
func (p *pool) Put(offset int64, rc util.SizeReadSeekCloser) (bool, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
if _, ok := p.items[offset]; ok {
return false, nil
}
ent := &entry{offset, rc}
entry := p.evictList.PushFront(ent)
p.items[offset] = entry
var err error
evict := p.evictList.Len() > p.size
if evict {
err = p.removeOldest()
}
return evict, err
}
func (p *pool) keys() []int64 {
keys := make([]int64, len(p.items))
i := 0
for ent := p.evictList.Back(); ent != nil; ent = ent.Prev() {
keys[i] = ent.Value.(*entry).key //nolint:forcetypeassert
i++
}
return keys
}
func (p *pool) removeOldest() error {
if ent := p.evictList.Back(); ent != nil {
return p.removeElement(ent, true)
}
return nil
}
func (p *pool) removeElement(e *list.Element, cb bool) error {
p.evictList.Remove(e)
kv := e.Value.(*entry) //nolint:forcetypeassert
delete(p.items, kv.key)
if cb {
return kv.value.Close() //nolint:wrapcheck
}
return nil
}
@@ -0,0 +1,8 @@
package util
import "bytes"
// CRC32Equal compares CRC32 checksums.
func CRC32Equal(b []byte, c uint32) bool {
return bytes.Equal(b, []byte{byte(0xff & (c >> 24)), byte(0xff & (c >> 16)), byte(0xff & (c >> 8)), byte(0xff & c)})
}
+68
View File
@@ -0,0 +1,68 @@
// Package util implements various utility types and interfaces.
package util
import "io"
// SizeReadSeekCloser is an io.Reader, io.Seeker, and io.Closer with a Size
// method.
type SizeReadSeekCloser interface {
io.Reader
io.Seeker
io.Closer
Size() int64
}
// Reader is both an io.Reader and io.ByteReader.
type Reader interface {
io.Reader
io.ByteReader
}
// ReadCloser is a Reader that is also an io.Closer.
type ReadCloser interface {
Reader
io.Closer
}
type nopCloser struct {
Reader
}
func (nopCloser) Close() error {
return nil
}
// NopCloser returns a ReadCloser with a no-op Close method wrapping the
// provided Reader r.
func NopCloser(r Reader) ReadCloser {
return &nopCloser{r}
}
type byteReadCloser struct {
io.ReadCloser
}
func (rc *byteReadCloser) ReadByte() (byte, error) {
var b [1]byte
n, err := rc.Read(b[:])
if err != nil {
return 0, err //nolint:wrapcheck
}
if n == 0 {
return 0, io.ErrNoProgress
}
return b[0], nil
}
// ByteReadCloser returns a ReadCloser either by returning the io.ReadCloser
// r if it implements the interface, or wrapping it with a ReadByte method.
func ByteReadCloser(r io.ReadCloser) ReadCloser {
if rc, ok := r.(ReadCloser); ok {
return rc
}
return &byteReadCloser{r}
}
+80
View File
@@ -0,0 +1,80 @@
// Package zstd implements the Zstandard decompressor.
package zstd
import (
"errors"
"fmt"
"io"
"runtime"
"sync"
"github.com/klauspost/compress/zstd"
)
type readCloser struct {
c io.Closer
r *zstd.Decoder
}
var (
//nolint:gochecknoglobals
zstdReaderPool sync.Pool
errAlreadyClosed = errors.New("zstd: already closed")
errNeedOneReader = errors.New("zstd: need exactly one reader")
)
func (rc *readCloser) Close() error {
if rc.c == nil {
return errAlreadyClosed
}
if err := rc.c.Close(); err != nil {
return fmt.Errorf("zstd: error closing: %w", err)
}
zstdReaderPool.Put(rc.r)
rc.c, rc.r = nil, nil
return nil
}
func (rc *readCloser) Read(p []byte) (int, error) {
if rc.r == nil {
return 0, errAlreadyClosed
}
n, err := rc.r.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
err = fmt.Errorf("zstd: error reading: %w", err)
}
return n, err
}
// NewReader returns a new Zstandard io.ReadCloser.
func NewReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
var err error
r, ok := zstdReaderPool.Get().(*zstd.Decoder)
if ok {
if err = r.Reset(readers[0]); err != nil {
return nil, fmt.Errorf("zstd: error resetting: %w", err)
}
} else {
if r, err = zstd.NewReader(readers[0]); err != nil {
return nil, fmt.Errorf("zstd: error creating reader: %w", err)
}
runtime.SetFinalizer(r, (*zstd.Decoder).Close)
}
return &readCloser{
c: readers[0],
r: r,
}, nil
}
+87
View File
@@ -0,0 +1,87 @@
// Package sevenzip provides read access to 7-Zip archives.
//
// Portions Copyright 2016 The go4 Authors
// Licensed under the Apache License, Version 2.0
package sevenzip
import (
"io"
"sort"
)
// SizeReaderAt is a ReaderAt with a Size method.
//
// An io.SectionReader implements SizeReaderAt.
type SizeReaderAt interface {
Size() int64
io.ReaderAt
}
// NewMultiReaderAt is like io.MultiReader but produces a ReaderAt
// (and Size), instead of just a reader.
func NewMultiReaderAt(parts ...SizeReaderAt) SizeReaderAt {
m := &multiRA{
parts: make([]offsetAndSource, 0, len(parts)),
}
var off int64
for _, p := range parts {
m.parts = append(m.parts, offsetAndSource{off, p})
off += p.Size()
}
m.size = off
return m
}
type offsetAndSource struct {
off int64
SizeReaderAt
}
type multiRA struct {
parts []offsetAndSource
size int64
}
func (m *multiRA) Size() int64 { return m.size }
func (m *multiRA) ReadAt(p []byte, off int64) (n int, err error) {
wantN := len(p)
// Skip past the requested offset.
skipParts := sort.Search(len(m.parts), func(i int) bool {
// This function returns whether parts[i] will
// contribute any bytes to our output.
part := m.parts[i]
return part.off+part.Size() > off
})
parts := m.parts[skipParts:]
// How far to skip in the first part.
needSkip := off
if len(parts) > 0 {
needSkip -= parts[0].off
}
for len(parts) > 0 && len(p) > 0 {
readP := p
partSize := parts[0].Size()
if int64(len(readP)) > partSize-needSkip {
readP = readP[:partSize-needSkip]
}
pn, err0 := parts[0].ReadAt(readP, needSkip)
if err0 != nil {
return n, err0
}
n += pn
p = p[pn:]
if int64(pn)+needSkip == partSize {
parts = parts[1:]
}
needSkip = 0
}
if n != wantN {
err = io.ErrUnexpectedEOF
}
return
}
+855
View File
@@ -0,0 +1,855 @@
// Package sevenzip provides read access to 7-zip archives.
package sevenzip
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"io"
iofs "io/fs"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"bookhoard/internal/sevenzip/internal/pool"
"bookhoard/internal/sevenzip/internal/util"
"github.com/bodgit/plumbing"
"github.com/spf13/afero"
)
var (
errFormat = errors.New("sevenzip: not a valid 7-zip file")
errChecksum = errors.New("sevenzip: checksum error")
errTooMuch = errors.New("sevenzip: too much data")
errNegativeSize = errors.New("sevenzip: size cannot be negative")
errOneHeaderStream = errors.New("sevenzip: expected only one folder in header stream")
)
// sizeSectionReader wraps io.SectionReader to add Size() method
type sizeSectionReader struct {
*io.SectionReader
size int64
}
func (s *sizeSectionReader) Size() int64 {
return s.size
}
// ReadError is used to wrap read I/O errors.
type ReadError struct {
// Encrypted is a hint that there is encryption involved.
Encrypted bool
Err error
}
func (e ReadError) Error() string {
return fmt.Sprintf("sevenzip: read error: %v", e.Err)
}
func (e ReadError) Unwrap() error {
return e.Err
}
// A Reader serves content from a 7-Zip archive.
type Reader struct {
r io.ReaderAt
start int64
end int64
si *streamsInfo
p string
File []*File
pool []pool.Pooler
fileListOnce sync.Once
fileList []fileListEntry
}
// A ReadCloser is a [Reader] that must be closed when no longer needed.
type ReadCloser struct {
f []afero.File
Reader
}
// A File is a single file in a 7-Zip archive. The file information is in the
// embedded [FileHeader]. The file content can be accessed by calling
// [File.Open].
type File struct {
FileHeader
zip *Reader
folder int
offset int64
}
type fileReader struct {
rc util.SizeReadSeekCloser
f *File
n int64
}
func (fr *fileReader) Stat() (iofs.FileInfo, error) {
return headerFileInfo{&fr.f.FileHeader}, nil
}
func (fr *fileReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
if fr.n <= 0 {
return 0, io.EOF
}
if int64(len(p)) > fr.n {
p = p[0:fr.n]
}
n, err := fr.rc.Read(p)
fr.n -= int64(n)
if err != nil && !errors.Is(err, io.EOF) {
e := &ReadError{
Err: err,
}
if frc, ok := fr.rc.(*folderReadCloser); ok {
e.Encrypted = frc.hasEncryption
}
return n, e
}
return n, err //nolint:wrapcheck
}
func (fr *fileReader) Close() error {
if fr.rc == nil {
return nil
}
offset, err := fr.rc.Seek(0, io.SeekCurrent)
if err != nil {
return fmt.Errorf("sevenzip: error seeking current position: %w", err)
}
if offset == fr.rc.Size() { // EOF reached
if err := fr.rc.Close(); err != nil {
return fmt.Errorf("sevenzip: error closing: %w", err)
}
} else {
f := fr.f
if _, err := f.zip.pool[f.folder].Put(offset, fr.rc); err != nil {
return fmt.Errorf("sevenzip: error adding to pool: %w", err)
}
}
fr.rc = nil
return nil
}
// Open returns an [io.ReadCloser] that provides access to the [File]'s
// contents. Multiple files may be read concurrently.
func (f *File) Open() (io.ReadCloser, error) {
if f.FileHeader.isEmptyStream || f.FileHeader.isEmptyFile {
// Return empty reader for directory or empty file
return io.NopCloser(bytes.NewReader(nil)), nil
}
rc, _ := f.zip.pool[f.folder].Get(f.offset)
if rc == nil {
var (
encrypted bool
err error
)
rc, _, encrypted, err = f.zip.folderReader(f.zip.si, f.folder)
if err != nil {
return nil, &ReadError{
Encrypted: encrypted,
Err: err,
}
}
}
if _, err := rc.Seek(f.offset, io.SeekStart); err != nil {
e := &ReadError{
Err: err,
}
if fr, ok := rc.(*folderReadCloser); ok {
e.Encrypted = fr.hasEncryption
}
return nil, e
}
return &fileReader{
rc: rc,
f: f,
n: int64(f.UncompressedSize), //nolint:gosec
}, nil
}
func openReader(fs afero.Fs, name string) (io.ReaderAt, int64, []afero.File, error) {
f, err := fs.Open(filepath.Clean(name))
if err != nil {
return nil, 0, nil, fmt.Errorf("sevenzip: error opening: %w", err)
}
info, err := f.Stat()
if err != nil {
err = errors.Join(err, f.Close())
return nil, 0, nil, fmt.Errorf("sevenzip: error retrieving file info: %w", err)
}
var reader io.ReaderAt = f
size := info.Size()
files := []afero.File{f}
if ext := filepath.Ext(name); ext == ".001" {
sr := []SizeReaderAt{&sizeSectionReader{io.NewSectionReader(f, 0, size), size}}
for i := 2; true; i++ {
f, err := fs.Open(fmt.Sprintf("%s.%03d", strings.TrimSuffix(name, ext), i))
if err != nil {
if errors.Is(err, iofs.ErrNotExist) {
break
}
errs := make([]error, 0, len(files)+1)
errs = append(errs, err)
for _, file := range files {
errs = append(errs, file.Close())
}
return nil, 0, nil, fmt.Errorf("sevenzip: error opening: %w", errors.Join(errs...))
}
files = append(files, f)
info, err = f.Stat()
if err != nil {
errs := make([]error, 0, len(files)+1)
errs = append(errs, err)
for _, file := range files {
errs = append(errs, file.Close())
}
return nil, 0, nil, fmt.Errorf("sevenzip: error retrieving file info: %w", errors.Join(errs...))
}
sr = append(sr, io.NewSectionReader(f, 0, info.Size()))
}
mr := NewMultiReaderAt(sr...)
reader, size = mr, mr.Size()
}
return reader, size, files, nil
}
// OpenReaderWithPassword will open the 7-zip file specified by name using
// password as the basis of the decryption key and return a [*ReadCloser]. If
// name has a ".001" suffix it is assumed there are multiple volumes and each
// sequential volume will be opened.
func OpenReaderWithPassword(name, password string) (*ReadCloser, error) {
reader, size, files, err := openReader(afero.NewOsFs(), name)
if err != nil {
return nil, err
}
r := new(ReadCloser)
r.p = password
if err := r.init(reader, size); err != nil {
errs := make([]error, 0, len(files)+1)
errs = append(errs, err)
for _, file := range files {
errs = append(errs, file.Close())
}
return nil, fmt.Errorf("sevenzip: error initialising: %w", errors.Join(errs...))
}
r.f = files
return r, nil
}
// OpenReader will open the 7-zip file specified by name and return a
// [*ReadCloser]. If name has a ".001" suffix it is assumed there are multiple
// volumes and each sequential volume will be opened.
func OpenReader(name string) (*ReadCloser, error) {
return OpenReaderWithPassword(name, "")
}
// NewReaderWithPassword returns a new [*Reader] reading from r using password
// as the basis of the decryption key, which is assumed to have the given size
// in bytes.
func NewReaderWithPassword(r io.ReaderAt, size int64, password string) (*Reader, error) {
if size < 0 {
return nil, errNegativeSize
}
zr := new(Reader)
zr.p = password
if err := zr.init(r, size); err != nil {
return nil, err
}
return zr, nil
}
// NewReader returns a new [*Reader] reading from r, which is assumed to have
// the given size in bytes.
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
return NewReaderWithPassword(r, size, "")
}
func (z *Reader) folderReader(si *streamsInfo, f int) (*folderReadCloser, uint32, bool, error) {
// Create a SectionReader covering all of the streams data
return si.FolderReader(io.NewSectionReader(z.r, z.start, z.end-z.start), f, z.p)
}
const (
chunkSize = 4096
searchLimit = 1 << 20 // 1 MiB
)
func findSignature(r io.ReaderAt, search []byte) ([]int64, error) {
chunk := make([]byte, chunkSize+len(search))
offsets := make([]int64, 0, 2)
for offset := int64(0); offset < searchLimit; offset += chunkSize {
n, err := r.ReadAt(chunk, offset)
for i := 0; ; {
idx := bytes.Index(chunk[i:n], search)
if idx == -1 {
break
}
offsets = append(offsets, offset+int64(i+idx))
if offsets[0] == 0 {
// If signature is at the beginning, return immediately, it's a regular archive
return offsets, nil
}
i += idx + 1
}
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("sevenzip: error reading chunk: %w", err)
}
}
return offsets, nil
}
//nolint:cyclop,funlen,gocognit,gocyclo,maintidx
func (z *Reader) init(r io.ReaderAt, size int64) (err error) {
h := crc32.NewIEEE()
tra := plumbing.TeeReaderAt(r, h)
var (
signature = []byte{'7', 'z', 0xbc, 0xaf, 0x27, 0x1c}
offsets []int64
)
offsets, err = findSignature(r, signature)
if err != nil {
return err
}
if len(offsets) == 0 {
return errFormat
}
var (
sr *io.SectionReader
off int64
start startHeader
)
for _, off = range offsets {
sr = io.NewSectionReader(tra, off, size-off) // Will only read first 32 bytes
var sh signatureHeader
if err = binary.Read(sr, binary.LittleEndian, &sh); err != nil {
return fmt.Errorf("sevenzip: error reading signature header: %w", err)
}
z.r = r
h.Reset()
if err = binary.Read(sr, binary.LittleEndian, &start); err != nil {
return fmt.Errorf("sevenzip: error reading start header: %w", err)
}
// CRC of the start header should match
if util.CRC32Equal(h.Sum(nil), sh.CRC) {
break
}
err = errChecksum
}
if err != nil {
return err
}
// Work out where we are in the file (32, avoiding magic numbers)
if z.start, err = sr.Seek(0, io.SeekCurrent); err != nil {
return fmt.Errorf("sevenzip: error seeking current position: %w", err)
}
// Seek over the streams
if z.end, err = sr.Seek(int64(start.Offset), io.SeekCurrent); err != nil { //nolint:gosec
return fmt.Errorf("sevenzip: error seeking over streams: %w", err)
}
z.start += off
z.end += off
h.Reset()
// Bound bufio.Reader otherwise it can read trailing garbage which screws up the CRC check
br := bufio.NewReader(io.NewSectionReader(tra, z.end, int64(start.Size))) //nolint:gosec
var (
id byte
header *header
streamsInfo *streamsInfo
)
if id, err = br.ReadByte(); err != nil {
return fmt.Errorf("sevenzip: error reading header id: %w", err)
}
switch id {
case idHeader:
if header, err = readHeader(br); err != nil {
return err
}
case idEncodedHeader:
if streamsInfo, err = readStreamsInfo(br); err != nil {
return err
}
default:
return errUnexpectedID
}
// If there's more data to read, we've not parsed this correctly. This
// won't break with trailing data as the bufio.Reader was bounded
if n, _ := io.CopyN(io.Discard, br, 1); n != 0 {
return errTooMuch
}
// CRC should match the one from the start header
if !util.CRC32Equal(h.Sum(nil), start.CRC) {
return errChecksum
}
// If the header was encoded we should have sufficient information now
// to decode it
if streamsInfo != nil {
if streamsInfo.Folders() != 1 {
return errOneHeaderStream
}
var (
fr *folderReadCloser
crc uint32
encrypted bool
)
fr, crc, encrypted, err = z.folderReader(streamsInfo, 0)
if err != nil {
return &ReadError{
Encrypted: encrypted,
Err: err,
}
}
defer func() {
err = errors.Join(err, fr.Close())
}()
if header, err = readEncodedHeader(util.ByteReadCloser(fr)); err != nil {
return &ReadError{
Encrypted: fr.hasEncryption,
Err: err,
}
}
if crc != 0 && !util.CRC32Equal(fr.Checksum(), crc) {
return errChecksum
}
}
z.si = header.streamsInfo
// spew.Dump(header)
filesPerStream := make(map[int]int, z.si.Folders())
if header.filesInfo != nil {
folder, offset := 0, int64(0)
z.File = make([]*File, 0, len(header.filesInfo.file))
j := 0
for _, fh := range header.filesInfo.file {
f := new(File)
f.zip = z
f.FileHeader = fh
if f.FileHeader.FileInfo().IsDir() && !strings.HasSuffix(f.FileHeader.Name, "/") {
f.FileHeader.Name += "/"
}
if !fh.isEmptyStream && !fh.isEmptyFile {
f.folder, _, _ = header.streamsInfo.FileFolderAndSize(j)
// Make an exported copy of the folder index
f.Stream = f.folder
filesPerStream[f.folder]++
if f.folder != folder {
offset = 0
}
f.offset = offset
offset += int64(f.UncompressedSize) //nolint:gosec
folder = f.folder
j++
}
z.File = append(z.File, f)
}
}
// spew.Dump(filesPerStream)
z.pool = make([]pool.Pooler, z.si.Folders())
for i := range z.pool {
var newPool pool.Constructor = pool.NewNoopPool
if filesPerStream[i] > 1 {
newPool = pool.NewPool
}
if z.pool[i], err = newPool(); err != nil {
return err
}
}
return nil
}
// Volumes returns the list of volumes that have been opened as part of the
// current archive.
func (rc *ReadCloser) Volumes() []string {
volumes := make([]string, len(rc.f))
for idx, f := range rc.f {
volumes[idx] = f.Name()
}
return volumes
}
// Close closes the 7-zip file or volumes, rendering them unusable for I/O.
func (rc *ReadCloser) Close() error {
errs := make([]error, 0, len(rc.f))
for _, f := range rc.f {
errs = append(errs, f.Close())
}
err := errors.Join(errs...)
if err != nil {
err = fmt.Errorf("sevenzip: error closing: %w", err)
}
return err
}
type fileListEntry struct {
name string
file *File
isDir bool
isDup bool
}
type fileInfoDirEntry interface {
iofs.FileInfo
iofs.DirEntry
}
func (e *fileListEntry) stat() (fileInfoDirEntry, error) {
if e.isDup {
return nil, errors.New(e.name + ": duplicate entries in 7-zip file") //nolint:err113
}
if !e.isDir {
return headerFileInfo{&e.file.FileHeader}, nil
}
return e, nil
}
func (e *fileListEntry) Name() string {
_, elem := split(e.name)
return elem
}
func (e *fileListEntry) Size() int64 { return 0 }
func (e *fileListEntry) Mode() iofs.FileMode { return iofs.ModeDir | 0o555 }
func (e *fileListEntry) Type() iofs.FileMode { return iofs.ModeDir }
func (e *fileListEntry) IsDir() bool { return true }
func (e *fileListEntry) Sys() interface{} { return nil }
func (e *fileListEntry) ModTime() time.Time {
if e.file == nil {
return time.Time{}
}
return e.file.FileHeader.Modified.UTC()
}
func (e *fileListEntry) Info() (iofs.FileInfo, error) { return e, nil }
func toValidName(name string) string {
name = strings.ReplaceAll(name, `\`, `/`)
p := strings.TrimPrefix(path.Clean(name), "/")
for strings.HasPrefix(p, "../") {
p = p[len("../"):]
}
return p
}
//nolint:cyclop,funlen
func (z *Reader) initFileList() {
z.fileListOnce.Do(func() {
files := make(map[string]int)
knownDirs := make(map[string]int)
dirs := make(map[string]struct{})
for _, file := range z.File {
isDir := len(file.Name) > 0 && file.Name[len(file.Name)-1] == '/'
name := toValidName(file.Name)
if name == "" {
continue
}
if idx, ok := files[name]; ok {
z.fileList[idx].isDup = true
continue
}
if idx, ok := knownDirs[name]; ok {
z.fileList[idx].isDup = true
continue
}
for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
dirs[dir] = struct{}{}
}
idx := len(z.fileList)
entry := fileListEntry{
name: name,
file: file,
isDir: isDir,
}
z.fileList = append(z.fileList, entry)
if isDir {
knownDirs[name] = idx
} else {
files[name] = idx
}
}
for dir := range dirs {
if _, ok := knownDirs[dir]; !ok {
if idx, ok := files[dir]; ok {
z.fileList[idx].isDup = true
} else {
entry := fileListEntry{
name: dir,
file: nil,
isDir: true,
}
z.fileList = append(z.fileList, entry)
}
}
}
sort.Slice(z.fileList, func(i, j int) bool { return fileEntryLess(z.fileList[i].name, z.fileList[j].name) })
})
}
func fileEntryLess(x, y string) bool {
xdir, xelem := split(x)
ydir, yelem := split(y)
return xdir < ydir || xdir == ydir && xelem < yelem
}
// Open opens the named file in the 7-zip archive, using the semantics of
// [fs.FS.Open]: paths are always slash separated, with no leading / or ../
// elements.
func (z *Reader) Open(name string) (iofs.File, error) {
z.initFileList()
if !iofs.ValidPath(name) {
return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrInvalid}
}
e := z.openLookup(name)
if e == nil {
return nil, &iofs.PathError{Op: "open", Path: name, Err: iofs.ErrNotExist}
}
if e.isDir {
return &openDir{e, z.openReadDir(name), 0}, nil
}
rc, err := e.file.Open()
if err != nil {
return nil, err
}
return rc.(iofs.File), nil //nolint:forcetypeassert
}
func split(name string) (dir, elem string) {
if len(name) > 0 && name[len(name)-1] == '/' {
name = name[:len(name)-1]
}
i := len(name) - 1
for i >= 0 && name[i] != '/' {
i--
}
if i < 0 {
return ".", name
}
return name[:i], name[i+1:]
}
//nolint:gochecknoglobals
var dotFile = &fileListEntry{name: "./", isDir: true}
func (z *Reader) openLookup(name string) *fileListEntry {
if name == "." {
return dotFile
}
dir, elem := split(name)
files := z.fileList
i := sort.Search(len(files), func(i int) bool {
idir, ielem := split(files[i].name)
return idir > dir || idir == dir && ielem >= elem
})
if i < len(files) {
fname := files[i].name
if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
return &files[i]
}
}
return nil
}
func (z *Reader) openReadDir(dir string) []fileListEntry {
files := z.fileList
i := sort.Search(len(files), func(i int) bool {
idir, _ := split(files[i].name)
return idir >= dir
})
j := sort.Search(len(files), func(j int) bool {
jdir, _ := split(files[j].name)
return jdir > dir
})
return files[i:j]
}
type openDir struct {
e *fileListEntry
files []fileListEntry
offset int
}
func (d *openDir) Close() error { return nil }
func (d *openDir) Stat() (iofs.FileInfo, error) { return d.e.stat() }
var errIsDirectory = errors.New("is a directory")
func (d *openDir) Read([]byte) (int, error) {
return 0, &iofs.PathError{Op: "read", Path: d.e.name, Err: errIsDirectory}
}
func (d *openDir) ReadDir(count int) ([]iofs.DirEntry, error) {
n := len(d.files) - d.offset
if count > 0 && n > count {
n = count
}
if n == 0 {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
list := make([]iofs.DirEntry, n)
for i := range list {
s, err := d.files[d.offset+i].stat()
if err != nil {
return nil, err
}
list[i] = s
}
d.offset += n
return list, nil
}
+92
View File
@@ -0,0 +1,92 @@
package sevenzip
import (
"errors"
"io"
"sync"
"bookhoard/internal/sevenzip/internal/aes7z"
"bookhoard/internal/sevenzip/internal/bcj2"
"bookhoard/internal/sevenzip/internal/bra"
"bookhoard/internal/sevenzip/internal/brotli"
"bookhoard/internal/sevenzip/internal/bzip2"
"bookhoard/internal/sevenzip/internal/deflate"
"bookhoard/internal/sevenzip/internal/delta"
"bookhoard/internal/sevenzip/internal/lz4"
"bookhoard/internal/sevenzip/internal/lzma"
"bookhoard/internal/sevenzip/internal/lzma2"
"bookhoard/internal/sevenzip/internal/zstd"
)
// Decompressor describes the function signature that decompression/decryption
// methods must implement to return a new instance of themselves. They are
// passed any property bytes, the size of the stream and a slice of at least
// one io.ReadCloser's providing the stream(s) of bytes.
type Decompressor func([]byte, uint64, []io.ReadCloser) (io.ReadCloser, error)
var (
//nolint:gochecknoglobals
decompressors sync.Map
errNeedOneReader = errors.New("copy: need exactly one reader")
)
func newCopyReader(_ []byte, _ uint64, readers []io.ReadCloser) (io.ReadCloser, error) {
if len(readers) != 1 {
return nil, errNeedOneReader
}
// just return the passed io.ReadCloser)
return readers[0], nil
}
//nolint:gochecknoinits
func init() {
// Copy
RegisterDecompressor([]byte{0x00}, Decompressor(newCopyReader))
// Delta
RegisterDecompressor([]byte{0x03}, Decompressor(delta.NewReader))
// LZMA
RegisterDecompressor([]byte{0x03, 0x01, 0x01}, Decompressor(lzma.NewReader))
// BCJ
RegisterDecompressor([]byte{0x03, 0x03, 0x01, 0x03}, Decompressor(bra.NewBCJReader))
// BCJ2
RegisterDecompressor([]byte{0x03, 0x03, 0x01, 0x1b}, Decompressor(bcj2.NewReader))
// PPC
RegisterDecompressor([]byte{0x03, 0x03, 0x02, 0x05}, Decompressor(bra.NewPPCReader))
// ARM
RegisterDecompressor([]byte{0x03, 0x03, 0x05, 0x01}, Decompressor(bra.NewARMReader))
// SPARC
RegisterDecompressor([]byte{0x03, 0x03, 0x08, 0x05}, Decompressor(bra.NewSPARCReader))
// Deflate
RegisterDecompressor([]byte{0x04, 0x01, 0x08}, Decompressor(deflate.NewReader))
// Bzip2
RegisterDecompressor([]byte{0x04, 0x02, 0x02}, Decompressor(bzip2.NewReader))
// Zstandard
RegisterDecompressor([]byte{0x04, 0xf7, 0x11, 0x01}, Decompressor(zstd.NewReader))
// Brotli
RegisterDecompressor([]byte{0x04, 0xf7, 0x11, 0x02}, Decompressor(brotli.NewReader))
// LZ4
RegisterDecompressor([]byte{0x04, 0xf7, 0x11, 0x04}, Decompressor(lz4.NewReader))
// AES-CBC-256 & SHA-256
RegisterDecompressor([]byte{0x06, 0xf1, 0x07, 0x01}, Decompressor(aes7z.NewReader))
// LZMA2
RegisterDecompressor([]byte{0x21}, Decompressor(lzma2.NewReader))
}
// RegisterDecompressor allows custom decompressors for a specified method ID.
func RegisterDecompressor(method []byte, dcomp Decompressor) {
decompressors.Store(string(method), dcomp)
}
func decompressor(method []byte) Decompressor {
di, ok := decompressors.Load(string(method))
if !ok {
return nil
}
if d, ok := di.(Decompressor); ok {
return d
}
return nil
}
+467
View File
@@ -0,0 +1,467 @@
package sevenzip
import (
"bufio"
"errors"
"fmt"
"hash"
"hash/crc32"
"io"
iofs "io/fs"
"path"
"time"
"bookhoard/internal/sevenzip/internal/util"
"github.com/bodgit/plumbing"
)
var (
errAlgorithm = errors.New("sevenzip: unsupported compression algorithm")
errInvalidWhence = errors.New("invalid whence")
errNegativeSeek = errors.New("negative seek")
errSeekBackwards = errors.New("cannot seek backwards")
errSeekEOF = errors.New("cannot seek beyond EOF")
errMultipleOutputStreams = errors.New("more than one output stream")
errNoBoundStream = errors.New("cannot find bound stream")
errNoUnboundStream = errors.New("expecting one unbound output stream")
)
// CryptoReadCloser adds a Password method to decompressors.
type CryptoReadCloser interface {
Password(password string) error
}
type signatureHeader struct {
Signature [6]byte
Major byte
Minor byte
CRC uint32
}
type startHeader struct {
Offset uint64
Size uint64
CRC uint32
}
type packInfo struct {
position uint64
streams uint64
size []uint64
digest []uint32
}
type coder struct {
id []byte
in, out uint64
properties []byte
}
type bindPair struct {
in, out uint64
}
type folder struct {
in, out uint64
packedStreams uint64
coder []*coder
bindPair []*bindPair
size []uint64
packed []uint64
}
func (f *folder) findInBindPair(i uint64) *bindPair {
for _, v := range f.bindPair {
if v.in == i {
return v
}
}
return nil
}
func (f *folder) findOutBindPair(i uint64) *bindPair {
for _, v := range f.bindPair {
if v.out == i {
return v
}
}
return nil
}
func (f *folder) coderReader(readers []io.ReadCloser, coder uint64, password string) (io.ReadCloser, bool, error) {
dcomp := decompressor(f.coder[coder].id)
if dcomp == nil {
return nil, false, errAlgorithm
}
cr, err := dcomp(f.coder[coder].properties, f.size[coder], readers)
if err != nil {
return nil, false, err
}
crc, ok := cr.(CryptoReadCloser)
if ok {
if err = crc.Password(password); err != nil {
return nil, true, fmt.Errorf("sevenzip: error setting password: %w", err)
}
}
return plumbing.LimitReadCloser(cr, int64(f.size[coder])), ok, nil //nolint:gosec
}
type folderReadCloser struct {
io.ReadCloser
h hash.Hash
wc *plumbing.WriteCounter
size int64
hasEncryption bool
}
func (rc *folderReadCloser) Checksum() []byte {
return rc.h.Sum(nil)
}
func (rc *folderReadCloser) Seek(offset int64, whence int) (int64, error) {
var newo int64
switch whence {
case io.SeekStart:
newo = offset
case io.SeekCurrent:
newo = int64(rc.wc.Count()) + offset //nolint:gosec
case io.SeekEnd:
newo = rc.Size() + offset
default:
return 0, errInvalidWhence
}
if newo < 0 {
return 0, errNegativeSeek
}
if uint64(newo) < rc.wc.Count() {
return 0, errSeekBackwards
}
if newo > rc.Size() {
return 0, errSeekEOF
}
if _, err := io.CopyN(io.Discard, rc, newo-int64(rc.wc.Count())); err != nil { //nolint:gosec
return 0, fmt.Errorf("sevenzip: error seeking: %w", err)
}
return newo, nil
}
func (rc *folderReadCloser) Size() int64 {
return rc.size
}
func newFolderReadCloser(rc io.ReadCloser, size int64, hasEncryption bool) *folderReadCloser {
nrc := new(folderReadCloser)
nrc.h = crc32.NewIEEE()
nrc.wc = new(plumbing.WriteCounter)
nrc.ReadCloser = plumbing.TeeReadCloser(rc, io.MultiWriter(nrc.h, nrc.wc))
nrc.size = size
nrc.hasEncryption = hasEncryption
return nrc
}
func (f *folder) unpackSize() uint64 {
if len(f.size) == 0 {
return 0
}
for i := len(f.size) - 1; i >= 0; i-- {
if f.findOutBindPair(uint64(i)) == nil {
return f.size[i]
}
}
return f.size[len(f.size)-1]
}
type unpackInfo struct {
folder []*folder
digest []uint32
}
type subStreamsInfo struct {
streams []uint64
size []uint64
digest []uint32
}
type streamsInfo struct {
packInfo *packInfo
unpackInfo *unpackInfo
subStreamsInfo *subStreamsInfo
}
func (si *streamsInfo) Folders() int {
if si != nil && si.unpackInfo != nil {
return len(si.unpackInfo.folder)
}
return 0
}
func (si *streamsInfo) FileFolderAndSize(file int) (int, uint64, uint32) {
var (
folder int
streams uint64 = 1
crc uint32
)
if si.subStreamsInfo != nil {
total := uint64(0)
for folder, streams = range si.subStreamsInfo.streams {
total += streams
if uint64(file) < total { //nolint:gosec
break
}
}
if len(si.subStreamsInfo.digest) > 0 {
crc = si.subStreamsInfo.digest[file]
}
}
if streams == 1 {
if len(si.unpackInfo.digest) > 0 {
crc = si.unpackInfo.digest[folder]
}
return folder, si.unpackInfo.folder[folder].size[len(si.unpackInfo.folder[folder].coder)-1], crc
}
return folder, si.subStreamsInfo.size[file], crc
}
func (si *streamsInfo) folderOffset(folder int) int64 {
offset := uint64(0)
for i, k := 0, uint64(0); i < folder; i++ {
for j := k; j < k+si.unpackInfo.folder[i].packedStreams; j++ {
offset += si.packInfo.size[j]
}
k += si.unpackInfo.folder[i].packedStreams
}
return int64(si.packInfo.position + offset) //nolint:gosec
}
//nolint:cyclop,funlen,lll
func (si *streamsInfo) FolderReader(r io.ReaderAt, folder int, password string) (*folderReadCloser, uint32, bool, error) {
f := si.unpackInfo.folder[folder]
in := make([]io.ReadCloser, f.in)
out := make([]io.ReadCloser, f.out)
packedOffset := 0
for i := 0; i < folder; i++ {
packedOffset += len(si.unpackInfo.folder[i].packed)
}
offset := int64(0)
for i, input := range f.packed {
size := int64(si.packInfo.size[packedOffset+i]) //nolint:gosec
in[input] = util.NopCloser(bufio.NewReader(io.NewSectionReader(r, si.folderOffset(folder)+offset, size)))
offset += size
}
var (
hasEncryption bool
input, output uint64
)
for i, c := range f.coder {
if c.out != 1 {
return nil, 0, hasEncryption, errMultipleOutputStreams
}
for j := input; j < input+c.in; j++ {
if in[j] != nil {
continue
}
bp := f.findInBindPair(j)
if bp == nil || out[bp.out] == nil {
return nil, 0, hasEncryption, errNoBoundStream
}
in[j] = out[bp.out]
}
var (
isEncrypted bool
err error
)
out[output], isEncrypted, err = f.coderReader(in[input:input+c.in], uint64(i), password) //nolint:gosec
if err != nil {
return nil, 0, hasEncryption, err
}
if isEncrypted {
hasEncryption = true
}
input += c.in
output += c.out
}
unbound := make([]uint64, 0, f.out)
for i := uint64(0); i < f.out; i++ {
if bp := f.findOutBindPair(i); bp == nil {
unbound = append(unbound, i)
}
}
if len(unbound) != 1 || out[unbound[0]] == nil {
return nil, 0, hasEncryption, errNoUnboundStream
}
fr := newFolderReadCloser(out[unbound[0]], int64(f.unpackSize()), hasEncryption) //nolint:gosec
if si.unpackInfo.digest != nil {
return fr, si.unpackInfo.digest[folder], hasEncryption, nil
}
return fr, 0, hasEncryption, nil
}
type filesInfo struct {
file []FileHeader
}
type header struct {
streamsInfo *streamsInfo
filesInfo *filesInfo
}
// FileHeader describes a file within a 7-zip file.
type FileHeader struct {
Name string
Created time.Time
Accessed time.Time
Modified time.Time
Attributes uint32
CRC32 uint32
UncompressedSize uint64
// Stream is an opaque identifier representing the compressed stream
// that contains the file. Any File with the same value can be assumed
// to be stored within the same stream.
Stream int
isEmptyStream bool
isEmptyFile bool
}
// FileInfo returns an [fs.FileInfo] for the FileHeader.
func (h *FileHeader) FileInfo() iofs.FileInfo {
return headerFileInfo{h}
}
type headerFileInfo struct {
fh *FileHeader
}
func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) }
func (fi headerFileInfo) Size() int64 { return int64(fi.fh.UncompressedSize) } //nolint:gosec
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
func (fi headerFileInfo) ModTime() time.Time { return fi.fh.Modified.UTC() }
func (fi headerFileInfo) Mode() iofs.FileMode { return fi.fh.Mode() }
func (fi headerFileInfo) Type() iofs.FileMode { return fi.fh.Mode().Type() }
func (fi headerFileInfo) Sys() interface{} { return fi.fh }
func (fi headerFileInfo) Info() (iofs.FileInfo, error) { return fi, nil }
const (
// Unix constants. The specification doesn't mention them,
// but these seem to be the values agreed on by tools.
sIFMT = 0xf000
sIFSOCK = 0xc000
sIFLNK = 0xa000
sIFREG = 0x8000
sIFBLK = 0x6000
sIFDIR = 0x4000
sIFCHR = 0x2000
sIFIFO = 0x1000
sISUID = 0x800
sISGID = 0x400
sISVTX = 0x200
msdosDir = 0x10
msdosReadOnly = 0x01
)
// Mode returns the permission and mode bits for the FileHeader.
func (h *FileHeader) Mode() (mode iofs.FileMode) {
// Prefer the POSIX attributes if they're present
if h.Attributes&0xf0000000 != 0 {
mode = unixModeToFileMode(h.Attributes >> 16)
} else {
mode = msdosModeToFileMode(h.Attributes)
}
return
}
func msdosModeToFileMode(m uint32) (mode iofs.FileMode) {
if m&msdosDir != 0 {
mode = iofs.ModeDir | 0o777
} else {
mode = 0o666
}
if m&msdosReadOnly != 0 {
mode &^= 0o222
}
return mode
}
//nolint:cyclop
func unixModeToFileMode(m uint32) iofs.FileMode {
mode := iofs.FileMode(m & 0o777)
switch m & sIFMT {
case sIFBLK:
mode |= iofs.ModeDevice
case sIFCHR:
mode |= iofs.ModeDevice | iofs.ModeCharDevice
case sIFDIR:
mode |= iofs.ModeDir
case sIFIFO:
mode |= iofs.ModeNamedPipe
case sIFLNK:
mode |= iofs.ModeSymlink
case sIFREG:
// nothing to do
case sIFSOCK:
mode |= iofs.ModeSocket
}
if m&sISGID != 0 {
mode |= iofs.ModeSetgid
}
if m&sISUID != 0 {
mode |= iofs.ModeSetuid
}
if m&sISVTX != 0 {
mode |= iofs.ModeSticky
}
return mode
}
+876
View File
@@ -0,0 +1,876 @@
package sevenzip
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/bits"
"time"
"bookhoard/internal/sevenzip/internal/util"
"github.com/bodgit/windows"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
const (
idEnd = iota
idHeader
idArchiveProperties
idAdditionalStreamsInfo
idMainStreamsInfo
idFilesInfo
idPackInfo
idUnpackInfo
idSubStreamsInfo
idSize
idCRC
idFolder
idCodersUnpackSize
idNumUnpackStream
idEmptyStream
idEmptyFile
idAnti //nolint:deadcode,varcheck
idName
idCTime
idATime
idMTime
idWinAttributes
idComment //nolint:deadcode,varcheck
idEncodedHeader
idStartPos
idDummy
)
var (
errIncompleteRead = errors.New("sevenzip: incomplete read")
errUnexpectedID = errors.New("sevenzip: unexpected id")
errMissingUnpackInfo = errors.New("sevenzip: missing unpack info")
errWrongNumberOfFilenames = errors.New("sevenzip: wrong number of filenames")
)
func readUint64(r io.ByteReader) (uint64, error) {
b, err := r.ReadByte()
if err != nil {
return 0, fmt.Errorf("readUint64: ReadByte error: %w", err)
}
l := bits.LeadingZeros8(^b)
var v uint64
if l < 7 {
v |= uint64(b&((1<<(8-l))-1)) << (8 * l)
}
for i := 0; i < l; i++ {
b, err := r.ReadByte()
if err != nil {
return 0, fmt.Errorf("readUint64: ReadByte error: %w", err)
}
v |= uint64(b) << (8 * i)
}
return v, nil
}
func readBool(r io.ByteReader, count uint64) ([]bool, error) {
defined := make([]bool, count)
var b, mask byte
for i := range defined {
if mask == 0 {
var err error
b, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readBool: ReadByte error: %w", err)
}
mask = 0x80
}
defined[i] = (b & mask) != 0
mask >>= 1
}
return defined, nil
}
func readOptionalBool(r io.ByteReader, count uint64) ([]bool, error) {
all, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readOptionalBool: ReadByte error: %w", err)
}
if all == 0 {
return readBool(r, count)
}
defined := make([]bool, count)
for i := range defined {
defined[i] = true
}
return defined, nil
}
func readSizes(r io.ByteReader, count uint64) ([]uint64, error) {
sizes := make([]uint64, count)
for i := uint64(0); i < count; i++ {
size, err := readUint64(r)
if err != nil {
return nil, err
}
sizes[i] = size
}
return sizes, nil
}
func readCRC(r util.Reader, count uint64) ([]uint32, error) {
defined, err := readOptionalBool(r, count)
if err != nil {
return nil, err
}
crcs := make([]uint32, count)
for i := range defined {
if defined[i] {
if err := binary.Read(r, binary.LittleEndian, &crcs[i]); err != nil {
return nil, fmt.Errorf("readCRC: Read error: %w", err)
}
}
}
return crcs, nil
}
//nolint:cyclop
func readPackInfo(r util.Reader) (*packInfo, error) {
p := new(packInfo)
var err error
p.position, err = readUint64(r)
if err != nil {
return nil, err
}
p.streams, err = readUint64(r)
if err != nil {
return nil, err
}
id, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
}
if id == idSize {
if p.size, err = readSizes(r, p.streams); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
}
}
if id == idCRC {
if p.digest, err = readCRC(r, p.streams); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
}
}
if id != idEnd {
return nil, errUnexpectedID
}
return p, nil
}
//nolint:cyclop
func readCoder(r util.Reader) (*coder, error) {
c := new(coder)
v, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readCoder: ReadByte error: %w", err)
}
c.id = make([]byte, v&0xf)
if n, err := r.Read(c.id); err != nil || n != int(v&0xf) {
if err != nil {
return nil, fmt.Errorf("readCoder: Read error: %w", err)
}
return nil, errIncompleteRead
}
if v&0x10 != 0 {
c.in, err = readUint64(r)
if err != nil {
return nil, err
}
c.out, err = readUint64(r)
if err != nil {
return nil, err
}
} else {
c.in, c.out = 1, 1
}
if v&0x20 != 0 {
size, err := readUint64(r)
if err != nil {
return nil, err
}
c.properties = make([]byte, size)
if n, err := r.Read(c.properties); err != nil || uint64(n) != size { //nolint:gosec
if err != nil {
return nil, fmt.Errorf("readCoder: Read error: %w", err)
}
return nil, errIncompleteRead
}
}
return c, nil
}
//nolint:cyclop
func readFolder(r util.Reader) (*folder, error) {
f := new(folder)
coders, err := readUint64(r)
if err != nil {
return nil, err
}
f.coder = make([]*coder, coders)
for i := uint64(0); i < coders; i++ {
if f.coder[i], err = readCoder(r); err != nil {
return nil, err
}
f.in += f.coder[i].in
f.out += f.coder[i].out
}
bindPairs := f.out - 1
f.bindPair = make([]*bindPair, bindPairs)
for i := uint64(0); i < bindPairs; i++ {
in, err := readUint64(r)
if err != nil {
return nil, err
}
out, err := readUint64(r)
if err != nil {
return nil, err
}
f.bindPair[i] = &bindPair{
in: in,
out: out,
}
}
f.packedStreams = f.in - bindPairs
if f.packedStreams == 1 {
f.packed = []uint64{}
for i := uint64(0); i < f.in; i++ {
if f.findInBindPair(i) == nil {
f.packed = append(f.packed, i)
}
}
} else {
f.packed = make([]uint64, f.packedStreams)
for i := uint64(0); i < f.packedStreams; i++ {
if f.packed[i], err = readUint64(r); err != nil {
return nil, err
}
}
}
return f, nil
}
//nolint:cyclop,funlen
func readUnpackInfo(r util.Reader) (*unpackInfo, error) {
u := new(unpackInfo)
if id, err := r.ReadByte(); err != nil || id != idFolder {
if err != nil {
return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
}
return nil, errUnexpectedID
}
folders, err := readUint64(r)
if err != nil {
return nil, err
}
external, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
}
if external > 0 {
/*
_, err := readUint64(r)
if err != nil {
return nil, err
}
*/
// TODO Apparently we seek to this read offset and read the
// folder information from there. Not clear if the offset is
// absolute for the whole file, or relative to some known
// position in the file. Cowardly waiting for an example
return nil, errors.New("sevenzip: TODO readUnpackInfo external") //nolint:goerr113
}
u.folder = make([]*folder, folders)
for i := uint64(0); i < folders; i++ {
if u.folder[i], err = readFolder(r); err != nil {
return nil, err
}
}
if id, err := r.ReadByte(); err != nil || id != idCodersUnpackSize {
if err != nil {
return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
}
return nil, errUnexpectedID
}
for _, f := range u.folder {
total := uint64(0)
for _, c := range f.coder {
total += c.out
}
f.size = make([]uint64, total)
for i := range f.size {
if f.size[i], err = readUint64(r); err != nil {
return nil, err
}
}
}
id, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
}
if id == idCRC {
if u.digest, err = readCRC(r, folders); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
}
}
if id != idEnd {
return nil, errUnexpectedID
}
return u, nil
}
//nolint:cyclop,funlen
func readSubStreamsInfo(r util.Reader, folder []*folder) (*subStreamsInfo, error) {
s := new(subStreamsInfo)
id, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
}
s.streams = make([]uint64, len(folder))
if id == idNumUnpackStream {
for i := range s.streams {
if s.streams[i], err = readUint64(r); err != nil {
return nil, err
}
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
}
} else {
for i := range s.streams {
s.streams[i] = 1
}
}
// Count the files in each stream
files := uint64(0)
for _, v := range s.streams {
files += v
}
if id == idSize {
s.size = make([]uint64, files)
k := 0
for i := range s.streams {
total := uint64(0)
for j := uint64(1); j < s.streams[i]; j++ {
if s.size[k], err = readUint64(r); err != nil {
return nil, err
}
total += s.size[k]
k++
}
s.size[k] = folder[i].unpackSize() - total
k++
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
}
}
if id == idCRC {
if s.digest, err = readCRC(r, files); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
}
}
if id != idEnd {
return nil, errUnexpectedID
}
return s, nil
}
//nolint:cyclop
func readStreamsInfo(r util.Reader) (*streamsInfo, error) {
s := new(streamsInfo)
id, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
}
if id == idPackInfo {
if s.packInfo, err = readPackInfo(r); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
}
}
if id == idUnpackInfo {
if s.unpackInfo, err = readUnpackInfo(r); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
}
}
if id == idSubStreamsInfo {
if s.unpackInfo == nil {
return nil, errMissingUnpackInfo
}
if s.subStreamsInfo, err = readSubStreamsInfo(r, s.unpackInfo.folder); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
}
}
if id != idEnd {
return nil, errUnexpectedID
}
return s, nil
}
func readTimes(r util.Reader, count uint64) ([]time.Time, error) {
defined, err := readOptionalBool(r, count)
if err != nil {
return nil, err
}
external, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readTimes: ReadByte error: %w", err)
}
if external > 0 {
/*
_, err := readUint64(r)
if err != nil {
return nil, err
}
*/
// TODO Apparently we seek to this read offset and read the
// folder information from there. Not clear if the offset is
// absolute for the whole file, or relative to some known
// position in the file. Cowardly waiting for an example
return nil, errors.New("sevenzip: TODO readTimes external") //nolint:goerr113
}
times := make([]time.Time, count)
for i := range defined {
if defined[i] {
var ft windows.Filetime
if err := binary.Read(r, binary.LittleEndian, &ft); err != nil {
return nil, fmt.Errorf("readTimes: Read error: %w", err)
}
times[i] = time.Unix(0, ft.Nanoseconds()).UTC()
}
}
return times, nil
}
func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexRune(data, rune(0)); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return
}
func readNames(r util.Reader, count, length uint64) ([]string, error) {
external, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readNames: ReadByte error: %w", err)
}
if external > 0 {
/*
_, err := readUint64(r)
if err != nil {
return nil, err
}
*/
// TODO Apparently we seek to this read offset and read the
// folder information from there. Not clear if the offset is
// absolute for the whole file, or relative to some known
// position in the file. Cowardly waiting for an example
return nil, errors.New("sevenzip: TODO readNames external") //nolint:goerr113
}
utf16le := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
scanner := bufio.NewScanner(transform.NewReader(io.LimitReader(r, int64(length-1)), utf16le.NewDecoder())) //nolint:gosec,lll
scanner.Split(splitNull)
names, i := make([]string, 0, count), uint64(0)
for scanner.Scan() {
names = append(names, scanner.Text())
i++
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("readNames: Scan error: %w", err)
}
if i != count {
return nil, errWrongNumberOfFilenames
}
return names, nil
}
func readAttributes(r util.Reader, count uint64) ([]uint32, error) {
defined, err := readOptionalBool(r, count)
if err != nil {
return nil, err
}
external, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readAttributes: ReadByte error: %w", err)
}
if external > 0 {
/*
_, err := readUint64(r)
if err != nil {
return nil, err
}
*/
// TODO Apparently we seek to this read offset and read the
// folder information from there. Not clear if the offset is
// absolute for the whole file, or relative to some known
// position in the file. Cowardly waiting for an example
return nil, errors.New("sevenzip: TODO readAttributes external") //nolint:goerr113
}
attributes := make([]uint32, count)
for i := range defined {
if defined[i] {
if err := binary.Read(r, binary.LittleEndian, &attributes[i]); err != nil {
return nil, fmt.Errorf("readAttributes: Read error: %w", err)
}
}
}
return attributes, nil
}
//nolint:cyclop,funlen,gocognit,gocyclo
func readFilesInfo(r util.Reader) (*filesInfo, error) {
f := new(filesInfo)
files, err := readUint64(r)
if err != nil {
return nil, err
}
f.file = make([]FileHeader, files)
var emptyStreams uint64
for {
property, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readFilesInfo: ReadByte error: %w", err)
}
if property == idEnd {
break
}
length, err := readUint64(r)
if err != nil {
return nil, err
}
switch property {
case idEmptyStream:
empty, err := readBool(r, files)
if err != nil {
return nil, err
}
for i := range f.file {
f.file[i].isEmptyStream = empty[i]
if empty[i] {
emptyStreams++
}
}
case idEmptyFile:
empty, err := readBool(r, emptyStreams)
if err != nil {
return nil, err
}
j := 0
for i := range f.file {
if f.file[i].isEmptyStream {
f.file[i].isEmptyFile = empty[j]
j++
}
}
case idCTime:
times, err := readTimes(r, files)
if err != nil {
return nil, err
}
for i, t := range times {
f.file[i].Created = t
}
case idATime:
times, err := readTimes(r, files)
if err != nil {
return nil, err
}
for i, t := range times {
f.file[i].Accessed = t
}
case idMTime:
times, err := readTimes(r, files)
if err != nil {
return nil, err
}
for i, t := range times {
f.file[i].Modified = t
}
case idName:
names, err := readNames(r, files, length)
if err != nil {
return nil, err
}
for i, n := range names {
f.file[i].Name = n
}
case idWinAttributes:
attributes, err := readAttributes(r, files)
if err != nil {
return nil, err
}
for i, a := range attributes {
f.file[i].Attributes = a
}
case idStartPos:
return nil, errors.New("sevenzip: TODO idStartPos") //nolint:goerr113
case idDummy:
if _, err := io.CopyN(io.Discard, r, int64(length)); err != nil { //nolint:gosec
return nil, fmt.Errorf("readFilesInfo: CopyN error: %w", err)
}
default:
return nil, errUnexpectedID
}
}
return f, nil
}
//nolint:cyclop,funlen
func readHeader(r util.Reader) (*header, error) {
h := new(header)
id, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
}
if id == idArchiveProperties {
/*
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
}
*/
return nil, errors.New("sevenzip: TODO idArchiveProperties") //nolint:goerr113
}
if id == idAdditionalStreamsInfo {
/*
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
}
*/
return nil, errors.New("sevenzip: TODO idAdditionalStreamsInfo") //nolint:goerr113
}
if id == idMainStreamsInfo {
if h.streamsInfo, err = readStreamsInfo(r); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
}
}
if id == idFilesInfo {
if h.filesInfo, err = readFilesInfo(r); err != nil {
return nil, err
}
id, err = r.ReadByte()
if err != nil {
return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
}
}
if id != idEnd {
return nil, errUnexpectedID
}
if h.streamsInfo == nil || h.filesInfo == nil {
return h, nil
}
j := 0
for i := range h.filesInfo.file {
if h.filesInfo.file[i].isEmptyStream {
continue
}
_, h.filesInfo.file[i].UncompressedSize, h.filesInfo.file[i].CRC32 = h.streamsInfo.FileFolderAndSize(j)
j++
}
return h, nil
}
func readEncodedHeader(r util.Reader) (*header, error) {
if id, err := r.ReadByte(); err != nil || id != idHeader {
if err != nil {
return nil, fmt.Errorf("readEncodedHeader: ReadByte error: %w", err)
}
return nil, errUnexpectedID
}
header, err := readHeader(r)
if err != nil {
return nil, err
}
return header, nil
}