File size: 6,678 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// test/codec.test.js
import { test } from 'vitest';
import assert from 'node:assert/strict';
import {
  encodeFrame,
  encodeBatchOps,
  decodeBatchOps,
  scanBatchOpRefs,
  FrameParser,
  CorruptFrameError,
  TYPE_SET,
  TYPE_DEL,
  HEADER_SIZE,
} from '../src/codec.js';

const B = (s) => Buffer.from(s);

test('round-trip SET', () => {
  const f = encodeFrame({ type: TYPE_SET, key: B('foo'), value: B('bar'), expireAt: 123 });
  const p = new FrameParser();
  const out = [...p.feed(f)];
  assert.equal(out.length, 1);
  assert.equal(out[0].type, TYPE_SET);
  assert.equal(out[0].key.toString(), 'foo');
  assert.equal(out[0].value.toString(), 'bar');
  assert.equal(out[0].expireAt, 123);
});

test('round-trip SET with meta', () => {
  const meta = Buffer.from(JSON.stringify({ dt: { dt1: 123 } }));
  const f = encodeFrame({ type: TYPE_SET, key: B('d'), value: B('v'), meta });
  const out = [...new FrameParser().feed(f)];
  assert.equal(out[0].key.toString(), 'd');
  assert.deepEqual(JSON.parse(out[0].meta.toString()), { dt: { dt1: 123 } });
});

test('frame without meta yields meta=null', () => {
  const f = encodeFrame({ type: TYPE_SET, key: B('d'), value: B('v') });
  const out = [...new FrameParser().feed(f)];
  assert.equal(out[0].meta, null);
});

test('round-trip DEL tombstone (empty value)', () => {
  const f = encodeFrame({ type: TYPE_DEL, key: B('gone') });
  const out = [...new FrameParser().feed(f)];
  assert.equal(out[0].type, TYPE_DEL);
  assert.equal(out[0].key.toString(), 'gone');
  assert.equal(out[0].value.length, 0);
});

test('multiple frames in one chunk', () => {
  const a = encodeFrame({ type: TYPE_SET, key: B('a'), value: B('1') });
  const b = encodeFrame({ type: TYPE_SET, key: B('b'), value: B('2') });
  const out = [...new FrameParser().feed(Buffer.concat([a, b]))];
  assert.equal(out.map((r) => r.key.toString()).join(','), 'a,b');
});

test('partial feed across chunks still parses', () => {
  const f = encodeFrame({ type: TYPE_SET, key: B('split'), value: B('x'.repeat(50)) });
  const p = new FrameParser();
  const cut = 7;
  const out1 = [...p.feed(f.subarray(0, cut))];
  assert.equal(out1.length, 0);
  const out2 = [...p.feed(f.subarray(cut))];
  assert.equal(out2.length, 1);
  assert.equal(out2[0].key.toString(), 'split');
  assert.equal(out2[0].value.length, 50);
});

test('crc mismatch throws CorruptFrameError with offset', () => {
  const f = encodeFrame({ type: TYPE_SET, key: B('bad'), value: B('data') });
  const corrupted = Buffer.from(f);
  corrupted[HEADER_SIZE + 1] ^= 0xff; // flip a byte inside the payload
  assert.throws(() => [...new FrameParser().feed(corrupted)], (e) => {
    assert.ok(e instanceof CorruptFrameError);
    assert.equal(e.offset, 0);
    return true;
  });
});

test('finish() reports a torn trailing partial frame at the valid-data offset', () => {
  const p = new FrameParser();
  const good = encodeFrame({ type: TYPE_SET, key: B('ok'), value: B('v') });
  void [...p.feed(good)];
  void [...p.feed(Buffer.from([0x4d, 0x44, 0x01]))]; // magic + type: incomplete header
  assert.throws(
    () => p.finish(),
    (e) => e instanceof CorruptFrameError && e.offset === good.length,
  );
});

test('finish() returns the clean EOF offset when there is no leftover', () => {
  const p = new FrameParser();
  const a = encodeFrame({ type: TYPE_SET, key: B('a'), value: B('1') });
  const b = encodeFrame({ type: TYPE_SET, key: B('b'), value: B('2') });
  void [...p.feed(Buffer.concat([a, b]))];
  assert.equal(p.finish(), a.length + b.length);
});

test('frame length = header + payload + crc trailer', () => {
  const f = encodeFrame({ type: TYPE_SET, key: B('k'), value: B('v') });
  assert.equal(f.length, HEADER_SIZE + 1 + 1 + 4);
});

// ---- BATCH body strict structure validation (stage 11, review #9) ----------

/** Hand-rolled batch-body encoder: unlike encodeBatchOps it imposes no type
 *  validation, so tests can craft bodies the real encoder would never emit. */
function rawBatchBody(ops) {
  const parts = [];
  let total = 2;
  for (const op of ops) {
    const key = B(op.key);
    const value = op.value === undefined ? Buffer.alloc(0) : B(op.value);
    total += 1 + 2 + 4 + 4 + 8 + key.length + value.length;
    parts.push({ ...op, key, value });
  }
  const body = Buffer.alloc(total);
  let o = 0;
  body.writeUInt16LE(parts.length, o); o += 2;
  for (const op of parts) {
    body.writeUInt8(op.type, o); o += 1;
    body.writeUInt16LE(op.key.length, o); o += 2;
    body.writeUInt32LE(op.value.length, o); o += 4;
    body.writeUInt32LE(0, o); o += 4; // metaLen
    body.writeBigInt64LE(0n, o); o += 8; // expireAt
    op.key.copy(body, o); o += op.key.length;
    op.value.copy(body, o); o += op.value.length;
  }
  return body;
}

test('decodeBatchOps / scanBatchOpRefs round-trip a valid body', () => {
  const body = encodeBatchOps([
    { type: TYPE_SET, key: B('a'), value: B('1'), meta: null, expireAt: 0 },
    { type: TYPE_DEL, key: B('b'), value: null, meta: null, expireAt: 0 },
  ]);
  const ops = decodeBatchOps(body);
  assert.equal(ops.length, 2);
  assert.equal(ops[0].type, TYPE_SET);
  assert.equal(ops[0].key.toString(), 'a');
  assert.equal(ops[1].type, TYPE_DEL);
  const refs = scanBatchOpRefs(body, 0);
  assert.equal(refs.length, 2);
  assert.equal(refs[0].valueOff, 2 + 19 + 1); // count + sub-header + key
});

test('batch decoders reject an unknown sub-op type', () => {
  const body = rawBatchBody([
    { type: TYPE_SET, key: 'accepted', value: 'yes' },
    { type: 99, key: 'unknown', value: 'ignored' },
  ]);
  assert.throws(() => decodeBatchOps(body), /batch op has unknown type 99/);
  assert.throws(() => scanBatchOpRefs(body, 0), /batch op has unknown type 99/);
});

test('batch decoders reject trailing bytes after the last op', () => {
  const valid = encodeBatchOps([{ type: TYPE_SET, key: B('a'), value: B('1'), meta: null, expireAt: 0 }]);
  const body = Buffer.concat([valid, Buffer.from([0xde, 0xad])]);
  assert.throws(() => decodeBatchOps(body), /batch body has 2 trailing byte\(s\)/);
  assert.throws(() => scanBatchOpRefs(body, 0), /batch body has 2 trailing byte\(s\)/);
});

test('batch decoders reject a body shorter than the count field', () => {
  assert.throws(() => decodeBatchOps(Buffer.alloc(1)), /batch body truncated: op count/);
  assert.throws(() => scanBatchOpRefs(Buffer.alloc(1), 0), /batch body truncated: op count/);
});

test('encodeBatchOps refuses to emit a non-SET/DEL sub-op (encode-side assertion)', () => {
  assert.throws(
    () => encodeBatchOps([{ type: 99, key: B('x'), value: B('y'), meta: null, expireAt: 0 }]),
    /batch op type must be SET or DEL, got 99/,
  );
});