import { isNumber, isString } from "es-toolkit"; import type { ConfigValidationIssue } from "../../schema/issues"; import type { CheckerValidationInput } from "../types"; import { isPlainRecord, validateRawContentExpectations, validateRawValueExpectation } from "../../expect/validate"; import { issue, joinPath } from "../../schema/issues"; export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isPlainRecord(target)) continue; if (target["type"] !== "tcp") continue; issues.push(...validateTcpTarget(target, `targets[${i}]`)); } return issues; } function getTargetName(target: Record): string | undefined { if (isString(target["name"])) return target["name"]; return isString(target["id"]) ? target["id"] : undefined; } function isNonNegativeFiniteNumber(value: unknown): boolean { return isNumber(value) && Number.isFinite(value) && value >= 0; } function validateTcpExpect( target: Record, path: string, readBanner: boolean, ): ConfigValidationIssue[] { const targetName = getTargetName(target); const expect = target["expect"]; if (expect === undefined || expect === null || !isPlainRecord(expect)) return []; const issues: ConfigValidationIssue[] = []; const expectPath = joinPath(path, "expect"); if (expect["connected"] !== undefined && typeof expect["connected"] !== "boolean") { issues.push(issue("invalid-type", joinPath(expectPath, "connected"), "必须为布尔值", targetName)); } if (expect["durationMs"] !== undefined) { issues.push(...validateRawValueExpectation(expect["durationMs"], joinPath(expectPath, "durationMs"), targetName)); } if (expect["banner"] !== undefined) { if (!readBanner) { issues.push( issue("invalid-value", joinPath(expectPath, "banner"), "banner 断言需要启用 tcp.readBanner", targetName), ); } else { issues.push(...validateRawContentExpectations(expect["banner"], joinPath(expectPath, "banner"), targetName)); } } const allowedKeys = new Set(["banner", "connected", "durationMs"]); for (const key of Object.keys(expect)) { if (!allowedKeys.has(key)) { issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName)); } } return issues; } function validateTcpTarget(target: Record, path: string): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; const targetName = getTargetName(target); const tcp = target["tcp"]; if (!isPlainRecord(tcp)) { issues.push(issue("required", joinPath(path, "tcp"), "缺少 tcp 配置分组", targetName)); issues.push(...validateTcpExpect(target, path, false)); return issues; } if (!isString(tcp["host"]) || tcp["host"].trim() === "") { issues.push(issue("required", joinPath(joinPath(path, "tcp"), "host"), "缺少 tcp.host 字段", targetName)); } if (tcp["port"] === undefined) { issues.push(issue("required", joinPath(joinPath(path, "tcp"), "port"), "缺少 tcp.port 字段", targetName)); } else if (!isNumber(tcp["port"]) || !Number.isInteger(tcp["port"]) || tcp["port"] < 1 || tcp["port"] > 65535) { issues.push( issue("invalid-value", joinPath(joinPath(path, "tcp"), "port"), "必须为 1-65535 之间的整数", targetName), ); } if (tcp["readBanner"] !== undefined && typeof tcp["readBanner"] !== "boolean") { issues.push(issue("invalid-type", joinPath(joinPath(path, "tcp"), "readBanner"), "必须为布尔值", targetName)); } if (tcp["bannerReadTimeout"] !== undefined && !isNonNegativeFiniteNumber(tcp["bannerReadTimeout"])) { issues.push( issue("invalid-type", joinPath(joinPath(path, "tcp"), "bannerReadTimeout"), "必须为非负有限数字", targetName), ); } if (tcp["maxBannerBytes"] !== undefined) { if ( !isString(tcp["maxBannerBytes"]) && !(isNumber(tcp["maxBannerBytes"]) && Number.isFinite(tcp["maxBannerBytes"]) && tcp["maxBannerBytes"] >= 0) ) { issues.push( issue("invalid-value", joinPath(joinPath(path, "tcp"), "maxBannerBytes"), "必须为合法 size 值", targetName), ); } } const allowedTcpKeys = new Set(["bannerReadTimeout", "host", "maxBannerBytes", "port", "readBanner"]); for (const key of Object.keys(tcp)) { if (!allowedTcpKeys.has(key)) { issues.push(issue("unknown-field", joinPath(joinPath(path, "tcp"), key), "是未知字段", targetName)); } } const readBanner = tcp["readBanner"] === true; issues.push(...validateTcpExpect(target, path, readBanner)); return issues; }