Files
john-okeefe 46ae45ee92 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.
2026-02-22 16:29:21 -05:00

877 lines
17 KiB
Go

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
}