From ec962a0d6a5cb06b30ba2243f1c75287b51dc555 Mon Sep 17 00:00:00 2001 From: chyzwar Date: Mon, 1 Jun 2026 14:41:43 +0200 Subject: [PATCH] fix(conf): stop INI parser corrupting octal and 0/1 values readValue coerced any numeric-looking string to a number and mapped 1/0 to booleans. UMask=0077 became 77 (then failed z.string()), and RestartSec=1 / OOMScoreAdjust=0 became booleans the numeric schemas rejected, so Service.fromINI threw on valid units. Now coerce to a number only when String(n) === trimmed, and drop the 1/0->boolean branches. Octal/leading-zero/hex and infinity tokens stay strings for the Zod schema, which knows each field's real type. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/conf/src/__tests__/Ini.test.ts | 40 +++++++++++++++++++++++++ packages/conf/src/ini.ts | 14 ++++----- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/conf/src/__tests__/Ini.test.ts b/packages/conf/src/__tests__/Ini.test.ts index 17ba0eb..065813a 100644 --- a/packages/conf/src/__tests__/Ini.test.ts +++ b/packages/conf/src/__tests__/Ini.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { INI } from "../ini.js"; +import { Service } from "../service.js"; describe("INI - fromObject and toObject", () => { test("should return same object", () => { @@ -130,3 +131,42 @@ describe("INI - fromString and toString", () => { expect(result.trim()).toStrictEqual(dataIni.trim()); }); }); + +describe("INI - value coercion", () => { + test("should keep octal / leading-zero values as strings", () => { + const result = INI.fromString("[X]\nUMask=0077").toObject(); + + expect(result).toEqual({ X: { UMask: "0077" } }); + }); + + test("should parse 0 and 1 as numbers, not booleans", () => { + const result = INI.fromString("[X]\nA=0\nB=1\nC=200").toObject(); + + expect(result).toEqual({ X: { A: 0, B: 1, C: 200 } }); + }); + + test("should keep infinity tokens as strings", () => { + const result = INI.fromString("[X]\nA=infinity\nB=Infinity\nC=-infinity").toObject(); + + expect(result).toEqual({ X: { A: "infinity", B: "Infinity", C: "-infinity" } }); + }); + + test("should not throw on a unit with UMask / RestartSec=1 / OOMScoreAdjust=0", () => { + const unit = [ + "[Unit]", + "Description=x", + "[Service]", + "ExecStart=/bin/true", + "UMask=0022", + "RestartSec=1", + "OOMScoreAdjust=0", + ].join("\n"); + + const service = Service.fromINI(INI.fromString(unit)); + const ini = service.toINIString(); + + expect(ini).toContain("UMask=0022"); + expect(ini).toContain("RestartSec=1"); + expect(ini).toContain("OOMScoreAdjust=0"); + }); +}); diff --git a/packages/conf/src/ini.ts b/packages/conf/src/ini.ts index 4dd636e..0aed5ad 100644 --- a/packages/conf/src/ini.ts +++ b/packages/conf/src/ini.ts @@ -7,22 +7,18 @@ function readValue(value: string): boolean | number | string { ) { return true; } - if (trimmed === "1") { - console.warn("Ambiguous boolean value: 1, use yes or true instead"); - return true; - } if ( trimmed === "false" || trimmed === "no" || trimmed === "off" ) { return false; } - if (trimmed === "0") { - console.warn("Ambiguous boolean value: 0, use no or false instead"); - return false; - } + // Only coerce when it round-trips exactly, so octal/leading-zero values + // (UMask=0077), hex, and "0"/"1" reach the Zod schema, which knows the type. const numberValue = Number(trimmed); - return isFinite(numberValue) ? numberValue : trimmed; + return Number.isFinite(numberValue) && String(numberValue) === trimmed + ? numberValue + : trimmed; } /**