From 46ae45ee924d2553658b90052d6dd4f243747049 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 22 Feb 2026 16:29:21 -0500 Subject: [PATCH] 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. --- internal/sevenzip/LICENSE | 29 + internal/sevenzip/internal/aes7z/key.go | 69 ++ internal/sevenzip/internal/aes7z/reader.go | 126 +++ internal/sevenzip/internal/bcj2/reader.go | 232 +++++ internal/sevenzip/internal/bra/arm.go | 55 ++ internal/sevenzip/internal/bra/bcj.go | 104 +++ internal/sevenzip/internal/bra/bra.go | 7 + internal/sevenzip/internal/bra/ppc.go | 48 + internal/sevenzip/internal/bra/reader.go | 72 ++ internal/sevenzip/internal/bra/sparc.go | 53 ++ internal/sevenzip/internal/brotli/reader.go | 113 +++ internal/sevenzip/internal/bzip2/reader.go | 58 ++ internal/sevenzip/internal/deflate/reader.go | 77 ++ internal/sevenzip/internal/delta/reader.go | 89 ++ internal/sevenzip/internal/lz4/reader.go | 71 ++ internal/sevenzip/internal/lzma/reader.go | 112 +++ internal/sevenzip/internal/lzma2/reader.go | 77 ++ internal/sevenzip/internal/pool/pool.go | 138 +++ internal/sevenzip/internal/util/checksum.go | 8 + internal/sevenzip/internal/util/reader.go | 68 ++ internal/sevenzip/internal/zstd/reader.go | 80 ++ internal/sevenzip/multireaderat.go | 87 ++ internal/sevenzip/reader.go | 855 ++++++++++++++++++ internal/sevenzip/register.go | 92 ++ internal/sevenzip/struct.go | 467 ++++++++++ internal/sevenzip/types.go | 876 +++++++++++++++++++ 26 files changed, 4063 insertions(+) create mode 100644 internal/sevenzip/LICENSE create mode 100644 internal/sevenzip/internal/aes7z/key.go create mode 100644 internal/sevenzip/internal/aes7z/reader.go create mode 100644 internal/sevenzip/internal/bcj2/reader.go create mode 100644 internal/sevenzip/internal/bra/arm.go create mode 100644 internal/sevenzip/internal/bra/bcj.go create mode 100644 internal/sevenzip/internal/bra/bra.go create mode 100644 internal/sevenzip/internal/bra/ppc.go create mode 100644 internal/sevenzip/internal/bra/reader.go create mode 100644 internal/sevenzip/internal/bra/sparc.go create mode 100644 internal/sevenzip/internal/brotli/reader.go create mode 100644 internal/sevenzip/internal/bzip2/reader.go create mode 100644 internal/sevenzip/internal/deflate/reader.go create mode 100644 internal/sevenzip/internal/delta/reader.go create mode 100644 internal/sevenzip/internal/lz4/reader.go create mode 100644 internal/sevenzip/internal/lzma/reader.go create mode 100644 internal/sevenzip/internal/lzma2/reader.go create mode 100644 internal/sevenzip/internal/pool/pool.go create mode 100644 internal/sevenzip/internal/util/checksum.go create mode 100644 internal/sevenzip/internal/util/reader.go create mode 100644 internal/sevenzip/internal/zstd/reader.go create mode 100644 internal/sevenzip/multireaderat.go create mode 100644 internal/sevenzip/reader.go create mode 100644 internal/sevenzip/register.go create mode 100644 internal/sevenzip/struct.go create mode 100644 internal/sevenzip/types.go diff --git a/internal/sevenzip/LICENSE b/internal/sevenzip/LICENSE new file mode 100644 index 0000000..5a19a8f --- /dev/null +++ b/internal/sevenzip/LICENSE @@ -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. diff --git a/internal/sevenzip/internal/aes7z/key.go b/internal/sevenzip/internal/aes7z/key.go new file mode 100644 index 0000000..51a5795 --- /dev/null +++ b/internal/sevenzip/internal/aes7z/key.go @@ -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<>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 +} diff --git a/internal/sevenzip/internal/bcj2/reader.go b/internal/sevenzip/internal/bcj2/reader.go new file mode 100644 index 0000000..bff3475 --- /dev/null +++ b/internal/sevenzip/internal/bcj2/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/bra/arm.go b/internal/sevenzip/internal/bra/arm.go new file mode 100644 index 0000000..3916a0c --- /dev/null +++ b/internal/sevenzip/internal/bra/arm.go @@ -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)) +} diff --git a/internal/sevenzip/internal/bra/bcj.go b/internal/sevenzip/internal/bra/bcj.go new file mode 100644 index 0000000..05f1fdf --- /dev/null +++ b/internal/sevenzip/internal/bra/bcj.go @@ -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)) +} diff --git a/internal/sevenzip/internal/bra/bra.go b/internal/sevenzip/internal/bra/bra.go new file mode 100644 index 0000000..a7a77d7 --- /dev/null +++ b/internal/sevenzip/internal/bra/bra.go @@ -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 +} diff --git a/internal/sevenzip/internal/bra/ppc.go b/internal/sevenzip/internal/bra/ppc.go new file mode 100644 index 0000000..9d38243 --- /dev/null +++ b/internal/sevenzip/internal/bra/ppc.go @@ -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)) +} diff --git a/internal/sevenzip/internal/bra/reader.go b/internal/sevenzip/internal/bra/reader.go new file mode 100644 index 0000000..733333b --- /dev/null +++ b/internal/sevenzip/internal/bra/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/bra/sparc.go b/internal/sevenzip/internal/bra/sparc.go new file mode 100644 index 0000000..8aa4553 --- /dev/null +++ b/internal/sevenzip/internal/bra/sparc.go @@ -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)) +} diff --git a/internal/sevenzip/internal/brotli/reader.go b/internal/sevenzip/internal/brotli/reader.go new file mode 100644 index 0000000..a07b764 --- /dev/null +++ b/internal/sevenzip/internal/brotli/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/bzip2/reader.go b/internal/sevenzip/internal/bzip2/reader.go new file mode 100644 index 0000000..3e82498 --- /dev/null +++ b/internal/sevenzip/internal/bzip2/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/deflate/reader.go b/internal/sevenzip/internal/deflate/reader.go new file mode 100644 index 0000000..da815dd --- /dev/null +++ b/internal/sevenzip/internal/deflate/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/delta/reader.go b/internal/sevenzip/internal/delta/reader.go new file mode 100644 index 0000000..0926a17 --- /dev/null +++ b/internal/sevenzip/internal/delta/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/lz4/reader.go b/internal/sevenzip/internal/lz4/reader.go new file mode 100644 index 0000000..4299334 --- /dev/null +++ b/internal/sevenzip/internal/lz4/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/lzma/reader.go b/internal/sevenzip/internal/lzma/reader.go new file mode 100644 index 0000000..b8d277c --- /dev/null +++ b/internal/sevenzip/internal/lzma/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/lzma2/reader.go b/internal/sevenzip/internal/lzma2/reader.go new file mode 100644 index 0000000..3f2e7be --- /dev/null +++ b/internal/sevenzip/internal/lzma2/reader.go @@ -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 +} diff --git a/internal/sevenzip/internal/pool/pool.go b/internal/sevenzip/internal/pool/pool.go new file mode 100644 index 0000000..4764b11 --- /dev/null +++ b/internal/sevenzip/internal/pool/pool.go @@ -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 +} diff --git a/internal/sevenzip/internal/util/checksum.go b/internal/sevenzip/internal/util/checksum.go new file mode 100644 index 0000000..978380c --- /dev/null +++ b/internal/sevenzip/internal/util/checksum.go @@ -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)}) +} diff --git a/internal/sevenzip/internal/util/reader.go b/internal/sevenzip/internal/util/reader.go new file mode 100644 index 0000000..21d2c94 --- /dev/null +++ b/internal/sevenzip/internal/util/reader.go @@ -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} +} diff --git a/internal/sevenzip/internal/zstd/reader.go b/internal/sevenzip/internal/zstd/reader.go new file mode 100644 index 0000000..6817f1a --- /dev/null +++ b/internal/sevenzip/internal/zstd/reader.go @@ -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 +} diff --git a/internal/sevenzip/multireaderat.go b/internal/sevenzip/multireaderat.go new file mode 100644 index 0000000..8a522ce --- /dev/null +++ b/internal/sevenzip/multireaderat.go @@ -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 +} diff --git a/internal/sevenzip/reader.go b/internal/sevenzip/reader.go new file mode 100644 index 0000000..651ac7b --- /dev/null +++ b/internal/sevenzip/reader.go @@ -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 +} diff --git a/internal/sevenzip/register.go b/internal/sevenzip/register.go new file mode 100644 index 0000000..36e68a9 --- /dev/null +++ b/internal/sevenzip/register.go @@ -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 +} diff --git a/internal/sevenzip/struct.go b/internal/sevenzip/struct.go new file mode 100644 index 0000000..ebc3e46 --- /dev/null +++ b/internal/sevenzip/struct.go @@ -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 +} diff --git a/internal/sevenzip/types.go b/internal/sevenzip/types.go new file mode 100644 index 0000000..80746ca --- /dev/null +++ b/internal/sevenzip/types.go @@ -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 +}