187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { CHUNKS_CODES } from "../error-codes.js";
|
|
import { encodeChunk, parseChunk, validateChunk } from "./chunk.js";
|
|
|
|
describe("CHUNKS-FORMAT-V1-01 — Chunk binary layout is byte-exact", () => {
|
|
it("round-trips every field at exact offsets", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0001,
|
|
fileId: new Uint8Array(32).fill(0xab),
|
|
chunkId: 7,
|
|
totalChunks: 10,
|
|
fileSize: 12345n,
|
|
chunkSize: 65536,
|
|
payloadLen: 100,
|
|
flags: 0x0000,
|
|
nonce: new Uint8Array(24).fill(0xcd),
|
|
aeadTag: new Uint8Array(16).fill(0xef),
|
|
};
|
|
const sig = new Uint8Array(64).fill(0x12);
|
|
const ciphertext = new Uint8Array(100).fill(0x34);
|
|
|
|
const encoded = encodeChunk({ metadata, sig, ciphertext });
|
|
expect(encoded.length).toBe(164 + 100);
|
|
|
|
const view = new DataView(encoded.buffer, encoded.byteOffset);
|
|
expect(view.getUint16(0, false)).toBe(0x0001);
|
|
expect(encoded.slice(2, 34)).toEqual(metadata.fileId);
|
|
expect(view.getUint32(34, false)).toBe(7);
|
|
expect(view.getUint32(38, false)).toBe(10);
|
|
expect(view.getBigUint64(42, false)).toBe(12345n);
|
|
expect(view.getUint32(50, false)).toBe(65536);
|
|
expect(view.getUint32(54, false)).toBe(100);
|
|
expect(view.getUint16(58, false)).toBe(0x0000);
|
|
expect(encoded.slice(60, 84)).toEqual(metadata.nonce);
|
|
expect(encoded.slice(84, 100)).toEqual(metadata.aeadTag);
|
|
expect(encoded.slice(100, 164)).toEqual(sig);
|
|
expect(encoded.slice(164)).toEqual(ciphertext);
|
|
|
|
const parsed = parseChunk(encoded);
|
|
expect(parsed.metadata.version).toBe(0x0001);
|
|
expect(parsed.metadata.chunkId).toBe(7);
|
|
expect(parsed.metadata.payloadLen).toBe(100);
|
|
expect(parsed.sig).toEqual(sig);
|
|
expect(parsed.ciphertext).toEqual(ciphertext);
|
|
});
|
|
});
|
|
|
|
describe("CHUNKS-FORMAT-V1-02 — flags reserved in v1", () => {
|
|
it("rejects non-zero flags", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0001,
|
|
fileId: new Uint8Array(32),
|
|
chunkId: 0,
|
|
totalChunks: 1,
|
|
fileSize: 1n,
|
|
chunkSize: 65536,
|
|
payloadLen: 1,
|
|
flags: 0x0001,
|
|
nonce: new Uint8Array(24),
|
|
aeadTag: new Uint8Array(16),
|
|
};
|
|
const chunk = {
|
|
metadata,
|
|
sig: new Uint8Array(64),
|
|
ciphertext: new Uint8Array(1),
|
|
};
|
|
expect(() => validateChunk(chunk)).toThrow();
|
|
try {
|
|
validateChunk(chunk);
|
|
} catch (e) {
|
|
expect((e as Error).code).toBe(CHUNKS_CODES.FLAGS_RESERVED_NONZERO);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("CHUNKS-FORMAT-V1-03 — Payload length invariants", () => {
|
|
it("rejects non-final chunk payloadLen mismatch", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0001,
|
|
fileId: new Uint8Array(32),
|
|
chunkId: 0,
|
|
totalChunks: 2,
|
|
fileSize: 100000n,
|
|
chunkSize: 65536,
|
|
payloadLen: 100,
|
|
flags: 0x0000,
|
|
nonce: new Uint8Array(24),
|
|
aeadTag: new Uint8Array(16),
|
|
};
|
|
const chunk = {
|
|
metadata,
|
|
sig: new Uint8Array(64),
|
|
ciphertext: new Uint8Array(100),
|
|
};
|
|
expect(() => validateChunk(chunk)).toThrow();
|
|
try {
|
|
validateChunk(chunk);
|
|
} catch (e) {
|
|
expect((e as Error).code).toBe(CHUNKS_CODES.PAYLOAD_LEN_INVALID);
|
|
}
|
|
});
|
|
|
|
it("accepts correct non-final payloadLen", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0001,
|
|
fileId: new Uint8Array(32),
|
|
chunkId: 0,
|
|
totalChunks: 2,
|
|
fileSize: 100000n,
|
|
chunkSize: 65536,
|
|
payloadLen: 65536 - 164,
|
|
flags: 0x0000,
|
|
nonce: new Uint8Array(24),
|
|
aeadTag: new Uint8Array(16),
|
|
};
|
|
const chunk = {
|
|
metadata,
|
|
sig: new Uint8Array(64),
|
|
ciphertext: new Uint8Array(65536 - 164),
|
|
};
|
|
expect(() => validateChunk(chunk)).not.toThrow();
|
|
});
|
|
|
|
it("rejects final chunk payloadLen mismatch", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0001,
|
|
fileId: new Uint8Array(32),
|
|
chunkId: 1,
|
|
totalChunks: 2,
|
|
fileSize: 500n,
|
|
chunkSize: 65536,
|
|
payloadLen: 100,
|
|
flags: 0x0000,
|
|
nonce: new Uint8Array(24),
|
|
aeadTag: new Uint8Array(16),
|
|
};
|
|
const chunk = {
|
|
metadata,
|
|
sig: new Uint8Array(64),
|
|
ciphertext: new Uint8Array(100),
|
|
};
|
|
expect(() => validateChunk(chunk)).toThrow();
|
|
try {
|
|
validateChunk(chunk);
|
|
} catch (e) {
|
|
expect((e as Error).code).toBe(CHUNKS_CODES.PAYLOAD_LEN_INVALID);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("CHUNKS-FORMAT-V1-04 — parseChunk edge cases", () => {
|
|
it("rejects chunk shorter than header", { timeout: 5000 }, () => {
|
|
expect(() => parseChunk(new Uint8Array(10))).toThrow();
|
|
try {
|
|
parseChunk(new Uint8Array(10));
|
|
} catch (e) {
|
|
expect((e as Error).code).toBe(CHUNKS_CODES.METADATA_INVALID);
|
|
}
|
|
});
|
|
|
|
it("rejects unsupported chunk version", { timeout: 5000 }, () => {
|
|
const metadata = {
|
|
version: 0x0002,
|
|
fileId: new Uint8Array(32),
|
|
chunkId: 0,
|
|
totalChunks: 1,
|
|
fileSize: 1n,
|
|
chunkSize: 65536,
|
|
payloadLen: 1,
|
|
flags: 0x0000,
|
|
nonce: new Uint8Array(24),
|
|
aeadTag: new Uint8Array(16),
|
|
};
|
|
const chunk = {
|
|
metadata,
|
|
sig: new Uint8Array(64),
|
|
ciphertext: new Uint8Array(1),
|
|
};
|
|
expect(() => validateChunk(chunk)).toThrow();
|
|
try {
|
|
validateChunk(chunk);
|
|
} catch (e) {
|
|
expect((e as Error).code).toBe(CHUNKS_CODES.VERSION_UNSUPPORTED);
|
|
}
|
|
});
|
|
});
|