diablo2-web/tests/formats-malformed.test.ts

179 lines
6.7 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { decodePal, decodeTrn, indicesToRgba } from '../src/formats/pal';
import { decodePcx } from '../src/formats/pcx';
import { decodeDc6 } from '../src/formats/dc6';
import { decodeSpriteFile } from '../src/formats/cel';
import { decodePl2 } from '../src/formats/pl2';
import { decodeDs1 } from '../src/formats/ds1';
import { decodeDt1 } from '../src/formats/dt1';
import { decodeCof, cofLayerOrder } from '../src/formats/cof';
import { decodeDcc } from '../src/formats/dcc';
import { TruncatedDataError, InvalidFieldError, FormatError } from '../src/formats/reader';
describe('Malformed binary formats fail loudly', () => {
describe('pal', () => {
test('truncated', () => {
const data = new Uint8Array(700);
expect(() => decodePal(data)).toThrowError();
// original was PaletteError, check it fails safely
});
test('invalid trn', () => {
expect(() => decodeTrn(new Uint8Array(100))).toThrowError();
});
test('indicesToRgba bounds checks', () => {
const pal = { rgb: new Uint8Array(768), size: 256 };
expect(() => indicesToRgba(new Uint8Array(10), new Uint8Array(5), pal)).toThrow(FormatError);
expect(() => indicesToRgba(new Uint8Array(10), new Uint8Array(10), { rgb: new Uint8Array(100), size: 256 })).toThrow(FormatError);
expect(() => indicesToRgba(new Uint8Array(10), new Uint8Array(10), pal, new Uint8Array(5))).toThrow(FormatError);
});
});
describe('pcx', () => {
test('truncated header', () => {
expect(() => decodePcx(new Uint8Array(10))).toThrow(FormatError);
});
test('invalid manufacturer', () => {
const data = new Uint8Array(1000);
data[0] = 0xff; // Invalid
expect(() => decodePcx(data)).toThrow(FormatError);
});
test('geometry bounds checks', () => {
const data = new Uint8Array(1000);
data[0] = 0x0a;
data[3] = 8;
data[65] = 1;
const view = new DataView(data.buffer);
view.setUint16(4, 0, true);
view.setUint16(8, 0xffff, true); // Absurd width
// A well-formed palette trailer, so `geometry` is the ONLY broken
// invariant. Asserting the field name keeps this test honest: a coarse
// `toThrow(FormatError)` would still pass via the palette-marker guard
// even if the geometry guard were deleted outright.
data[data.length - 769] = 0x0c;
let caught: unknown;
try {
decodePcx(data);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(InvalidFieldError);
expect((caught as InvalidFieldError).field).toBe('geometry');
});
});
describe('dc6', () => {
const valid = fs.readFileSync(path.join(__dirname, '../samples/fixtures/fixture.dc6'));
test('valid decodes successfully', () => {
const out = decodeDc6(valid);
expect(out.header.version).toBe(6);
});
test('truncated file', () => {
expect(() => decodeDc6(valid.subarray(0, 10))).toThrow(TruncatedDataError);
expect(() => decodeDc6(valid.subarray(0, 24))).toThrow(TruncatedDataError);
});
test('absurd directions', () => {
const bad = new Uint8Array(valid);
new DataView(bad.buffer).setInt32(0x10, 1000000, true); // Absurd direction
expect(() => decodeDc6(bad)).toThrow(FormatError);
});
});
describe('pl2', () => {
test('truncated file', () => {
const expectedElements = 1024 + (32+16+1+256*3+256+256+111+1+1+1+14+256+1) * 256 + 39 + 13*256;
expect(() => decodePl2(new Uint8Array(expectedElements - 100))).toThrow(TruncatedDataError);
});
});
describe('ds1', () => {
const valid = fs.readFileSync(path.join(__dirname, '../samples/fixtures/fixture.ds1'));
test('valid decodes successfully', () => {
const out = decodeDs1(valid);
expect(out.version).toBe(16); // Check known fixture version
});
test('truncated file', () => {
expect(() => decodeDs1(valid.subarray(0, 10))).toThrow(TruncatedDataError);
expect(() => decodeDs1(valid.subarray(0, valid.length - 100))).toThrow(TruncatedDataError);
});
test('absurd size', () => {
const bad = new Uint8Array(valid);
new DataView(bad.buffer).setInt32(4, 0xffff, true); // Absurd width
expect(() => decodeDs1(bad)).toThrow(FormatError);
});
});
describe('dt1', () => {
const valid = fs.readFileSync(path.join(__dirname, '../samples/fixtures/fixture.dt1'));
test('valid decodes successfully', () => {
const out = decodeDt1(valid);
expect(out.versionMajor).toBe(7);
});
test('truncated file', () => {
expect(() => decodeDt1(valid.subarray(0, 20))).toThrow(TruncatedDataError);
expect(() => decodeDt1(valid.subarray(0, 50))).toThrow(TruncatedDataError);
});
test('absurd block offsets', () => {
const bad = new Uint8Array(valid);
new DataView(bad.buffer).setInt32(268 + 72 + 16, 0xffffff, true); new DataView(bad.buffer).setInt32(268 + 72 + 10, 100, true); // corrupt pointer
expect(() => decodeDt1(bad)).toThrow(FormatError);
});
});
describe('cof', () => {
// `samples/d2/` holds the original MPQ *archives*, not an extracted tree,
// so the presence of that directory says nothing about whether this loose
// .cof exists. Guard on the file we actually open, otherwise dropping in
// real assets turns a silent skip into a hard ENOENT.
const realCofPath = path.join(__dirname, '../samples/d2/data/global/chars/so/co/sowaxlbh.cof');
const hasRealCof = fs.existsSync(realCofPath);
test.skipIf(!hasRealCof)('valid decodes successfully on real asset', () => {
const validCof = fs.readFileSync(realCofPath);
const out = decodeCof(validCof);
expect(out.numberOfLayers).toBe(16);
});
test('truncated file', () => {
expect(() => decodeCof(new Uint8Array(10))).toThrow(TruncatedDataError);
});
test('absurd priority tables sizes', () => {
const header = new Uint8Array(28); // large enough for minimum headers
header[0] = 50; // layers
header[1] = 50; // frames
header[2] = 50; // directions
expect(() => decodeCof(header)).toThrow(TruncatedDataError);
});
});
describe('dcc', () => {
test('invalid signature', () => {
const bad = new Uint8Array(100);
bad[0] = 0x11;
expect(() => decodeDcc(bad)).toThrow(/signature/);
});
test('absurd sizes', () => {
const bad = new Uint8Array(100);
bad[0] = 0x74;
bad[1] = 1;
bad[2] = 0xff; // directions = 255
const dv = new DataView(bad.buffer);
dv.setInt32(3, 0xffffff, true); // Frames per dir
dv.setInt32(7, 1, true); // serialized = 1
expect(() => decodeDcc(bad)).toThrow(/frames per direction/);
});
});
});