|
|
import { StringType, UINT32_LE } from 'token-types'; |
|
|
import { decompressSync } from 'fflate'; |
|
|
import initDebug from 'debug'; |
|
|
import { DataDescriptor, EndOfCentralDirectoryRecordToken, FileHeader, LocalFileHeaderToken, Signature } from "./ZipToken.js"; |
|
|
function signatureToArray(signature) { |
|
|
const signatureBytes = new Uint8Array(UINT32_LE.len); |
|
|
UINT32_LE.put(signatureBytes, 0, signature); |
|
|
return signatureBytes; |
|
|
} |
|
|
const debug = initDebug('tokenizer:inflate'); |
|
|
const syncBufferSize = 256 * 1024; |
|
|
const ddSignatureArray = signatureToArray(Signature.DataDescriptor); |
|
|
const eocdSignatureBytes = signatureToArray(Signature.EndOfCentralDirectory); |
|
|
export class ZipHandler { |
|
|
constructor(tokenizer) { |
|
|
this.tokenizer = tokenizer; |
|
|
this.syncBuffer = new Uint8Array(syncBufferSize); |
|
|
} |
|
|
async isZip() { |
|
|
return await this.peekSignature() === Signature.LocalFileHeader; |
|
|
} |
|
|
peekSignature() { |
|
|
return this.tokenizer.peekToken(UINT32_LE); |
|
|
} |
|
|
async findEndOfCentralDirectoryLocator() { |
|
|
const randomReadTokenizer = this.tokenizer; |
|
|
const chunkLength = Math.min(16 * 1024, randomReadTokenizer.fileInfo.size); |
|
|
const buffer = this.syncBuffer.subarray(0, chunkLength); |
|
|
await this.tokenizer.readBuffer(buffer, { position: randomReadTokenizer.fileInfo.size - chunkLength }); |
|
|
|
|
|
|
|
|
for (let i = buffer.length - 4; i >= 0; i--) { |
|
|
|
|
|
if (buffer[i] === eocdSignatureBytes[0] && |
|
|
buffer[i + 1] === eocdSignatureBytes[1] && |
|
|
buffer[i + 2] === eocdSignatureBytes[2] && |
|
|
buffer[i + 3] === eocdSignatureBytes[3]) { |
|
|
return randomReadTokenizer.fileInfo.size - chunkLength + i; |
|
|
} |
|
|
} |
|
|
return -1; |
|
|
} |
|
|
async readCentralDirectory() { |
|
|
if (!this.tokenizer.supportsRandomAccess()) { |
|
|
debug('Cannot reading central-directory without random-read support'); |
|
|
return; |
|
|
} |
|
|
debug('Reading central-directory...'); |
|
|
const pos = this.tokenizer.position; |
|
|
const offset = await this.findEndOfCentralDirectoryLocator(); |
|
|
if (offset > 0) { |
|
|
debug('Central-directory 32-bit signature found'); |
|
|
const eocdHeader = await this.tokenizer.readToken(EndOfCentralDirectoryRecordToken, offset); |
|
|
const files = []; |
|
|
this.tokenizer.setPosition(eocdHeader.offsetOfStartOfCd); |
|
|
for (let n = 0; n < eocdHeader.nrOfEntriesOfSize; ++n) { |
|
|
const entry = await this.tokenizer.readToken(FileHeader); |
|
|
if (entry.signature !== Signature.CentralFileHeader) { |
|
|
throw new Error('Expected Central-File-Header signature'); |
|
|
} |
|
|
entry.filename = await this.tokenizer.readToken(new StringType(entry.filenameLength, 'utf-8')); |
|
|
await this.tokenizer.ignore(entry.extraFieldLength); |
|
|
await this.tokenizer.ignore(entry.fileCommentLength); |
|
|
files.push(entry); |
|
|
debug(`Add central-directory file-entry: n=${n + 1}/${files.length}: filename=${files[n].filename}`); |
|
|
} |
|
|
this.tokenizer.setPosition(pos); |
|
|
return files; |
|
|
} |
|
|
this.tokenizer.setPosition(pos); |
|
|
} |
|
|
async unzip(fileCb) { |
|
|
const entries = await this.readCentralDirectory(); |
|
|
if (entries) { |
|
|
|
|
|
return this.iterateOverCentralDirectory(entries, fileCb); |
|
|
} |
|
|
|
|
|
let stop = false; |
|
|
do { |
|
|
const zipHeader = await this.readLocalFileHeader(); |
|
|
if (!zipHeader) |
|
|
break; |
|
|
const next = fileCb(zipHeader); |
|
|
stop = !!next.stop; |
|
|
let fileData = undefined; |
|
|
await this.tokenizer.ignore(zipHeader.extraFieldLength); |
|
|
if (zipHeader.dataDescriptor && zipHeader.compressedSize === 0) { |
|
|
const chunks = []; |
|
|
let len = syncBufferSize; |
|
|
debug('Compressed-file-size unknown, scanning for next data-descriptor-signature....'); |
|
|
let nextHeaderIndex = -1; |
|
|
while (nextHeaderIndex < 0 && len === syncBufferSize) { |
|
|
len = await this.tokenizer.peekBuffer(this.syncBuffer, { mayBeLess: true }); |
|
|
nextHeaderIndex = indexOf(this.syncBuffer.subarray(0, len), ddSignatureArray); |
|
|
const size = nextHeaderIndex >= 0 ? nextHeaderIndex : len; |
|
|
if (next.handler) { |
|
|
const data = new Uint8Array(size); |
|
|
await this.tokenizer.readBuffer(data); |
|
|
chunks.push(data); |
|
|
} |
|
|
else { |
|
|
|
|
|
await this.tokenizer.ignore(size); |
|
|
} |
|
|
} |
|
|
debug(`Found data-descriptor-signature at pos=${this.tokenizer.position}`); |
|
|
if (next.handler) { |
|
|
await this.inflate(zipHeader, mergeArrays(chunks), next.handler); |
|
|
} |
|
|
} |
|
|
else { |
|
|
if (next.handler) { |
|
|
debug(`Reading compressed-file-data: ${zipHeader.compressedSize} bytes`); |
|
|
fileData = new Uint8Array(zipHeader.compressedSize); |
|
|
await this.tokenizer.readBuffer(fileData); |
|
|
await this.inflate(zipHeader, fileData, next.handler); |
|
|
} |
|
|
else { |
|
|
debug(`Ignoring compressed-file-data: ${zipHeader.compressedSize} bytes`); |
|
|
await this.tokenizer.ignore(zipHeader.compressedSize); |
|
|
} |
|
|
} |
|
|
debug(`Reading data-descriptor at pos=${this.tokenizer.position}`); |
|
|
if (zipHeader.dataDescriptor) { |
|
|
|
|
|
const dataDescriptor = await this.tokenizer.readToken(DataDescriptor); |
|
|
if (dataDescriptor.signature !== 0x08074b50) { |
|
|
throw new Error(`Expected data-descriptor-signature at position ${this.tokenizer.position - DataDescriptor.len}`); |
|
|
} |
|
|
} |
|
|
} while (!stop); |
|
|
} |
|
|
async iterateOverCentralDirectory(entries, fileCb) { |
|
|
for (const fileHeader of entries) { |
|
|
const next = fileCb(fileHeader); |
|
|
if (next.handler) { |
|
|
this.tokenizer.setPosition(fileHeader.relativeOffsetOfLocalHeader); |
|
|
const zipHeader = await this.readLocalFileHeader(); |
|
|
if (zipHeader) { |
|
|
await this.tokenizer.ignore(zipHeader.extraFieldLength); |
|
|
const fileData = new Uint8Array(fileHeader.compressedSize); |
|
|
await this.tokenizer.readBuffer(fileData); |
|
|
await this.inflate(zipHeader, fileData, next.handler); |
|
|
} |
|
|
} |
|
|
if (next.stop) |
|
|
break; |
|
|
} |
|
|
} |
|
|
inflate(zipHeader, fileData, cb) { |
|
|
if (zipHeader.compressedMethod === 0) { |
|
|
return cb(fileData); |
|
|
} |
|
|
debug(`Decompress filename=${zipHeader.filename}, compressed-size=${fileData.length}`); |
|
|
const uncompressedData = decompressSync(fileData); |
|
|
return cb(uncompressedData); |
|
|
} |
|
|
async readLocalFileHeader() { |
|
|
const signature = await this.tokenizer.peekToken(UINT32_LE); |
|
|
if (signature === Signature.LocalFileHeader) { |
|
|
const header = await this.tokenizer.readToken(LocalFileHeaderToken); |
|
|
header.filename = await this.tokenizer.readToken(new StringType(header.filenameLength, 'utf-8')); |
|
|
return header; |
|
|
} |
|
|
if (signature === Signature.CentralFileHeader) { |
|
|
return false; |
|
|
} |
|
|
if (signature === 0xE011CFD0) { |
|
|
throw new Error('Encrypted ZIP'); |
|
|
} |
|
|
throw new Error('Unexpected signature'); |
|
|
} |
|
|
} |
|
|
function indexOf(buffer, portion) { |
|
|
const bufferLength = buffer.length; |
|
|
const portionLength = portion.length; |
|
|
|
|
|
if (portionLength > bufferLength) |
|
|
return -1; |
|
|
|
|
|
for (let i = 0; i <= bufferLength - portionLength; i++) { |
|
|
let found = true; |
|
|
for (let j = 0; j < portionLength; j++) { |
|
|
if (buffer[i + j] !== portion[j]) { |
|
|
found = false; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (found) { |
|
|
return i; |
|
|
} |
|
|
} |
|
|
return -1; |
|
|
} |
|
|
function mergeArrays(chunks) { |
|
|
|
|
|
const totalLength = chunks.reduce((acc, curr) => acc + curr.length, 0); |
|
|
const mergedArray = new Uint8Array(totalLength); |
|
|
let offset = 0; |
|
|
for (const chunk of chunks) { |
|
|
mergedArray.set(chunk, offset); |
|
|
offset += chunk.length; |
|
|
} |
|
|
return mergedArray; |
|
|
} |
|
|
|