diff --git a/packages-user/data-common/src/replay/array.test.ts b/packages-user/data-common/src/replay/array.test.ts index e36565f..246cb42 100644 --- a/packages-user/data-common/src/replay/array.test.ts +++ b/packages-user/data-common/src/replay/array.test.ts @@ -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(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(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', () => { diff --git a/packages-user/data-common/src/replay/array.ts b/packages-user/data-common/src/replay/array.ts index 65dd381..2323d8f 100644 --- a/packages-user/data-common/src/replay/array.ts +++ b/packages-user/data-common/src/replay/array.ts @@ -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); diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 14b8b74..6e964c5 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -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.",