mirror of
https://github.com/motajs/template.git
synced 2026-09-13 18:38:50 +08:00
fix(01-06): reject malformed raw event structures
- Validate map, layer, position, priority, and event id shapes before registration. - Add dedicated logger codes and no-partial-registration coverage.
This commit is contained in:
parent
564ef25360
commit
a730f33195
@ -62,3 +62,60 @@ describe('MapState raw event path', () => {
|
||||
expect(layer?.event(1, 0)?.dirty()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MapState malformed raw event structures', () => {
|
||||
const cases = [
|
||||
{
|
||||
name: 'missing raw.map container',
|
||||
mutate: raw => Reflect.set(raw, 'map', null),
|
||||
code: 63
|
||||
},
|
||||
{
|
||||
name: 'missing raw.events container',
|
||||
mutate: raw => Reflect.set(raw, 'events', null),
|
||||
code: 63
|
||||
},
|
||||
{
|
||||
name: 'missing event layer container',
|
||||
mutate: raw => Reflect.set(raw.events, '0', null),
|
||||
code: 63
|
||||
},
|
||||
{
|
||||
name: 'invalid event position container',
|
||||
mutate: raw => Reflect.set(raw.events[0], '1', []),
|
||||
code: 63
|
||||
},
|
||||
{
|
||||
name: 'non-numeric map layer key',
|
||||
mutate: raw => Reflect.set(raw.map, 'bad', [1, 1, 1, 1]),
|
||||
code: 62
|
||||
},
|
||||
{
|
||||
name: 'invalid map layer value',
|
||||
mutate: raw => Reflect.set(raw.map, '0', null),
|
||||
code: 64
|
||||
},
|
||||
{
|
||||
name: 'out of range event position',
|
||||
mutate: raw => Reflect.set(raw.events[0], '4', { 5: 'id' }),
|
||||
code: 64
|
||||
},
|
||||
{
|
||||
name: 'non-string event id',
|
||||
mutate: raw => Reflect.set(raw.events[0][1], '5', 3),
|
||||
code: 64
|
||||
}
|
||||
];
|
||||
|
||||
it.each(cases)('$name is rejected before map registration', testCase => {
|
||||
const mapState = createMapState();
|
||||
const raw = createRaw();
|
||||
testCase.mutate(raw);
|
||||
|
||||
const result = modules.logger.catch(() => mapState.fromRaw(raw));
|
||||
|
||||
expect(result.ret).toBeNull();
|
||||
expect(result.info.map(info => info.code)).toContain(testCase.code);
|
||||
expect(mapState.getMap(raw.floorId)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -35,9 +35,160 @@ export class MapState implements IMapState {
|
||||
public readonly state: IDataCommon
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 判断原始数据中的值是否为可枚举的对象容器
|
||||
* @param value 待判断的运行时值
|
||||
*/
|
||||
private isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串键是否能无歧义地转换为有限数字
|
||||
* @param key 原始对象键
|
||||
*/
|
||||
private isNumericKey(key: string): boolean {
|
||||
return key.trim() !== '' && Number.isFinite(Number(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在创建地图前验证外部原始地图及事件结构
|
||||
* @param raw 待验证的楼层原始数据
|
||||
*/
|
||||
private validateRaw(raw: IMapRawData): boolean {
|
||||
const rawMap: unknown = raw.map;
|
||||
const rawEvents: unknown = raw.events;
|
||||
const rawAliases: unknown = raw.layerAlias;
|
||||
if (!this.isRecord(rawMap)) {
|
||||
logger.error(63, 'map', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (!this.isRecord(rawEvents)) {
|
||||
logger.error(63, 'events', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (!this.isRecord(rawAliases)) {
|
||||
logger.error(63, 'layerAlias', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (!Number.isInteger(raw.width) || raw.width <= 0) {
|
||||
logger.error(64, 'width', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
for (const [zIndex, map] of Object.entries(rawMap)) {
|
||||
if (!this.isNumericKey(zIndex)) {
|
||||
logger.error(
|
||||
62,
|
||||
'layer',
|
||||
raw.floorId,
|
||||
'IMapRawData.map',
|
||||
zIndex
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(map)) {
|
||||
logger.error(64, 'map layer', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!map.every(
|
||||
value =>
|
||||
typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
Number.isInteger(value) &&
|
||||
value >= 0
|
||||
)
|
||||
) {
|
||||
logger.error(64, 'map value', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (length > 0 && map.length !== length) {
|
||||
logger.error(
|
||||
60,
|
||||
map.length.toString(),
|
||||
length.toString(),
|
||||
raw.floorId
|
||||
);
|
||||
return false;
|
||||
}
|
||||
length = map.length;
|
||||
|
||||
const alias = rawAliases[zIndex];
|
||||
if (typeof alias !== 'string') {
|
||||
logger.error(64, 'layer alias', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
const events = rawEvents[zIndex];
|
||||
if (!this.isRecord(events)) {
|
||||
logger.error(63, `events layer ${zIndex}`, raw.floorId);
|
||||
return false;
|
||||
}
|
||||
for (const [index, tileEvents] of Object.entries(events)) {
|
||||
if (!this.isNumericKey(index)) {
|
||||
logger.error(
|
||||
62,
|
||||
'event',
|
||||
raw.floorId,
|
||||
'IMapRawData.events',
|
||||
index
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const indexNum = Number(index);
|
||||
if (
|
||||
!Number.isInteger(indexNum) ||
|
||||
indexNum < 0 ||
|
||||
indexNum >= length
|
||||
) {
|
||||
logger.error(64, 'event position', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
if (!this.isRecord(tileEvents)) {
|
||||
logger.error(63, `events position ${index}`, raw.floorId);
|
||||
return false;
|
||||
}
|
||||
for (const [priority, id] of Object.entries(tileEvents)) {
|
||||
if (!this.isNumericKey(priority)) {
|
||||
logger.error(
|
||||
62,
|
||||
'event',
|
||||
raw.floorId,
|
||||
'IMapRawData.events',
|
||||
priority
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (typeof id !== 'string') {
|
||||
logger.error(64, 'event id', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length % raw.width !== 0) {
|
||||
logger.error(
|
||||
61,
|
||||
length.toString(),
|
||||
raw.width.toString(),
|
||||
raw.floorId
|
||||
);
|
||||
return false;
|
||||
}
|
||||
for (const zIndex of Object.keys(rawEvents)) {
|
||||
if (!this.isNumericKey(zIndex) || !Object.hasOwn(rawMap, zIndex)) {
|
||||
logger.error(64, 'event layer', raw.floorId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
fromRaw(raw: IMapRawData): IGameMap | null {
|
||||
if (!this.validateRaw(raw)) return null;
|
||||
let length = 0;
|
||||
const entries = Object.entries(raw.map);
|
||||
for (const [_, map] of Object.entries(raw.map)) {
|
||||
|
||||
@ -59,9 +59,11 @@
|
||||
"57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore.",
|
||||
"58": "Cannot find max equipment uid while loading equipment store.",
|
||||
"59": "Cannot find item raw data for tile num $1.",
|
||||
"60": "Cannot create layer state from raw data since the area of every layer of the map does not equal: $1 vs $2.",
|
||||
"61": "Cannot create layer state from raw data since map area cannot be divided by width evenly: area = $1 while divider = $2.",
|
||||
"62": "Expected a number for the key of $1, but got $2."
|
||||
"60": "Cannot create game map '$3' from raw data since the area of every layer of the map does not equal: $1 vs $2.",
|
||||
"61": "Cannot create game map '$3' from raw data since map area cannot be divided by width evenly: area = $1 while divider = $2.",
|
||||
"62": "Cannot add $1 from raw data for game map '$2' since Expected a number for the key of $3, but got $4.",
|
||||
"63": "Cannot create game map '$2' from raw data since the '$1' container is missing, null, or not an object.",
|
||||
"64": "Cannot create game map '$2' from raw data since the '$1' value has an invalid type or range."
|
||||
},
|
||||
"warn": {
|
||||
"1": "Resource with type of 'none' is loaded.",
|
||||
@ -232,6 +234,9 @@
|
||||
"166": "Related floor id or related position needs to be specified while change floor type is 'ChangeFloorType.Related'.",
|
||||
"167": "Hero must at a specified floor before using related change floor method.",
|
||||
"168": "The current floor of hero does not in the specified map list.",
|
||||
"169": "The current floor is the last or the first floor in the map list, so related floor change method will not work."
|
||||
"169": "The current floor is the last or the first floor in the map list, so related floor change method will not work.",
|
||||
"170": "Game event id '$1' has already been used, old event will be overridden.",
|
||||
"171": "Event id '$1' not found in event store, event will be skipped.",
|
||||
"172": "Event returned non-boolean value '$1' during reduction. JavaScript short-circuit semantics will be used."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user