mirror of
https://github.com/motajs/template.git
synced 2026-10-09 14:00:18 +08:00
fix(07-10): 写入路径越界校验与读流跨读档过期
- insert/delete/set 在 disabled 早退后、任何缓冲读写前校验 index,越界不修改任何缓冲区并告警新码 179(携带操作名/索引/当前长度) - insert(index === length) 判为合法追加,按 add 语义写入(paramUsed 起、indexArray[length] = paramUsed),insert 合法区间变为 [0, length] - setReplayArray 在 rebuildIndexArray 后补 expireStreams(),活跃读流跨 ReplaySystem.loadState 标记过期(#06-17-3 / 审计 H) - types.ts 的 insert/delete/set jsDoc 写明合法区间与越界契约 - logger.json 新增 warn 179(当前最大 warn 码 178 之后) - array.test.ts 新增 5 条边界回归(含越界前后逐字节不变量);saveLoad.test.ts 新增活跃读流跨读档告警 156 回归
This commit is contained in:
parent
9ae28c68cc
commit
1f9fb18cf5
@ -167,6 +167,25 @@ function expectRouteStream(
|
||||
expect(stream.index).toBe(expectedSteps.length);
|
||||
}
|
||||
|
||||
// 执行越界编辑并对比前后全部可观测状态:长度、两个缓冲区字节与每一步解码结果,返回本次告警码
|
||||
function expectArrayUnchanged(array: ReplayArray, run: () => void): number[] {
|
||||
const length = array.length;
|
||||
const commands = new Uint8Array(array.getCommandArray()).slice();
|
||||
const params = new Uint8Array(array.getParamArray()).slice();
|
||||
const steps = Array.from({ length }, (_, i) => array.get(i));
|
||||
|
||||
const { info } = logger.catch(run);
|
||||
|
||||
expect(array.length).toBe(length);
|
||||
expect(new Uint8Array(array.getCommandArray())).toEqual(commands);
|
||||
expect(new Uint8Array(array.getParamArray())).toEqual(params);
|
||||
expect(
|
||||
Array.from({ length: array.length }, (_, i) => array.get(i))
|
||||
).toEqual(steps);
|
||||
|
||||
return info.map(v => v.code!);
|
||||
}
|
||||
|
||||
describe('ReplayArray single operations', () => {
|
||||
// 验证 add 追加一条录像步并在原索引读回指令与参数
|
||||
it('appends one step and reads it back at the original index', () => {
|
||||
@ -357,6 +376,76 @@ describe('ReplayArray set index maintenance', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplayArray index bounds', () => {
|
||||
// 验证 insert 传入当前总步数时按末尾追加处理,追加后按索引与读流读回结果一致
|
||||
it('appends when the insert index equals the current length', () => {
|
||||
const array = createArray();
|
||||
array.add(1, [10]);
|
||||
array.add(2, [20]);
|
||||
|
||||
array.insert(array.length, 3, [30]);
|
||||
|
||||
expect(array.length).toBe(3);
|
||||
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
|
||||
const stream = array.createReadStream(0);
|
||||
expectStepTyped(stream.read()!, 1, [10], 1);
|
||||
expectStepTyped(stream.read()!, 2, [20], 2);
|
||||
expectStepTyped(stream.read()!, 3, [30], 3);
|
||||
expect(stream.read()).toBeNull();
|
||||
});
|
||||
|
||||
// 验证 insert 负索引与超出总步数的索引触发告警码 179 且不修改任何缓冲区内容
|
||||
it('warns code 179 and keeps the buffers unchanged for an out-of-range insert', () => {
|
||||
const array = createHeterogeneousArray();
|
||||
|
||||
for (const index of [-1, array.length + 1]) {
|
||||
const codes = expectArrayUnchanged(array, () =>
|
||||
array.insert(index, 9, [true, 5])
|
||||
);
|
||||
expect(codes).toContain(179);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证 delete 负索引与等于总步数的索引触发告警码 179 且不修改任何缓冲区内容
|
||||
it('warns code 179 and keeps the buffers unchanged for an out-of-range delete', () => {
|
||||
const array = createHeterogeneousArray();
|
||||
|
||||
for (const index of [-1, array.length]) {
|
||||
const codes = expectArrayUnchanged(array, () =>
|
||||
array.delete(index)
|
||||
);
|
||||
expect(codes).toContain(179);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证 set 负索引与等于总步数的索引触发告警码 179 且不修改任何缓冲区内容
|
||||
it('warns code 179 and keeps the buffers unchanged for an out-of-range set', () => {
|
||||
const array = createHeterogeneousArray();
|
||||
|
||||
for (const index of [-1, array.length]) {
|
||||
const codes = expectArrayUnchanged(array, () =>
|
||||
array.set(index, 9, [true, 5])
|
||||
);
|
||||
expect(codes).toContain(179);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证空录像上的 delete 与 set 一律按越界处理,既不告警 179 之外的内容也不产生录像步
|
||||
it('treats every index as out of range on an empty route', () => {
|
||||
const array = createArray();
|
||||
|
||||
for (const index of [0, -1]) {
|
||||
expect(
|
||||
expectArrayUnchanged(array, () => array.delete(index))
|
||||
).toContain(179);
|
||||
}
|
||||
expect(
|
||||
expectArrayUnchanged(array, () => array.set(0, 1, [10]))
|
||||
).toContain(179);
|
||||
expect(array.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplayArray param codec', () => {
|
||||
// 验证 boolean 参数按 type 0 写入并读回布尔值
|
||||
it('round-trips boolean parameters', () => {
|
||||
|
||||
@ -436,14 +436,36 @@ export class ReplayArray implements IReplayArray {
|
||||
|
||||
insert(index: number, command: number, params: ReplayParamValue[]): void {
|
||||
if (this.disabled > 0) return;
|
||||
if (index < 0 || index > this.length) {
|
||||
logger.warn(
|
||||
179,
|
||||
'insert',
|
||||
index.toString(),
|
||||
this.length.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
const normalized = this.normalizeParamList(params);
|
||||
const length = this.calculateParamsLength(normalized);
|
||||
this.checkBufferExpand(length);
|
||||
|
||||
const commandSize = this.getCommandSize();
|
||||
const commandStart = index * commandSize;
|
||||
|
||||
// 插入到末尾等同于追加,此时无需任何位移
|
||||
if (index === this.length) {
|
||||
this.setCommandArray(commandStart, params.length, command);
|
||||
this.setParamArray(this.paramUsed, normalized);
|
||||
this.indexArray[this.length] = this.paramUsed;
|
||||
|
||||
this.length++;
|
||||
this.paramUsed += length;
|
||||
|
||||
this.expireStreams();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先进行位移
|
||||
const commandStart = index * commandSize;
|
||||
const paramStart = this.indexArray[index];
|
||||
this.commandArray.copyWithin(commandStart + commandSize, commandStart);
|
||||
this.paramArray.copyWithin(paramStart + length, paramStart);
|
||||
@ -466,6 +488,15 @@ export class ReplayArray implements IReplayArray {
|
||||
|
||||
delete(index: number): void {
|
||||
if (this.disabled > 0) return;
|
||||
if (index < 0 || index >= this.length) {
|
||||
logger.warn(
|
||||
179,
|
||||
'delete',
|
||||
index.toString(),
|
||||
this.length.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
const commandSize = this.getCommandSize();
|
||||
const commandStart = index * commandSize;
|
||||
const range = this.getParamRange(index);
|
||||
@ -501,6 +532,10 @@ export class ReplayArray implements IReplayArray {
|
||||
|
||||
set(index: number, command: number, params: ReplayParamValue[]): void {
|
||||
if (this.disabled > 0) return;
|
||||
if (index < 0 || index >= this.length) {
|
||||
logger.warn(179, 'set', index.toString(), this.length.toString());
|
||||
return;
|
||||
}
|
||||
const normalized = this.normalizeParamList(params);
|
||||
const length = this.calculateParamsLength(normalized);
|
||||
// 末步的参数终点取 paramUsed,与 delete 同口径,避免读到未初始化的 indexArray[index + 1]
|
||||
@ -825,6 +860,9 @@ export class ReplayArray implements IReplayArray {
|
||||
this.length = length;
|
||||
|
||||
this.rebuildIndexArray();
|
||||
|
||||
// 缓冲区被整体替换后,此前创建的读流偏移已失效,需要与其它写路径一样标记过期
|
||||
this.expireStreams();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// 测试录像存读档:ReplaySystem 同实例往返与多类型参数读回
|
||||
// 测试录像存读档:ReplaySystem 同实例往返、多类型参数读回与活跃读流跨读档过期
|
||||
import { logger } from '@motajs/common';
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
||||
import { ReplaySystem } from './system';
|
||||
|
||||
@ -63,4 +64,24 @@ describe('ReplaySystem save and load round trips', () => {
|
||||
expect(system.route.getParamArray()).toBe(snapshot.paramArray);
|
||||
expect(system.route.get(0).command).toBe(2);
|
||||
});
|
||||
|
||||
// 验证读档整体替换缓冲区后,此前创建的活跃沙箱读流被标记过期并触发告警码 156(#06-17-3)
|
||||
it('expires an active sandbox read stream when a snapshot is loaded', () => {
|
||||
const system = createSystem();
|
||||
system.registerCommand(1, { execute: async () => true });
|
||||
system.record(1, 7);
|
||||
|
||||
const snapshot = system.saveState();
|
||||
system.record(1, 9);
|
||||
|
||||
const sandbox = system.createReplaySandbox({
|
||||
route: system.route,
|
||||
reseter: { reset: () => {} }
|
||||
});
|
||||
system.loadState(snapshot);
|
||||
|
||||
const { info } = logger.catch(() => sandbox.play());
|
||||
|
||||
expect(info.map(v => v.code)).toContain(156);
|
||||
});
|
||||
});
|
||||
|
||||
@ -196,8 +196,10 @@ export interface IReplayArray {
|
||||
|
||||
/**
|
||||
* 在指定索引处插入一条录像步,后续录像会自动后移。
|
||||
* 索引可传当前总步数,此时等同于在末尾追加一条录像步。
|
||||
* 索引越界时不会产生任何修改,并触发告警码 179。
|
||||
* 由于此操作会涉及大量的内存迁移,耗时较长,因此不建议频繁调用。
|
||||
* @param index 插入位置
|
||||
* @param index 插入位置,合法区间为 `[0, length]`
|
||||
* @param command 指令标识
|
||||
* @param params 参数列表
|
||||
*/
|
||||
@ -205,16 +207,18 @@ export interface IReplayArray {
|
||||
|
||||
/**
|
||||
* 删除指定索引处的录像步,此行为不会产生空槽。
|
||||
* 索引越界时不会产生任何修改,并触发告警码 179。
|
||||
* 由于此操作会涉及大量的内存迁移,耗时较长,因此不建议频繁调用。
|
||||
* @param index 要删除的步索引
|
||||
* @param index 要删除的步索引,合法区间为 `[0, length)`
|
||||
*/
|
||||
delete(index: number): void;
|
||||
|
||||
/**
|
||||
* 修改指定索引处的录像步。新的参数列表允许与原有参数编码长度不同,
|
||||
* 此情形下其后所有录像步的参数起始索引会同步平移。
|
||||
* 索引越界时不会产生任何修改,并触发告警码 179。
|
||||
* 由于此操作会涉及大量的内存迁移,耗时较长,因此不建议频繁调用。
|
||||
* @param index 要修改的步索引
|
||||
* @param index 要修改的步索引,合法区间为 `[0, length)`
|
||||
* @param command 新的指令标识
|
||||
* @param params 新的参数列表
|
||||
*/
|
||||
|
||||
@ -253,6 +253,7 @@
|
||||
"176": "Cannot add move step to replay system since only 'up,right,down,left' directions are acceptable.",
|
||||
"177": "Save data for saveable content $1 is needed, but there's no data in save data. Maybe a version issue for it? System will try to load other contents to make game run normally.",
|
||||
"178": "Save data with keys of '$1' are saved but not be loaded, is there some issue for it?",
|
||||
"179": "Replay $1 received an out-of-range index $2, the current replay length is $3. The operation will be ignored.",
|
||||
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user