refactor: centralize strict numeric parsing

This commit is contained in:
Peter Steinberger 2026-03-08 02:52:44 +00:00
parent 3087893ef9
commit 2646739d23
11 changed files with 128 additions and 30 deletions

View File

@ -30,6 +30,7 @@ import {
listSkillCommandsForAgents,
type HistoryEntry,
} from "openclaw/plugin-sdk/mattermost";
import { parseStrictPositiveInteger } from "../../../../src/infra/parse-finite-number.js";
import { getMattermostRuntime } from "../runtime.js";
import { resolveMattermostAccount } from "./accounts.js";
import {
@ -348,9 +349,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
// The gateway sets OPENCLAW_GATEWAY_PORT when it boots, but the config file may still contain
// a different port.
const envPortRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim();
const envPort = envPortRaw ? Number.parseInt(envPortRaw, 10) : NaN;
const slashGatewayPort =
Number.isFinite(envPort) && envPort > 0 ? envPort : (cfg.gateway?.port ?? 18789);
const envPort = parseStrictPositiveInteger(envPortRaw);
const slashGatewayPort = envPort ?? cfg.gateway?.port ?? 18789;
const slashCallbackUrl = resolveCallbackUrl({
config: slashConfig,

View File

@ -27,6 +27,7 @@ import {
} from "../../gateway/credentials.js";
import { resolveGatewayBindHost } from "../../gateway/net.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../../gateway/resolve-configured-secret-input-string.js";
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
import {
formatPortDiagnostics,
inspectPortUsage,
@ -308,8 +309,7 @@ export async function gatherDaemonStatus(
{ deep: Boolean(opts.deep) },
).catch(() => []);
const timeoutMsRaw = Number.parseInt(String(opts.rpc.timeout ?? "10000"), 10);
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 10_000;
const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? "10000") ?? 10_000;
const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true;
const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled;

View File

@ -1,19 +1,8 @@
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
export function parsePort(raw: unknown): number | null {
if (raw === undefined || raw === null) {
return null;
}
const value =
typeof raw === "string"
? raw
: typeof raw === "number" || typeof raw === "bigint"
? raw.toString()
: null;
if (value === null) {
return null;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return parsed;
return parseStrictPositiveInteger(raw) ?? null;
}

View File

@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { resolveCacheTtlMs } from "./cache-utils.js";
describe("resolveCacheTtlMs", () => {
it("accepts exact non-negative integers", () => {
expect(resolveCacheTtlMs({ envValue: "0", defaultTtlMs: 60_000 })).toBe(0);
expect(resolveCacheTtlMs({ envValue: "120000", defaultTtlMs: 60_000 })).toBe(120_000);
});
it("rejects malformed env values and falls back to the default", () => {
expect(resolveCacheTtlMs({ envValue: "0abc", defaultTtlMs: 60_000 })).toBe(60_000);
expect(resolveCacheTtlMs({ envValue: "15ms", defaultTtlMs: 60_000 })).toBe(60_000);
});
});

View File

@ -1,4 +1,5 @@
import fs from "node:fs";
import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js";
export function resolveCacheTtlMs(params: {
envValue: string | undefined;
@ -6,8 +7,8 @@ export function resolveCacheTtlMs(params: {
}): number {
const { envValue, defaultTtlMs } = params;
if (envValue) {
const parsed = Number.parseInt(envValue, 10);
if (Number.isFinite(parsed) && parsed >= 0) {
const parsed = parseStrictNonNegativeInteger(envValue);
if (parsed !== undefined) {
return parsed;
}
}

View File

@ -122,6 +122,19 @@ describe("launchd runtime parsing", () => {
expect(info.pid).toBeUndefined();
expect(info.state).toBe("waiting");
});
it("rejects pid and exit status values with junk suffixes", () => {
const output = [
"state = waiting",
"pid = 123abc",
"last exit status = 7ms",
"last exit reason = exited",
].join("\n");
expect(parseLaunchctlPrint(output)).toEqual({
state: "waiting",
lastExitReason: "exited",
});
});
});
describe("launchctl list detection", () => {

View File

@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parseStrictInteger, parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
import {
GATEWAY_LAUNCH_AGENT_LABEL,
resolveGatewayServiceDescription,
@ -127,15 +128,15 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo {
}
const pidValue = entries.pid;
if (pidValue) {
const pid = Number.parseInt(pidValue, 10);
if (Number.isFinite(pid) && pid > 0) {
const pid = parseStrictPositiveInteger(pidValue);
if (pid !== undefined) {
info.pid = pid;
}
}
const exitStatusValue = entries["last exit status"];
if (exitStatusValue) {
const status = Number.parseInt(exitStatusValue, 10);
if (Number.isFinite(status)) {
const status = parseStrictInteger(exitStatusValue);
if (status !== undefined) {
info.lastExitStatus = status;
}
}

View File

@ -351,6 +351,21 @@ describe("systemd runtime parsing", () => {
execMainCode: "exited",
});
});
it("rejects pid and exit status values with junk suffixes", () => {
const output = [
"ActiveState=inactive",
"SubState=dead",
"MainPID=42abc",
"ExecMainStatus=2ms",
"ExecMainCode=exited",
].join("\n");
expect(parseSystemdShow(output)).toEqual({
activeState: "inactive",
subState: "dead",
execMainCode: "exited",
});
});
});
describe("resolveSystemdUserUnitPath", () => {

View File

@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { parseStrictInteger, parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
import { splitArgsPreservingQuotes } from "./arg-split.js";
import {
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
@ -231,15 +232,15 @@ export function parseSystemdShow(output: string): SystemdServiceInfo {
}
const mainPidValue = entries.mainpid;
if (mainPidValue) {
const pid = Number.parseInt(mainPidValue, 10);
if (Number.isFinite(pid) && pid > 0) {
const pid = parseStrictPositiveInteger(mainPidValue);
if (pid !== undefined) {
info.mainPid = pid;
}
}
const execMainStatusValue = entries.execmainstatus;
if (execMainStatusValue) {
const status = Number.parseInt(execMainStatusValue, 10);
if (Number.isFinite(status)) {
const status = parseStrictInteger(execMainStatusValue);
if (status !== undefined) {
info.execMainStatus = status;
}
}

View File

@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { parseFiniteNumber } from "./parse-finite-number.js";
import {
parseFiniteNumber,
parseStrictInteger,
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "./parse-finite-number.js";
describe("parseFiniteNumber", () => {
it("returns finite numbers", () => {
@ -17,3 +22,32 @@ describe("parseFiniteNumber", () => {
expect(parseFiniteNumber(null)).toBeUndefined();
});
});
describe("parseStrictInteger", () => {
it("parses exact integers", () => {
expect(parseStrictInteger("42")).toBe(42);
expect(parseStrictInteger(" -7 ")).toBe(-7);
});
it("rejects junk prefixes and suffixes", () => {
expect(parseStrictInteger("42ms")).toBeUndefined();
expect(parseStrictInteger("0abc")).toBeUndefined();
expect(parseStrictInteger("1.5")).toBeUndefined();
});
});
describe("parseStrictPositiveInteger", () => {
it("accepts only positive integers", () => {
expect(parseStrictPositiveInteger("9")).toBe(9);
expect(parseStrictPositiveInteger("0")).toBeUndefined();
expect(parseStrictPositiveInteger("-1")).toBeUndefined();
});
});
describe("parseStrictNonNegativeInteger", () => {
it("accepts zero and positive integers only", () => {
expect(parseStrictNonNegativeInteger("0")).toBe(0);
expect(parseStrictNonNegativeInteger("9")).toBe(9);
expect(parseStrictNonNegativeInteger("-1")).toBeUndefined();
});
});

View File

@ -1,3 +1,8 @@
function normalizeNumericString(value: string): string | undefined {
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
export function parseFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
@ -10,3 +15,28 @@ export function parseFiniteNumber(value: unknown): number | undefined {
}
return undefined;
}
export function parseStrictInteger(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isSafeInteger(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = normalizeNumericString(value);
if (!normalized || !/^[+-]?\d+$/.test(normalized)) {
return undefined;
}
const parsed = Number(normalized);
return Number.isSafeInteger(parsed) ? parsed : undefined;
}
export function parseStrictPositiveInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed > 0 ? parsed : undefined;
}
export function parseStrictNonNegativeInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed >= 0 ? parsed : undefined;
}