Files

46 lines
2.2 KiB
TypeScript

// Reads a ZIP the way a spreadsheet application does: from the central directory at the end. It is
// the test's independent reader of what `buildWorkbook` wrote, so the writer is not checked by itself.
import { inflateRawSync } from "node:zlib"
export function unzip(bytes: Uint8Array): Map<string, string> {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
let eocd = bytes.length - 22
while (eocd >= 0 && view.getUint32(eocd, true) !== 0x06054b50) eocd -= 1
if (eocd < 0) throw new Error("no end-of-central-directory record")
const entries = view.getUint16(eocd + 10, true)
let at = view.getUint32(eocd + 16, true)
const files = new Map<string, string>()
for (let i = 0; i < entries; i += 1) {
if (view.getUint32(at, true) !== 0x02014b50) throw new Error("a bad central directory entry")
const method = view.getUint16(at + 10, true)
const size = view.getUint32(at + 20, true)
const nameLength = view.getUint16(at + 28, true)
const extraLength = view.getUint16(at + 30, true)
const commentLength = view.getUint16(at + 32, true)
const local = view.getUint32(at + 42, true)
const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLength))
const dataAt = local + 30 + view.getUint16(local + 26, true) + view.getUint16(local + 28, true)
const raw = bytes.subarray(dataAt, dataAt + size)
files.set(name, new TextDecoder().decode(method === 8 ? inflateRawSync(raw) : raw))
at += 46 + nameLength + extraLength + commentLength
}
return files
}
/** The text and number cells of the first sheet, row by row. */
export function sheetRows(bytes: Uint8Array): Array<Array<string | number>> {
const sheet = unzip(bytes).get("xl/worksheets/sheet1.xml") ?? ""
return [...sheet.matchAll(/<row [^>]*>(.*?)<\/row>/gs)].map(([, row]) =>
[...(row as string).matchAll(/<c ([^>]*)>(.*?)<\/c>/gs)].map(([, attributes, body]) => {
if (!(attributes as string).includes('t="inlineStr"')) return Number(/<v>(.*?)<\/v>/s.exec(body as string)?.[1])
const text = /<t[^>]*>(.*?)<\/t>/s.exec(body as string)?.[1] ?? ""
return text
.replace(/&quot;/g, '"')
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<")
.replace(/&amp;/g, "&")
}),
)
}