7z keeps its file list at the end of the archive, so the 7z-tail candidates (the last segment of the highest volume) were queued but never probed, and the .7z.001 start only gave the type. The prober now claims 7z-tail candidates after the archives; lib/sevenzip finds the end header in the article, decodes the LZMA-packed header (a TypeScript port of the LZMA SDK's reference decoder) and lists the files with their sizes. An AES-encrypted header means passworded without names. Names feed the inner name and the judge's fake rule. An unsplit .7z of several segments now also gets a tail candidate, and .zip files an archive candidate: their first segment's local headers give the first entries and whether they are encrypted. Both in the Rust edge and the TypeScript indexer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
7.0 KiB
TypeScript
222 lines
7.0 KiB
TypeScript
/**
|
|
* An LZMA decoder (raw stream, as a 7z coder stores it: the properties apart, the unpacked size known), after the
|
|
* LZMA SDK's reference decoder (LzmaSpec.cpp). For 7z headers: those are small, so the whole output is one buffer.
|
|
*/
|
|
|
|
const MODEL_BITS = 11
|
|
const BIT_MODEL_TOTAL = 1 << MODEL_BITS
|
|
const MOVE_BITS = 5
|
|
const TOP = 1 << 24
|
|
const POS_BITS_MAX = 4
|
|
const STATES = 12
|
|
const LEN_TO_POS_STATES = 4
|
|
const ALIGN_BITS = 4
|
|
const END_POS_MODEL_INDEX = 14
|
|
const FULL_DISTANCES = 1 << (END_POS_MODEL_INDEX >> 1)
|
|
const MATCH_MIN_LEN = 2
|
|
|
|
export class LzmaError extends Error {}
|
|
|
|
const probs = (n: number) => new Uint16Array(n).fill(BIT_MODEL_TOTAL >>> 1)
|
|
|
|
class RangeDecoder {
|
|
private range = 0xffffffff
|
|
private code = 0
|
|
|
|
constructor(
|
|
private input: Uint8Array,
|
|
private pos: number
|
|
) {
|
|
if (this.byte() !== 0) throw new LzmaError('LZMA: bad stream start')
|
|
for (let i = 0; i < 4; i++) this.code = ((this.code << 8) | this.byte()) >>> 0
|
|
if (this.code === this.range) throw new LzmaError('LZMA: corrupt stream')
|
|
}
|
|
|
|
private byte(): number {
|
|
if (this.pos >= this.input.length) throw new LzmaError('LZMA: input ended')
|
|
return this.input[this.pos++]
|
|
}
|
|
|
|
private normalize() {
|
|
if (this.range < TOP) {
|
|
this.range = (this.range << 8) >>> 0
|
|
this.code = ((this.code << 8) | this.byte()) >>> 0
|
|
}
|
|
}
|
|
|
|
direct(bits: number): number {
|
|
let result = 0
|
|
do {
|
|
this.range >>>= 1
|
|
this.code = (this.code - this.range) >>> 0
|
|
const t = this.code >>> 31 ? 0xffffffff : 0
|
|
this.code = (this.code + (this.range & t)) >>> 0
|
|
this.normalize()
|
|
result = ((result << 1) | (t ? 0 : 1)) >>> 0
|
|
} while (--bits)
|
|
return result
|
|
}
|
|
|
|
bit(p: Uint16Array, i: number): number {
|
|
const v = p[i]
|
|
const bound = ((this.range >>> MODEL_BITS) * v) >>> 0
|
|
let symbol: number
|
|
if (this.code < bound) {
|
|
p[i] = v + ((BIT_MODEL_TOTAL - v) >>> MOVE_BITS)
|
|
this.range = bound
|
|
symbol = 0
|
|
} else {
|
|
p[i] = v - (v >>> MOVE_BITS)
|
|
this.code = (this.code - bound) >>> 0
|
|
this.range = (this.range - bound) >>> 0
|
|
symbol = 1
|
|
}
|
|
this.normalize()
|
|
return symbol
|
|
}
|
|
|
|
tree(p: Uint16Array, offset: number, bits: number): number {
|
|
let m = 1
|
|
for (let i = 0; i < bits; i++) m = (m << 1) + this.bit(p, offset + m)
|
|
return m - (1 << bits)
|
|
}
|
|
|
|
reverseTree(p: Uint16Array, offset: number, bits: number): number {
|
|
let m = 1
|
|
let symbol = 0
|
|
for (let i = 0; i < bits; i++) {
|
|
const bit = this.bit(p, offset + m)
|
|
m = (m << 1) + bit
|
|
symbol |= bit << i
|
|
}
|
|
return symbol
|
|
}
|
|
}
|
|
|
|
class LenDecoder {
|
|
private choice = probs(2)
|
|
private low = probs((1 << POS_BITS_MAX) << 3)
|
|
private mid = probs((1 << POS_BITS_MAX) << 3)
|
|
private high = probs(1 << 8)
|
|
|
|
decode(rc: RangeDecoder, posState: number): number {
|
|
if (rc.bit(this.choice, 0) === 0) return rc.tree(this.low, posState << 3, 3)
|
|
if (rc.bit(this.choice, 1) === 0) return 8 + rc.tree(this.mid, posState << 3, 3)
|
|
return 16 + rc.tree(this.high, 0, 8)
|
|
}
|
|
}
|
|
|
|
/** Decodes `size` bytes of a raw LZMA stream with its 5 property bytes (lc/lp/pb, dictionary size). */
|
|
export function lzmaDecode(properties: Uint8Array, input: Uint8Array, size: number): Buffer {
|
|
if (properties.length < 5) throw new LzmaError('LZMA: short properties')
|
|
let d = properties[0]
|
|
if (d >= 9 * 5 * 5) throw new LzmaError('LZMA: bad properties')
|
|
const lc = d % 9
|
|
d = Math.floor(d / 9)
|
|
const lp = d % 5
|
|
const pb = Math.floor(d / 5)
|
|
const dictSize = Math.max(properties[1] | (properties[2] << 8) | (properties[3] << 16) | (properties[4] << 24), 1 << 12) >>> 0
|
|
|
|
const out = Buffer.alloc(size)
|
|
let pos = 0
|
|
const rc = new RangeDecoder(input, 0)
|
|
const literals = probs(0x300 << (lc + lp))
|
|
const posSlot = probs(LEN_TO_POS_STATES << 6)
|
|
const align = probs(1 << ALIGN_BITS)
|
|
const posDecoders = probs(1 + FULL_DISTANCES - END_POS_MODEL_INDEX)
|
|
const isMatch = probs(STATES << POS_BITS_MAX)
|
|
const isRep = probs(STATES)
|
|
const isRepG0 = probs(STATES)
|
|
const isRepG1 = probs(STATES)
|
|
const isRepG2 = probs(STATES)
|
|
const isRep0Long = probs(STATES << POS_BITS_MAX)
|
|
const lenDecoder = new LenDecoder()
|
|
const repLenDecoder = new LenDecoder()
|
|
|
|
const distance = (len: number): number => {
|
|
const lenState = Math.min(len, LEN_TO_POS_STATES - 1)
|
|
const slot = rc.tree(posSlot, lenState << 6, 6)
|
|
if (slot < 4) return slot
|
|
const directBits = (slot >>> 1) - 1
|
|
let dist = ((2 | (slot & 1)) * 2 ** directBits) >>> 0
|
|
if (slot < END_POS_MODEL_INDEX) {
|
|
dist = (dist + rc.reverseTree(posDecoders, dist - slot, directBits)) >>> 0
|
|
} else {
|
|
dist = (dist + rc.direct(directBits - ALIGN_BITS) * 2 ** ALIGN_BITS) >>> 0
|
|
dist = (dist + rc.reverseTree(align, 0, ALIGN_BITS)) >>> 0
|
|
}
|
|
return dist
|
|
}
|
|
|
|
let state = 0
|
|
let rep0 = 0
|
|
let rep1 = 0
|
|
let rep2 = 0
|
|
let rep3 = 0
|
|
while (pos < size) {
|
|
const posState = pos & ((1 << pb) - 1)
|
|
if (rc.bit(isMatch, (state << POS_BITS_MAX) + posState) === 0) {
|
|
const prev = pos > 0 ? out[pos - 1] : 0
|
|
const base = 0x300 * (((pos & ((1 << lp) - 1)) << lc) + (prev >>> (8 - lc)))
|
|
let symbol = 1
|
|
if (state >= 7) {
|
|
let match = out[pos - rep0 - 1]
|
|
do {
|
|
const matchBit = (match >>> 7) & 1
|
|
match <<= 1
|
|
const bit = rc.bit(literals, base + ((1 + matchBit) << 8) + symbol)
|
|
symbol = (symbol << 1) | bit
|
|
if (matchBit !== bit) break
|
|
} while (symbol < 0x100)
|
|
}
|
|
while (symbol < 0x100) symbol = (symbol << 1) | rc.bit(literals, base + symbol)
|
|
out[pos++] = symbol - 0x100
|
|
state = state < 4 ? 0 : state < 10 ? state - 3 : state - 6
|
|
continue
|
|
}
|
|
let len: number
|
|
if (rc.bit(isRep, state) !== 0) {
|
|
if (pos === 0) throw new LzmaError('LZMA: repeat before any output')
|
|
if (rc.bit(isRepG0, state) === 0) {
|
|
if (rc.bit(isRep0Long, (state << POS_BITS_MAX) + posState) === 0) {
|
|
state = state < 7 ? 9 : 11
|
|
out[pos] = out[pos - rep0 - 1]
|
|
pos++
|
|
continue
|
|
}
|
|
} else {
|
|
let dist: number
|
|
if (rc.bit(isRepG1, state) === 0) {
|
|
dist = rep1
|
|
} else {
|
|
if (rc.bit(isRepG2, state) === 0) {
|
|
dist = rep2
|
|
} else {
|
|
dist = rep3
|
|
rep3 = rep2
|
|
}
|
|
rep2 = rep1
|
|
}
|
|
rep1 = rep0
|
|
rep0 = dist
|
|
}
|
|
len = repLenDecoder.decode(rc, posState)
|
|
state = state < 7 ? 8 : 11
|
|
} else {
|
|
rep3 = rep2
|
|
rep2 = rep1
|
|
rep1 = rep0
|
|
len = lenDecoder.decode(rc, posState)
|
|
state = state < 7 ? 7 : 10
|
|
rep0 = distance(len)
|
|
// The end marker: allowed only where the output is complete, which the loop already ended at.
|
|
if (rep0 === 0xffffffff) throw new LzmaError('LZMA: end marker before the end')
|
|
if (rep0 >= dictSize || rep0 >= pos) throw new LzmaError('LZMA: distance out of range')
|
|
}
|
|
len += MATCH_MIN_LEN
|
|
if (len > size - pos) throw new LzmaError('LZMA: match past the end')
|
|
for (let i = 0; i < len; i++, pos++) out[pos] = out[pos - rep0 - 1]
|
|
}
|
|
return out
|
|
}
|