From 91bacc5c14fe757d746fa66789e34da293d382b2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:14:49 +0800 Subject: [PATCH] refactor(config): share bounded decimal setting validation --- src/config/bridgeMaxRequestBytes.ts | 24 ++++++------------------ src/config/captureDecoderHeap.ts | 25 +++++++------------------ src/config/mcpStartupBudgets.ts | 26 ++++++-------------------- src/config/positiveDecimalInteger.ts | 23 +++++++++++++++++++++++ src/config/regexMatchingDeadline.ts | 21 ++++++--------------- src/config/regexWorkerHeapRequest.ts | 24 ++++++------------------ 6 files changed, 54 insertions(+), 89 deletions(-) create mode 100644 src/config/positiveDecimalInteger.ts diff --git a/src/config/bridgeMaxRequestBytes.ts b/src/config/bridgeMaxRequestBytes.ts index 6c8ffc2fd8..7d9674622d 100644 --- a/src/config/bridgeMaxRequestBytes.ts +++ b/src/config/bridgeMaxRequestBytes.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; @@ -64,24 +64,12 @@ export type BridgePeer = "Hopper" | "Cutter" | "Ghidra"; export const bridgeMaxRequestBytesConstraint = `${BRIDGE_MAX_REQUEST_BYTES_SETTING} must be a positive decimal integer from ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} to ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes; the default is ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)} bytes (16 MiB). A value below ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} bytes cannot carry the largest admitted symbol-name command, and a larger value is refused so the encoded payload plus its framing LF stays one string under Node's single-string ceiling.`; -const decimalInteger = /^[1-9]\d*$/u; - -const withinRequestRange = (value: string): boolean => { - // The pattern check reports a non-decimal value. Refine only bounds integers. - if (!decimalInteger.test(value)) return true; - const parsed = BigInt(value); - return ( - parsed >= BigInt(MINIMUM_BRIDGE_MAX_REQUEST_BYTES) && - parsed <= BigInt(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES) - ); -}; - /** Optional decimal byte count for the Hopper and Cutter request payload ceiling. */ -export const bridgeMaxRequestBytesSchema = z - .string() - .regex(decimalInteger, { error: bridgeMaxRequestBytesConstraint }) - .refine(withinRequestRange, { error: bridgeMaxRequestBytesConstraint }) - .transform(Number); +export const bridgeMaxRequestBytesSchema = positiveDecimalIntegerSchema( + MINIMUM_BRIDGE_MAX_REQUEST_BYTES, + MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, + bridgeMaxRequestBytesConstraint, +); /** Resolve the effective ceiling from one environment value. */ export const parseBridgeMaxRequestBytes = ( diff --git a/src/config/captureDecoderHeap.ts b/src/config/captureDecoderHeap.ts index 1a3fb352ec..31abfb6025 100644 --- a/src/config/captureDecoderHeap.ts +++ b/src/config/captureDecoderHeap.ts @@ -3,7 +3,7 @@ import { CAPTURE_DECODER_SEMI_SPACE_MIB, MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB, } from "../domain/analysisResourceLimits.js"; -import { z } from "zod"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -24,24 +24,13 @@ export const DEFAULT_CAPTURE_DECODER_HEAP: CaptureDecoderHeap = { export const captureDecoderHeapConstraint = `${CAPTURE_DECODER_OLD_GENERATION_SETTING} must be a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB; the default is ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB. An old generation below the decoder's ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space aborts Node during startup instead of enforcing a limit, and a larger value is refused.`; -const decimalInteger = /^[1-9]\d*$/u; - -const withinHeapRange = (value: string): boolean => { - // The pattern check reports a non-decimal value. Refine only bounds integers. - if (!decimalInteger.test(value)) return true; - const parsed = BigInt(value); - return ( - parsed >= BigInt(CAPTURE_DECODER_SEMI_SPACE_MIB) && - parsed <= BigInt(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB) - ); -}; - /** Optional decimal mebibyte count for the HAR/SAZ/qlog old-generation heap. */ -export const captureDecoderOldGenerationMiBSchema = z - .string() - .regex(decimalInteger, { error: captureDecoderHeapConstraint }) - .refine(withinHeapRange, { error: captureDecoderHeapConstraint }) - .transform(Number); +export const captureDecoderOldGenerationMiBSchema = + positiveDecimalIntegerSchema( + CAPTURE_DECODER_SEMI_SPACE_MIB, + MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB, + captureDecoderHeapConstraint, + ); /** Resolve the effective heap from one environment value. */ export const parseCaptureDecoderHeap = ( diff --git a/src/config/mcpStartupBudgets.ts b/src/config/mcpStartupBudgets.ts index 5753661dbc..cf62a3c56c 100644 --- a/src/config/mcpStartupBudgets.ts +++ b/src/config/mcpStartupBudgets.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -69,26 +69,12 @@ const PHASES = [ const phaseConstraint = (phase: McpPhaseBudgetDefinition): string => `${phase.setting} must be a positive decimal integer from ${String(MCP_PHASE_BUDGET_FLOOR_MS)} to ${String(MCP_PHASE_BUDGET_MAXIMUM_MS)} ms; the default is ${String(phase.defaultMilliseconds)} ms. Node schedules a delay above that maximum as 1 ms, so a larger value is refused.`; -const decimalInteger = /^[1-9]\d*$/u; - -const withinPhaseBudgetRange = (value: string): boolean => { - // The pattern check reports a non-decimal value. Refine only bounds integers. - if (!decimalInteger.test(value)) return true; - const parsed = BigInt(value); - return ( - parsed >= BigInt(MCP_PHASE_BUDGET_FLOOR_MS) && - parsed <= BigInt(MCP_PHASE_BUDGET_MAXIMUM_MS) +const phaseBudgetSchema = (phase: McpPhaseBudgetDefinition) => + positiveDecimalIntegerSchema( + MCP_PHASE_BUDGET_FLOOR_MS, + MCP_PHASE_BUDGET_MAXIMUM_MS, + phaseConstraint(phase), ); -}; - -const phaseBudgetSchema = (phase: McpPhaseBudgetDefinition) => { - const constraint = phaseConstraint(phase); - return z - .string() - .regex(decimalInteger, { error: constraint }) - .refine(withinPhaseBudgetRange, { error: constraint }) - .transform(Number); -}; /** Optional decimal millisecond count for the MCP initialize phase budget. */ export const mcpInitializeBudgetMsSchema = phaseBudgetSchema(PHASES[0]); diff --git a/src/config/positiveDecimalInteger.ts b/src/config/positiveDecimalInteger.ts new file mode 100644 index 0000000000..9d406ddd6a --- /dev/null +++ b/src/config/positiveDecimalInteger.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +const decimalInteger = /^[1-9]\d*$/u; + +/** Preserve exact decimal bounds before conversion to a runtime number. */ +export const positiveDecimalIntegerSchema = ( + minimum: number, + maximum: number, + constraint: string, +) => + z + .string() + .regex(decimalInteger, { error: constraint }) + .refine( + (value) => { + // The pattern owns malformed input; range checks only interpret integers. + if (!decimalInteger.test(value)) return true; + const parsed = BigInt(value); + return parsed >= BigInt(minimum) && parsed <= BigInt(maximum); + }, + { error: constraint }, + ) + .transform(Number); diff --git a/src/config/regexMatchingDeadline.ts b/src/config/regexMatchingDeadline.ts index 6c8174eb0a..650748040b 100644 --- a/src/config/regexMatchingDeadline.ts +++ b/src/config/regexMatchingDeadline.ts @@ -2,7 +2,7 @@ import { DEFAULT_REGEX_MATCHING_DEADLINE_MS, MAXIMUM_REGEX_MATCHING_DEADLINE_MS, } from "../domain/analysisResourceLimits.js"; -import { z } from "zod"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -22,21 +22,12 @@ export const DEFAULT_REGEX_MATCHING_DEADLINE: RegexMatchingDeadline = { export const regexMatchingDeadlineConstraint = `${REGEX_MATCHING_DEADLINE_SETTING} must be a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms; the default is ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms. Node schedules a delay above that maximum as 1 ms, so a larger value is refused.`; -const decimalInteger = /^[1-9]\d*$/u; - -const withinDeadlineRange = (value: string): boolean => { - // The pattern check reports a non-decimal value. Refine only bounds integers. - if (!decimalInteger.test(value)) return true; - const parsed = BigInt(value); - return parsed >= 1n && parsed <= BigInt(MAXIMUM_REGEX_MATCHING_DEADLINE_MS); -}; - /** Optional decimal millisecond count for the supervised regex matching deadline. */ -export const regexMatchingDeadlineMsSchema = z - .string() - .regex(decimalInteger, { error: regexMatchingDeadlineConstraint }) - .refine(withinDeadlineRange, { error: regexMatchingDeadlineConstraint }) - .transform(Number); +export const regexMatchingDeadlineMsSchema = positiveDecimalIntegerSchema( + 1, + MAXIMUM_REGEX_MATCHING_DEADLINE_MS, + regexMatchingDeadlineConstraint, +); /** Resolve the effective deadline from one environment value. */ export const parseRegexMatchingDeadline = ( diff --git a/src/config/regexWorkerHeapRequest.ts b/src/config/regexWorkerHeapRequest.ts index 768465353a..3e8a4d35de 100644 --- a/src/config/regexWorkerHeapRequest.ts +++ b/src/config/regexWorkerHeapRequest.ts @@ -3,7 +3,7 @@ import { MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB, MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB, } from "../domain/analysisResourceLimits.js"; -import { z } from "zod"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -24,24 +24,12 @@ export const DEFAULT_REGEX_WORKER_HEAP_REQUEST: RegexWorkerHeapRequest = { export const regexWorkerHeapRequestConstraint = `${REGEX_WORKER_HEAP_REQUEST_SETTING} must be a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB; the default is ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB. A request below ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB aborts the worker during startup, and a larger value is refused.`; -const decimalInteger = /^[1-9]\d*$/u; - -const withinHeapRange = (value: string): boolean => { - // The pattern check reports a non-decimal value. Refine only bounds integers. - if (!decimalInteger.test(value)) return true; - const parsed = BigInt(value); - return ( - parsed >= BigInt(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB) && - parsed <= BigInt(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB) - ); -}; - /** Optional decimal mebibyte count for the regex worker old-generation request. */ -export const regexWorkerHeapRequestMiBSchema = z - .string() - .regex(decimalInteger, { error: regexWorkerHeapRequestConstraint }) - .refine(withinHeapRange, { error: regexWorkerHeapRequestConstraint }) - .transform(Number); +export const regexWorkerHeapRequestMiBSchema = positiveDecimalIntegerSchema( + MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB, + MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB, + regexWorkerHeapRequestConstraint, +); /** Resolve the effective request from one environment value. */ export const parseRegexWorkerHeapRequest = (