Files
erqiwang_youle/server/games/erqiwang/test/test_input.js
T
joywayerandClaude Opus 5 a4a8eb8408 二七王:补齐客户端牌id入参校验(重复/越界/非数组)
check_cards_inhand 此前只逐个校验牌id是否在手上,不查重复、不查
类型与范围,且在牌已出/已埋时 return 空值而非 false,造成两个可
被利用的缺陷:

1. 重复牌id 可伪造牌型:do_playcard([方块K,方块K]) 实测返回
   result=true、cardtype=201(同一张牌被判成一对),可进而伪造
   拖拉机与甩牌分量,本轮结算时该分牌的 score 还会被重复累加。
2. 埋牌传 8 个相同 id 实测只埋下 1 张,庄家带 35 张进入出牌阶段,
   整局牌数错乱。
3. 越界 id 或非数组入参会在解引用处抛异常,中断 DoPack。

新增 check_cards_valid 作为唯一入参校验入口(非空数组、元素为本
局牌表内的整数牌id、互不重复),由 check_cards_inhand 先行调用;
mod.maipai/mod.chupai 在读取 cards.length 之前先过该校验。按
engineering §03「显式失败优于隐式兜底」直接拒绝,不做类型兜底。

新增 test_input.js 28 项正/反/边界用例回归上述三点。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:10:15 +08:00

96 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 客户端入参校验:重复牌id / 越界id / 非数组 / 非整数(check_cards_valid + check_cards_inhand)
// 回归两个真实缺陷:① 重复id 被 can_playcard 当成对子/拖拉机;② 埋牌传重复id 只埋下 1 张
require('./_shim');
const { mod, setup, make108, id } = require('./_rpc.js');
const P = require('../class.paiju.js');
const t = require('./_assert')();
// 造一个最小可用牌局:108 张牌全发给指定座位,主花色=1
function newPaiju(step, banker, owner) {
const paiju = {
cards: make108(), step, banker, call: 65, flower: 1,
seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]],
playproc: {}, callproc: [], idx: 1
};
for (let i = 0; i < 108; i++) paiju.cards[i].dealowner = owner + 1;
paiju.method = {
check_cards_valid: c => P.check_cards_valid(paiju, c),
check_cards_inhand: (c, s) => P.check_cards_inhand(paiju, c, s),
do_burycard: c => P.do_burycard(paiju, c),
do_playcard: c => P.do_playcard(paiju, c),
get_seat_cards: s => P.get_seat_cards(paiju, s),
get_seat_zhucards: s => P.get_seat_zhucards(paiju, s),
have_baofu: () => (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0),
get_liangpai: () => null
};
return paiju;
}
// ============ L1 check_cards_valid:正 / 反 / 边界 ============
const pj = newPaiju(5, 0, 0);
const V = c => P.check_cards_valid(pj, c);
t.eq('valid 正 单张', V([0]), true);
t.eq('valid 正 多张不重复', V([0, 1, 2, 53]), true);
t.eq('valid 边界 最大牌id 107', V([107]), true);
t.eq('valid 边界 最小牌id 0', V([0]), true);
t.eq('valid 反 undefined', V(undefined), false);
t.eq('valid 反 null', V(null), false);
t.eq('valid 反 字符串', V('012'), false);
t.eq('valid 反 对象', V({ 0: 1, length: 1 }), false);
t.eq('valid 反 空数组', V([]), false);
t.eq('valid 反 重复id', V([5, 5]), false);
t.eq('valid 反 多张中含重复', V([1, 2, 3, 2]), false);
t.eq('valid 反 越界上界108', V([108]), false);
t.eq('valid 反 负数id', V([-1]), false);
t.eq('valid 反 小数id', V([1.5]), false);
t.eq('valid 反 数字字符串', V(['5']), false);
// ============ L1 check_cards_inhand ============
t.eq('inhand 正 在手上', P.check_cards_inhand(pj, [0, 1], 0), true);
t.eq('inhand 反 不在手上(别人的)', P.check_cards_inhand(pj, [0], 1), false);
t.eq('inhand 反 重复id(经 valid 拦截)', P.check_cards_inhand(pj, [0, 0], 0), false);
t.eq('inhand 反 越界id(经 valid 拦截)', P.check_cards_inhand(pj, [999], 0), false);
pj.cards[3].playround = 1; // 已打出
t.eq('inhand 反 已打出的牌', P.check_cards_inhand(pj, [3], 0), false);
pj.cards[4].playround = 0; // 已埋
t.eq('inhand 反 已埋下的牌', P.check_cards_inhand(pj, [4], 0), false);
// ============ L2 出牌:重复id 不得被当成一对 ============
const pj2 = newPaiju(5, 0, 0);
P.new_playround(pj2, 1, 0);
const cid = id(1, 1, 13); // 方块K
t.eq('出牌 反 重复id 被 mod 层拦截(valid)', P.check_cards_valid(pj2, [cid, cid]), false);
// ============ L3 mod.maipai:8 个重复id 必须被拒、不得进入出牌阶段 ============
const pj3 = newPaiju(3, 0, 0);
const s3 = setup('00000', pj3);
const dup = id(1, 1, 13);
mod.maipai({ data: { seat: 0, cards: [dup, dup, dup, dup, dup, dup, dup, dup] }, conmode: 0, fromid: 0 });
t.eq('埋牌 反 8个重复id 被拒(step仍为3)', pj3.step, 3);
t.eq('埋牌 反 8个重复id 未埋下任何牌', pj3.cards.filter(c => c.playround === 0).length, 0);
// 对照:8 张不同的牌可以正常埋下
const pj4 = newPaiju(3, 0, 0);
setup('00000', pj4);
mod.maipai({ data: { seat: 0, cards: [0, 1, 2, 3, 4, 5, 6, 7] }, conmode: 0, fromid: 0 });
t.eq('埋牌 正 8张不同牌 埋牌成功(step进5)', pj4.step, 5);
t.eq('埋牌 正 实际埋下8张', pj4.cards.filter(c => c.playround === 0).length, 8);
// ============ L3 mod.chupai:越界/非数组入参不得抛异常 ============
const pj5 = newPaiju(5, 0, 0);
P.new_playround(pj5, 1, 0);
const s5 = setup('00000', pj5);
let threw = null;
try {
mod.chupai({ data: { seat: 0, cards: [999] }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: undefined }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: 'x' }, conmode: 0, fromid: 0 });
mod.chupai({ data: { seat: 0, cards: [dup, dup] }, conmode: 0, fromid: 0 });
} catch (e) { threw = String(e); }
t.eq('出牌 反 非法入参不抛异常', threw, null);
t.eq('出牌 反 非法入参不产生任何出牌', pj5.cards.filter(c => c.playround > 0).length, 0);
process.exit(t.done('input') ? 0 : 1);