mirror of
https://gitlab.com/the-no-frauds-club/cosmonarchy-bw-prerelease.git
synced 2026-09-21 14:21:34 +00:00
132 lines
5.4 KiB
TypeScript
132 lines
5.4 KiB
TypeScript
|
|
import { describe, expect, it } from "bun:test"
|
||
|
|
import * as Tbl from "../src/Tbl.ts"
|
||
|
|
import { entry, failWith, show, succeed } from "./helpers.ts"
|
||
|
|
|
||
|
|
/** Hand-assemble a .tbl so the tests do not lean on the encoder. */
|
||
|
|
const build = (blocks: Array<string>) => {
|
||
|
|
const header = 2 + blocks.length * 2
|
||
|
|
const data = blocks.join("")
|
||
|
|
const bytes = new Uint8Array(header + data.length)
|
||
|
|
const view = new DataView(bytes.buffer)
|
||
|
|
view.setUint16(0, blocks.length, true)
|
||
|
|
let cursor = header
|
||
|
|
blocks.forEach((block, i) => {
|
||
|
|
view.setUint16(2 + i * 2, cursor, true)
|
||
|
|
for (let j = 0; j < block.length; j++) bytes[cursor + j] = block.charCodeAt(j)
|
||
|
|
cursor += block.length
|
||
|
|
})
|
||
|
|
return bytes
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Assemble a .tbl from an explicit offset table plus raw string data. */
|
||
|
|
const buildWithOffsets = (offsets: Array<number>, data: string) => {
|
||
|
|
const header = 2 + offsets.length * 2
|
||
|
|
const bytes = new Uint8Array(header + data.length)
|
||
|
|
const view = new DataView(bytes.buffer)
|
||
|
|
view.setUint16(0, offsets.length, true)
|
||
|
|
offsets.forEach((offset, i) => view.setUint16(2 + i * 2, offset, true))
|
||
|
|
for (let j = 0; j < data.length; j++) bytes[header + j] = data.charCodeAt(j)
|
||
|
|
return bytes
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("decode", () => {
|
||
|
|
it("reads a simple table", async () => {
|
||
|
|
const table = await succeed(Tbl.decode(build(["one\0", "two\0"])))
|
||
|
|
expect(show(table)).toEqual([["one"], ["two"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("keeps the NUL-separated parts of a multi-part entry together", async () => {
|
||
|
|
const table = await succeed(Tbl.decode(build(["Maverick\0*\0Ground Units\0", "Egg\0"])))
|
||
|
|
expect(show(table)).toEqual([["Maverick", "*", "Ground Units"], ["Egg"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("gives the last entry everything up to the end of the file", async () => {
|
||
|
|
const table = await succeed(Tbl.decode(build(["a\0", "hotkey\0button text\0"])))
|
||
|
|
expect(show(table)).toEqual([["a"], ["hotkey", "button text"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("reads an empty string entry", async () => {
|
||
|
|
expect(show(await succeed(Tbl.decode(build(["\0", "x\0"]))))).toEqual([[""], ["x"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("returns an empty table when the count is zero", async () => {
|
||
|
|
expect(await succeed(Tbl.decode(Uint8Array.from([0, 0])))).toEqual([])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("resolves shared offsets to the same entry", async () => {
|
||
|
|
// Entries 0 and 1 point at one copy of "one", as a deduped table would.
|
||
|
|
const bytes = buildWithOffsets([8, 8, 12], "one\0three\0")
|
||
|
|
expect(show(await succeed(Tbl.decode(bytes)))).toEqual([["one"], ["one"], ["three"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("keeps trailing bytes no offset points at, as stat_txt.tbl relies on", async () => {
|
||
|
|
const bytes = buildWithOffsets([4], "a\0button text\0")
|
||
|
|
expect(show(await succeed(Tbl.decode(bytes)))).toEqual([["a", "button text"]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("rejects a file too short to hold a header", async () => {
|
||
|
|
const error = await failWith(Tbl.decode(Uint8Array.from([1])))
|
||
|
|
expect(error.message).toContain("truncated header")
|
||
|
|
})
|
||
|
|
|
||
|
|
it("rejects a truncated offset table", async () => {
|
||
|
|
const error = await failWith(Tbl.decode(Uint8Array.from([9, 0, 0, 0])))
|
||
|
|
expect(error.message).toContain("truncated offset table")
|
||
|
|
})
|
||
|
|
|
||
|
|
it("rejects an offset that points outside the string data", async () => {
|
||
|
|
const bytes = build(["one\0"])
|
||
|
|
new DataView(bytes.buffer).setUint16(2, 999, true)
|
||
|
|
const error = await failWith(Tbl.decode(bytes))
|
||
|
|
expect(error.message).toContain("outside the string data")
|
||
|
|
expect(error.offset).toBe(999)
|
||
|
|
})
|
||
|
|
|
||
|
|
it("rejects an entry that is not NUL-terminated", async () => {
|
||
|
|
const error = await failWith(Tbl.decode(build(["one", "two\0"])))
|
||
|
|
expect(error.message).toContain("not NUL-terminated")
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe("encode", () => {
|
||
|
|
it("reproduces the exact bytes it decoded", async () => {
|
||
|
|
const bytes = build(["Maverick\0*\0Ground Units\0", "\0", "Egg\0"])
|
||
|
|
expect(await succeed(Tbl.encode(await succeed(Tbl.decode(bytes))))).toEqual(bytes)
|
||
|
|
})
|
||
|
|
|
||
|
|
it("writes a well-formed header", async () => {
|
||
|
|
const bytes = await succeed(Tbl.encode([entry("ab"), entry("c")]))
|
||
|
|
const view = new DataView(bytes.buffer)
|
||
|
|
expect(view.getUint16(0, true)).toBe(2)
|
||
|
|
expect(view.getUint16(2, true)).toBe(6)
|
||
|
|
expect(view.getUint16(4, true)).toBe(9) // 6 + "ab\0"
|
||
|
|
expect(bytes.length).toBe(11) // 6 header + "ab\0" + "c\0"
|
||
|
|
})
|
||
|
|
|
||
|
|
it("treats an entry with no parts as a single empty string", async () => {
|
||
|
|
expect(show(await succeed(Tbl.decode(await succeed(Tbl.encode([[]])))))).toEqual([[""]])
|
||
|
|
})
|
||
|
|
|
||
|
|
it("shares identical entries when asked, and still decodes the same", async () => {
|
||
|
|
const table = Array.from({ length: 64 }, () => entry("Ground Units"))
|
||
|
|
const plain = await succeed(Tbl.encode(table))
|
||
|
|
const shared = await succeed(Tbl.encode(table, { dedupe: true }))
|
||
|
|
expect(shared.length).toBeLessThan(plain.length)
|
||
|
|
expect(show(await succeed(Tbl.decode(shared)))).toEqual(show(table))
|
||
|
|
})
|
||
|
|
|
||
|
|
it("refuses to write an offset a u16 cannot address", async () => {
|
||
|
|
const table = Array.from({ length: 100 }, (_, i) => entry(`${i}`.padEnd(1000, "x")))
|
||
|
|
const error = await failWith(Tbl.encode(table))
|
||
|
|
expect(error.message).toContain("past the 65535")
|
||
|
|
expect(error.message).toContain("--dedupe")
|
||
|
|
expect(error.offset).toBeGreaterThan(Tbl.MAX_OFFSET)
|
||
|
|
})
|
||
|
|
|
||
|
|
it("refuses to write more entries than the count field holds", async () => {
|
||
|
|
const table = Array.from({ length: Tbl.MAX_ENTRIES + 1 }, () => entry(""))
|
||
|
|
const error = await failWith(Tbl.encode(table))
|
||
|
|
expect(error.message).toContain("exceeds the 65535")
|
||
|
|
})
|
||
|
|
})
|