mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
fix(07-10): 编解码自洽 —— bigint 长度字节守卫与参数计数取截断后长度
- WR-01:normalizeParam 的 bigint 分支按长度字节的真实容量(255 字节 = 2^2040)判定,|值| >= 2^2040 触发告警 152 并丢弃该参数;normalizeParamList 过滤被丢弃项,不保留占位 - WR-02:add/insert/set 写入的命令参数计数一律取标准化后的 normalizeParam.length,与 normalizeParamList 的 255 截断一致 - logger.json 的 warn 152 文案改为与真实可表示范围(2^2040 - 1)一致,并写明溢出参数会被忽略 - array.test.ts 新增 bigint 长度字节边界往返、超界丢弃后命令计数/读流对齐、256/300 参数截断计数回归
This commit is contained in:
parent
1f9fb18cf5
commit
35d4ad6f1c
@ -636,6 +636,62 @@ describe('ReplayArray param codec', () => {
|
||||
expect(info.map(v => v.code)).toContain(152);
|
||||
});
|
||||
|
||||
// 验证 bigint 的长度字节边界值(2^2040 - 1)仍可完整读写且后续参数不错位
|
||||
it('round-trips the largest bigint that fits the length byte', () => {
|
||||
const array = createArray({
|
||||
initParamLength: 512,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
const value = 2n ** 2040n - 1n;
|
||||
array.add(0, [value, 7]);
|
||||
|
||||
const params = array.get(0).params;
|
||||
expectParamTyped(params[0], value);
|
||||
expectParamTyped(params[1], 7);
|
||||
expect(firstParamToken(array)).toBe(7);
|
||||
// 长度字节为 255,与写入的幅值字节数一致
|
||||
expect(new Uint8Array(array.getParamArray())[1]).toBe(255);
|
||||
});
|
||||
|
||||
// 验证 2^2040 及以上的 bigint 触发告警码 152 并丢弃该参数,命令与其余参数仍完整写入且计数一致
|
||||
it('drops the unrepresentable bigint param and keeps the rest of the command', () => {
|
||||
for (const value of [
|
||||
2n ** 2040n,
|
||||
2n ** 2041n,
|
||||
2n ** 2048n,
|
||||
-(2n ** 2040n)
|
||||
]) {
|
||||
const array = createArray({
|
||||
initParamLength: 512,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
const { info } = logger.catch(() => array.add(0, [value, 7, 'x']));
|
||||
|
||||
expect(info.map(v => v.code)).toContain(152);
|
||||
expect(array.length).toBe(1);
|
||||
expect(array.get(0).command).toBe(0);
|
||||
expect(array.get(0).params).toEqual([7, 'x']);
|
||||
// 命令计数与实际写入的参数个数一致
|
||||
expect(new Uint8Array(array.getCommandArray())[0]).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证被丢弃的 bigint 参数不影响读流:后续命令仍从正确偏移解码
|
||||
it('keeps the read stream aligned after dropping an unrepresentable bigint', () => {
|
||||
const array = createArray({
|
||||
initParamLength: 512,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
logger.catch(() => array.add(1, [2n ** 2041n, 7]));
|
||||
array.add(2, [20]);
|
||||
|
||||
const stream = array.createReadStream(0);
|
||||
expectStepTyped(stream.read()!, 1, [7], 1);
|
||||
expectStepTyped(stream.read()!, 2, [20], 2);
|
||||
expect(stream.read()).toBeNull();
|
||||
expect(stream.index).toBe(2);
|
||||
});
|
||||
|
||||
// 验证单条命令参数超过 255 个时触发告警码 153 并忽略溢出参数
|
||||
it('warns code 153 when a command exceeds 255 params', () => {
|
||||
const array = createArray({
|
||||
@ -648,6 +704,64 @@ describe('ReplayArray param codec', () => {
|
||||
|
||||
expect(info.map(v => v.code)).toContain(153);
|
||||
});
|
||||
|
||||
// 验证 256 个参数的命令只写入截断后的计数 255,且后续命令仍能被读流正确读回
|
||||
it('writes the truncated param count when a command exceeds 255 params', () => {
|
||||
const array = createArray({
|
||||
initParamLength: 1024,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
const params = new Array<number>(256).fill(0);
|
||||
|
||||
logger.catch(() => array.add(0, params));
|
||||
array.add(1, [9]);
|
||||
|
||||
expect(new Uint8Array(array.getCommandArray())[0]).toBe(255);
|
||||
expect(array.get(0).params.length).toBe(255);
|
||||
|
||||
const stream = array.createReadStream(0);
|
||||
const first = stream.read()!;
|
||||
expect(first.command).toBe(0);
|
||||
expect(first.params.length).toBe(255);
|
||||
expectStepTyped(stream.read()!, 1, [9], 2);
|
||||
expect(stream.read()).toBeNull();
|
||||
});
|
||||
|
||||
// 验证 insert 与 set 同样写入截断后的参数计数,其后命令仍能正确读回
|
||||
it('writes the truncated param count through insert and set', () => {
|
||||
const params = new Array<number>(300).fill(0);
|
||||
|
||||
const inserted = createArray({
|
||||
initParamLength: 1024,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
inserted.add(1, [9]);
|
||||
logger.catch(() => inserted.insert(1, 2, params));
|
||||
inserted.add(3, [8]);
|
||||
|
||||
expect(new Uint8Array(inserted.getCommandArray())[2]).toBe(255);
|
||||
expect(inserted.get(1).params.length).toBe(255);
|
||||
const insertedStream = inserted.createReadStream(0);
|
||||
expectStepTyped(insertedStream.read()!, 1, [9], 1);
|
||||
expect(insertedStream.read()!.params.length).toBe(255);
|
||||
expectStepTyped(insertedStream.read()!, 3, [8], 3);
|
||||
expect(insertedStream.read()).toBeNull();
|
||||
|
||||
const replaced = createArray({
|
||||
initParamLength: 1024,
|
||||
paramMaxLength: 4096
|
||||
});
|
||||
replaced.add(1, [9]);
|
||||
replaced.add(2, [20]);
|
||||
logger.catch(() => replaced.set(0, 2, params));
|
||||
|
||||
expect(new Uint8Array(replaced.getCommandArray())[0]).toBe(255);
|
||||
expect(replaced.get(0).params.length).toBe(255);
|
||||
const replacedStream = replaced.createReadStream(0);
|
||||
expect(replacedStream.read()!.params.length).toBe(255);
|
||||
expectStepTyped(replacedStream.read()!, 2, [20], 2);
|
||||
expect(replacedStream.read()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplayArray stream and buffer combination', () => {
|
||||
|
||||
@ -221,8 +221,9 @@ export class ReplayArray implements IReplayArray {
|
||||
/**
|
||||
* 将数值标准化为数字或 Uint8Array
|
||||
* @param param 参数值
|
||||
* @returns 标准化结果,当参数无法用现有编码表示时返回 `null`
|
||||
*/
|
||||
private normalizeParam(param: ReplayParamValue): INormalizedParam {
|
||||
private normalizeParam(param: ReplayParamValue): INormalizedParam | null {
|
||||
if (typeof param === 'boolean') {
|
||||
// 0 - boolean
|
||||
return {
|
||||
@ -267,11 +268,12 @@ export class ReplayArray implements IReplayArray {
|
||||
};
|
||||
} else if (typeof param === 'bigint') {
|
||||
// 7 - 非负 bigint / 8 - 负 bigint,均以幅值按字节写入
|
||||
const wall = 2n ** 2047n;
|
||||
if (param > wall - 1n || param < -wall) {
|
||||
logger.warn(152);
|
||||
}
|
||||
// 幅值长度只有一个字节,故幅值必须小于 2 ^ 2040(即 255 字节),否则长度字节会溢出
|
||||
const magnitude = param < 0n ? -param : param;
|
||||
if (magnitude >= 2n ** 2040n) {
|
||||
logger.warn(152);
|
||||
return null;
|
||||
}
|
||||
const bit = magnitude.toString(2);
|
||||
const length = Math.ceil(bit.length / 8);
|
||||
const arr = new Uint8Array(length);
|
||||
@ -310,7 +312,7 @@ export class ReplayArray implements IReplayArray {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一系列参数值标准化为数字或 Uint8Array
|
||||
* 将一系列参数值标准化为数字或 Uint8Array,无法表示的参数会被丢弃
|
||||
* @param params 参数值列表
|
||||
*/
|
||||
private normalizeParamList(params: ReplayParamValue[]): INormalizedParam[] {
|
||||
@ -319,7 +321,13 @@ export class ReplayArray implements IReplayArray {
|
||||
logger.warn(153, params.length.toString());
|
||||
arr = params.slice(0, 255);
|
||||
}
|
||||
return arr.map(v => this.normalizeParam(v));
|
||||
// 丢弃的项不保留占位,保证命令参数计数与实际写入的参数个数一致
|
||||
const normalized: INormalizedParam[] = [];
|
||||
arr.forEach(v => {
|
||||
const param = this.normalizeParam(v);
|
||||
if (param) normalized.push(param);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -423,7 +431,7 @@ export class ReplayArray implements IReplayArray {
|
||||
// 追加指令
|
||||
const commandSize = this.getCommandSize();
|
||||
const commandStart = commandSize * this.length;
|
||||
this.setCommandArray(commandStart, params.length, command);
|
||||
this.setCommandArray(commandStart, normalized.length, command);
|
||||
|
||||
// 追加参数
|
||||
this.setParamArray(this.paramUsed, normalized);
|
||||
@ -454,7 +462,7 @@ export class ReplayArray implements IReplayArray {
|
||||
|
||||
// 插入到末尾等同于追加,此时无需任何位移
|
||||
if (index === this.length) {
|
||||
this.setCommandArray(commandStart, params.length, command);
|
||||
this.setCommandArray(commandStart, normalized.length, command);
|
||||
this.setParamArray(this.paramUsed, normalized);
|
||||
this.indexArray[this.length] = this.paramUsed;
|
||||
|
||||
@ -472,7 +480,7 @@ export class ReplayArray implements IReplayArray {
|
||||
this.indexArray.copyWithin(index + 1, index);
|
||||
|
||||
// 然后进行赋值操作,索引数组因为这一个指令的起始索引其实没变,所以不需要赋值
|
||||
this.setCommandArray(commandStart, params.length, command);
|
||||
this.setCommandArray(commandStart, normalized.length, command);
|
||||
this.setParamArray(paramStart, normalized);
|
||||
|
||||
this.length++;
|
||||
@ -551,7 +559,7 @@ export class ReplayArray implements IReplayArray {
|
||||
// 先写入指令
|
||||
const commandSize = this.getCommandSize();
|
||||
const commandStart = index * commandSize;
|
||||
this.setCommandArray(commandStart, params.length, command);
|
||||
this.setCommandArray(commandStart, normalized.length, command);
|
||||
|
||||
// 然后根据差值位移参数数组,如果参数长度减少还需要将最后几项置零
|
||||
this.paramArray.copyWithin(nextParam + deltaLength, nextParam);
|
||||
|
||||
@ -226,7 +226,7 @@
|
||||
"149": "Replay $1 array expand multiplier need to be greater than 1, but got $2.",
|
||||
"150": "Replay $1 array reached its max length $2. Just provide higher value if you do need more space, or check your logic to avoid add replay command repeatly.",
|
||||
"151": "0 or 1 is expected for boolean replay array, but got $1.",
|
||||
"152": "Bigint replay param reached its limit, min for -(2n ** 2047n), and max for 2n ** 2047n - 1n.",
|
||||
"152": "Bigint replay param out of representable range, min for -(2n ** 2040n - 1n), and max for 2n ** 2040n - 1n. The overflowed param will be ignored.",
|
||||
"153": "Up to 255 parameters for a single replay command, but got $1. Overflowed parameters will be ignored.",
|
||||
"154": "Cannot transfer replay command $1 from uint16 to uint8, since it's greater than 255.",
|
||||
"155": "Replay read stream expired, ignore this warning if you do need read expired replay data, or check your replay reading logic.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user