1
0
mirror of https://gitlab.com/the-no-frauds-club/cosmonarchy-bw-prerelease.git synced 2026-09-21 14:21:34 +00:00
cosmonarchy-bw-prerelease/tools/tbl/test/Tbl.test.ts

132 lines
5.4 KiB
TypeScript
Raw Normal View History

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")
})
})