Initial Commit
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user