You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
filepizza/src/zip-stream.ts

244 lines
7.1 KiB
TypeScript

// Based on https://github.com/jimmywarting/StreamSaver.js/blob/master/examples/zip-stream.js
//
// Disabling typechecking for now since this was originally JavaScript, should
// find some time to add types later.
//
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
class Crc32 {
crc: number
constructor() {
this.crc = -1
}
table = (() => {
let i
let j
let t
const table = []
for (i = 0; i < 256; i++) {
t = i
for (j = 0; j < 8; j++) {
t = t & 1 ? (t >>> 1) ^ 0xedb88320 : t >>> 1
}
table[i] = t
}
return table
})()
append(data) {
let crc = this.crc | 0
const table = this.table
for (let offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xff]
}
this.crc = crc
}
get() {
return ~this.crc
}
}
const getDataHelper = (byteLength) => {
const uint8 = new Uint8Array(byteLength)
return {
array: uint8,
view: new DataView(uint8.buffer),
}
}
const pump = (zipObj) =>
zipObj.reader.read().then((chunk) => {
if (chunk.done) return zipObj.writeFooter()
const outputData = chunk.value
zipObj.crc.append(outputData)
zipObj.uncompressedLength += outputData.length
zipObj.compressedLength += outputData.length
zipObj.ctrl.enqueue(outputData)
})
export function createZipStream(
underlyingSource: UnderlyingSource<any>,
): ReadableStream {
const files = Object.create(null)
const filenames = []
const encoder = new TextEncoder()
let offset = 0
let activeZipIndex = 0
let ctrl
let activeZipObject, closed
function next() {
activeZipIndex++
activeZipObject = files[filenames[activeZipIndex]]
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (activeZipObject) processNextChunk()
// eslint-disable-next-line @typescript-eslint/no-use-before-define
else if (closed) closeZip()
}
const zipWriter = {
enqueue(fileLike) {
if (closed)
throw new TypeError(
'Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed',
)
let name = fileLike.name.trim()
const date = new Date(
typeof fileLike.lastModified === 'undefined'
? Date.now()
: fileLike.lastModified,
)
if (fileLike.directory && !name.endsWith('/')) name += '/'
if (files[name]) throw new Error('File already exists.')
const nameBuf = encoder.encode(name)
filenames.push(name)
const zipObject = (files[name] = {
level: 0,
ctrl,
directory: !!fileLike.directory,
nameBuf,
comment: encoder.encode(fileLike.comment || ''),
compressedLength: 0,
uncompressedLength: 0,
writeHeader() {
const header = getDataHelper(26)
const data = getDataHelper(30 + nameBuf.length)
zipObject.offset = offset
zipObject.header = header
if (zipObject.level !== 0 && !zipObject.directory) {
header.view.setUint16(4, 0x0800)
}
header.view.setUint32(0, 0x14000808)
header.view.setUint16(
6,
(((date.getHours() << 6) | date.getMinutes()) << 5) |
(date.getSeconds() / 2),
true,
)
header.view.setUint16(
8,
((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) <<
5) |
date.getDate(),
true,
)
header.view.setUint16(22, nameBuf.length, true)
data.view.setUint32(0, 0x504b0304)
data.array.set(header.array, 4)
data.array.set(nameBuf, 30)
offset += data.array.length
ctrl.enqueue(data.array)
},
writeFooter() {
const footer = getDataHelper(16)
footer.view.setUint32(0, 0x504b0708)
if (zipObject.crc) {
zipObject.header.view.setUint32(10, zipObject.crc.get(), true)
zipObject.header.view.setUint32(
14,
zipObject.compressedLength,
true,
)
zipObject.header.view.setUint32(
18,
zipObject.uncompressedLength,
true,
)
footer.view.setUint32(4, zipObject.crc.get(), true)
footer.view.setUint32(8, zipObject.compressedLength, true)
footer.view.setUint32(12, zipObject.uncompressedLength, true)
}
ctrl.enqueue(footer.array)
offset += zipObject.compressedLength + 16
next()
},
fileLike,
})
if (!activeZipObject) {
activeZipObject = zipObject
// eslint-disable-next-line @typescript-eslint/no-use-before-define
processNextChunk()
}
},
close() {
if (closed)
throw new TypeError(
'Cannot close a readable stream that has already been requested to be closed',
)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (!activeZipObject) closeZip()
closed = true
},
}
function closeZip() {
let length = 0
let index = 0
let indexFilename, file
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
length += 46 + file.nameBuf.length + file.comment.length
}
const data = getDataHelper(length + 22)
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
data.view.setUint32(index, 0x504b0102)
data.view.setUint16(index + 4, 0x1400)
data.array.set(file.header.array, index + 6)
data.view.setUint16(index + 32, file.comment.length, true)
if (file.directory) {
data.view.setUint8(index + 38, 0x10)
}
data.view.setUint32(index + 42, file.offset, true)
data.array.set(file.nameBuf, index + 46)
data.array.set(file.comment, index + 46 + file.nameBuf.length)
index += 46 + file.nameBuf.length + file.comment.length
}
data.view.setUint32(index, 0x504b0506)
data.view.setUint16(index + 8, filenames.length, true)
data.view.setUint16(index + 10, filenames.length, true)
data.view.setUint32(index + 12, length, true)
data.view.setUint32(index + 16, offset, true)
ctrl.enqueue(data.array)
ctrl.close()
}
function processNextChunk() {
if (!activeZipObject) return
if (activeZipObject.directory)
return activeZipObject.writeFooter(activeZipObject.writeHeader())
if (activeZipObject.reader) return pump(activeZipObject)
if (activeZipObject.fileLike.stream) {
activeZipObject.crc = new Crc32()
activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
activeZipObject.writeHeader()
} else next()
}
return new ReadableStream({
start: (c) => {
ctrl = c
underlyingSource.start &&
Promise.resolve(underlyingSource.start(zipWriter))
},
pull() {
return (
processNextChunk() ||
(underlyingSource.pull &&
Promise.resolve(underlyingSource.pull(zipWriter)))
)
},
})
}