@quyse/client-zip
Version:
A tiny and fast client-side streaming ZIP generator
162 lines (137 loc) • 7.62 kB
text/typescript
import { assertEquals, assertStrictEquals } from "https://deno.land/std@0.132.0/testing/asserts.ts"
import { Buffer } from "https://deno.land/std@0.132.0/io/buffer.ts"
import { fileHeader, fileData, dataDescriptor, centralHeader, zip64ExtraField, contentLength, flagNameUTF8 } from "../src/zip.ts"
import type { ZipFileDescription, ZipFolderDescription } from "../src/input.ts"
import type { Metadata } from "../src/metadata.ts"
const BufferFromHex = (hex: string) => new Uint8Array(Array.from(hex.matchAll(/.{2}/g), ([s]) => parseInt(s, 16)))
const zipSpec = Deno.readFileSync("./test/APPNOTE.TXT")
const specName = new TextEncoder().encode("APPNOTE.TXT")
const specDate = new Date("2019-04-26T02:00")
const invalidUTF8 = BufferFromHex("fe")
const baseFile: ZipFileDescription & Metadata = Object.freeze(
{ isFile: true, bytes: new Uint8Array(zipSpec), encodedName: specName, nameIsBuffer: false, modDate: specDate, mode: 0o664 })
const baseFolder: ZipFolderDescription & Metadata = Object.freeze(
{ isFile: false, encodedName: new TextEncoder().encode("folder"), nameIsBuffer: false, modDate: specDate, mode: 0o775 })
Deno.test("the ZIP fileHeader function makes file headers", () => {
const file = {...baseFile}
const actual = fileHeader(file)
const expected = BufferFromHex("504b03042d000800000000109a4e0000000000000000000000000b000000")
assertEquals(actual, expected)
})
Deno.test("the ZIP fileHeader function makes folder headers", () => {
const folder = {...baseFolder}
const actual = fileHeader(folder)
const expected = BufferFromHex("504b03042d000800000000109a4e00000000000000000000000006000000")
assertEquals(actual, expected)
})
Deno.test("the ZIP fileHeader function merges extra flags", () => {
const file = {...baseFile}
const actual = fileHeader(file, 0x808)
const expected = BufferFromHex("504b03042d000808000000109a4e0000000000000000000000000b000000")
assertEquals(actual, expected)
})
Deno.test("the ZIP fileData function yields all the file's data", async () => {
const file = {...baseFile}
const actual = new Buffer()
for await (const chunk of fileData(file)) actual.writeSync(chunk)
assertEquals(actual.bytes({copy: false}), zipSpec)
})
Deno.test("the ZIP fileData function sets the file's size and CRC properties", async () => {
const file = {...baseFile}
assertStrictEquals(file.uncompressedSize, undefined)
assertStrictEquals(file.crc, undefined)
for await (const _ of fileData(file));
assertStrictEquals(file.uncompressedSize, BigInt(zipSpec.length))
assertStrictEquals(file.crc, 0xbb3afe3f)
})
Deno.test("the ZIP dataDescriptor function makes data descriptors", () => {
const file = {...baseFile, uncompressedSize: 0x10203040n, crc: 0x12345678}
const actual = dataDescriptor(file, false)
const expected = BufferFromHex("504b0708785634124030201040302010")
assertEquals(actual, expected)
})
Deno.test("the ZIP dataDescriptor function makes ZIP64 data descriptors", () => {
const file = {...baseFile, uncompressedSize: 0x110203040n, crc: 0x12345678}
const actual = dataDescriptor(file, true)
const expected = BufferFromHex("504b07087856341240302010010000004030201001000000")
assertEquals(actual, expected)
})
Deno.test("the ZIP dataDescriptor function makes folder data descriptors", () => {
const actual = dataDescriptor(baseFolder, false)
const expected = BufferFromHex("504b0708000000000000000000000000")
assertEquals(actual, expected)
})
Deno.test("the ZIP centralHeader function makes central record file headers", () => {
const file = {...baseFile, uncompressedSize: 0x10203040n, crc: 0x12345678}
const offset = 0x01020304n
const actual = centralHeader(file, offset, 0)
const expected = BufferFromHex("504b01022d032d000800000000109a4e7856341240302010403020100b0000000000000000000000b48104030201")
assertEquals(actual, expected)
})
Deno.test("the ZIP centralHeader function merges extra flags", () => {
const file = {...baseFile, uncompressedSize: 0x10203040n, crc: 0x12345678}
const offset = 0x01020304n
const actual = centralHeader(file, offset, 0x808)
const expected = BufferFromHex("504b01022d032d000808000000109a4e7856341240302010403020100b0000000000000000000000b48104030201")
assertEquals(actual, expected)
})
Deno.test("the ZIP centralHeader function makes ZIP64 central record file headers", () => {
const file = {...baseFile, uncompressedSize: 0x110203040n, crc: 0x12345678}
const offset = 0x101020304n
const actual = centralHeader(file, offset, 0, 28)
const expected = BufferFromHex("504b01022d032d000800000000109a4e78563412ffffffffffffffff0b001c000000000000000000b481ffffffff")
assertEquals(actual, expected)
})
Deno.test("the ZIP centralHeader function makes central record folder headers", () => {
const offset = 0x01020304n
const actual = centralHeader(baseFolder, offset, 0, 0)
const expected = BufferFromHex("504b01022d032d000800000000109a4e000000000000000000000000060000000000000000000000fd4104030201")
assertEquals(actual, expected)
})
Deno.test("the ZIP zip64ExtraField function makes Zip64 extra fields", () => {
const file = {...baseFile, uncompressedSize: 0x10203040n, crc: 0x12345678}
const offset = 0x01020304n
const actual = zip64ExtraField(file, offset, 28)
const expected = BufferFromHex("01001800403020100000000040302010000000000403020100000000")
assertEquals(actual, expected)
})
Deno.test("the contentLength function accurately predicts the length of an archive", () => {
const actual = contentLength([{uncompressedSize: BigInt(zipSpec.byteLength), encodedName: specName}])
const expected = 171462n
assertEquals(actual, expected)
})
Deno.test("the contentLength function does not throw on zero-length files", () => {
const actual = contentLength([{uncompressedSize: 0n, encodedName: specName}])
const expected = 136n
assertEquals(actual, expected)
})
Deno.test("the contentLength function accurately predicts the length of a large archive", () => {
const actual = contentLength([
{uncompressedSize: 0x110203040n, encodedName: specName},
{uncompressedSize: BigInt(zipSpec.byteLength), encodedName: specName},
])
const expected = 4565683956n
assertEquals(actual, expected)
})
Deno.test("the flagNameUTF8 function always turns on bit 11 if the name was not a Buffer", () => {
const actual = flagNameUTF8({encodedName: specName, nameIsBuffer: false})
assertEquals(actual, 0b1000)
assertEquals(flagNameUTF8({encodedName: specName, nameIsBuffer: false}, false), 0b1000)
assertEquals(flagNameUTF8({encodedName: specName, nameIsBuffer: false}, true), 0b1000)
assertEquals(flagNameUTF8({encodedName: invalidUTF8, nameIsBuffer: false}, false), 0b1000)
assertEquals(flagNameUTF8({encodedName: invalidUTF8, nameIsBuffer: false}, true), 0b1000)
})
Deno.test("the flagNameUTF8 function turns on bit 11 if the name is valid UTF-8", () => {
const actual = flagNameUTF8({encodedName: specName, nameIsBuffer: true})
assertEquals(actual, 0b1000)
})
Deno.test("the flagNameUTF8 function turns off bit 11 if the name is invalid UTF-8", () => {
const actual = flagNameUTF8({encodedName: invalidUTF8, nameIsBuffer: true})
assertEquals(actual, 0)
})
Deno.test("the flagNameUTF8 function does whatever the option says about Buffers", () => {
assertEquals(flagNameUTF8({encodedName: specName, nameIsBuffer: true}, false), 0)
assertEquals(flagNameUTF8({encodedName: specName, nameIsBuffer: true}, true), 0b1000)
assertEquals(flagNameUTF8({encodedName: invalidUTF8, nameIsBuffer: true}, false), 0)
assertEquals(flagNameUTF8({encodedName: invalidUTF8, nameIsBuffer: true}, true), 0b1000)
})