blob: 7fbc381d8f3e7885ab87c806a16b08ada65fd602 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
serializeOptions,
deserializePrefixedOptions
} from './options.utils.js';
import { HeaderValue } from './message/header.utils.js';
const prefixed = (block: Buffer): Buffer => {
const length = Buffer.alloc(4);
length.writeUInt32LE(block.length, 0);
return Buffer.concat([length, block]);
};
/**
* The cross-SDK golden vector for an options block.
*
* Rust pins the identical bytes in `core/binary_protocol/src/primitives/options.rs`,
* as do the Go and Java SDKs. Round-tripping through this SDK's own decoder proves
* nothing about interoperability; these bytes are the contract.
*/
const GOLDEN_OPTIONS_BLOCK = Buffer.from([
2, 13, 0, 0, 0,
...Buffer.from('enforce_fsync'),
3, 1, 0, 0, 0, 1,
2, 12, 0, 0, 0,
...Buffer.from('segment_size'),
12, 8, 0, 0, 0,
0, 0, 0, 64, 0, 0, 0, 0
]);
describe('serializeOptions', () => {
it('encodes the cross-SDK golden vector byte for byte', () => {
const encoded = serializeOptions([
{ key: 'enforce_fsync', value: HeaderValue.Bool(true) },
{ key: 'segment_size', value: HeaderValue.Uint64(1_073_741_824n) }
]);
assert.deepEqual(encoded, GOLDEN_OPTIONS_BLOCK);
});
it('refuses an empty key', () => {
assert.throws(
() => serializeOptions([{ key: '', value: HeaderValue.Bool(true) }]),
/Invalid option key '' length: 0 bytes/
);
});
it('refuses a key past the field bound', () => {
assert.throws(
() => serializeOptions([
{ key: 'k'.repeat(256), value: HeaderValue.Bool(true) }
]),
/length: 256 bytes, must be between 1 and 255/
);
});
it('bounds the key on encoded bytes, not characters', () => {
// 128 two-byte characters: a character count reads 128 and lets this
// through, leaving the server to refuse the whole block.
assert.throws(
() => serializeOptions([
{ key: 'ключ'.repeat(32), value: HeaderValue.Bool(true) }
]),
/length: 256 bytes/
);
});
it('accepts a key exactly at the field bound', () => {
const encoded = serializeOptions([
{ key: 'k'.repeat(255), value: HeaderValue.Bool(true) }
]);
assert.equal(encoded.readUInt32LE(1), 255);
});
it('refuses a value past the field bound', () => {
assert.throws(
() => serializeOptions([
{ key: 'segment_size', value: HeaderValue.String('v'.repeat(256)) }
]),
/Invalid option value for key 'segment_size' length: 256 bytes/
);
});
it('refuses an empty value', () => {
assert.throws(
() => serializeOptions([
{ key: 'segment_size', value: HeaderValue.Raw(Buffer.alloc(0)) }
]),
/Invalid option value for key 'segment_size' length: 0 bytes/
);
});
});
describe('deserializePrefixedOptions', () => {
it('reads a whole block and reports the bytes it consumed', () => {
const block = prefixed(serializeOptions([
{ key: 'segment_size', value: HeaderValue.Uint64(1_048_576n) }
]));
const { bytesRead, options } = deserializePrefixedOptions(block);
assert.equal(bytesRead, block.length);
assert.deepEqual(options, { segment_size: 1_048_576n });
});
it('rejects a block whose declared length runs past the payload', () => {
// `subarray` clamps instead of throwing, so without the bounds check the
// truncated value comes back as raw bytes through the forward-compat catch
// and `bytesRead` over-reports, shifting every later field.
const block = prefixed(serializeOptions([
{ key: 'segment_size', value: HeaderValue.Uint64(1_048_576n) }
]));
assert.throws(
() => deserializePrefixedOptions(block.subarray(0, block.length - 4)),
/overruns the payload/
);
});
});