commit 5bbe5b792e28954a76dae0aa38f16bf9da948e03
parent 03c36d57c23f6d21bbfbe46910577a2960035761
Author: Florian Dold <dold@taler.net>
Date: Tue, 21 Jul 2026 20:03:10 +0200
util: escape, quote and preserve names when writing a config back
A newline in a value emitted further configuration lines, leading and
trailing whitespace was lost for want of quoting, and section and option
names were upper-cased over the author's own file.
Diffstat:
2 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/packages/taler-util/src/talerconfig.test.ts b/packages/taler-util/src/talerconfig.test.ts
@@ -171,7 +171,7 @@ test("a commented-out option stays a comment with CRLF line endings", (t) => {
);
assert.strictEqual(
config.stringify(),
- "[EXCHANGE]\nBASE_URL = https://real/\n\n",
+ "[exchange]\nBASE_URL = https://real/\n\n",
);
});
@@ -204,7 +204,7 @@ test("% introduces a comment, as in the C implementation", (t) => {
);
assert.strictEqual(
config.stringify(),
- "[EXCHANGE]\nBASE_URL = https://real/\n\n",
+ "[exchange]\nBASE_URL = https://real/\n\n",
);
});
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
@@ -61,6 +61,11 @@ enum EntryOrigin {
}
interface Entry {
+ /**
+ * Option name as it was first written. Lookups are case insensitive, but
+ * write-back overwrites the user's own file and must not respell it.
+ */
+ name: string;
value: string;
sourceLine: number;
sourceFile: string;
@@ -68,6 +73,10 @@ interface Entry {
}
interface Section {
+ /**
+ * Section name as it was first written; see Entry.name.
+ */
+ name: string;
secretFilename?: string;
inaccessible: boolean;
entries: { [optionName: string]: Entry };
@@ -398,6 +407,22 @@ function compareFilenames(a: string, b: string): number {
return ba.length - bb.length;
}
+/**
+ * Render a value so that reading it back yields the same string.
+ *
+ * A newline would otherwise emit further configuration lines, which is how a
+ * value turns into an option the author never wrote; the C rewrites it to a
+ * literal backslash-n. Leading and trailing whitespace only survives inside
+ * quotes, which the reader strips again.
+ */
+function serializeValue(value: string): string {
+ const escaped = value.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
+ if (escaped !== escaped.trim() || /^".*"$/.test(escaped)) {
+ return `"${escaped}"`;
+ }
+ return escaped;
+}
+
export class Configuration {
private sectionMap: SectionMap = {};
@@ -619,7 +644,8 @@ export class Configuration {
`invalid configuration, expected section header in ${fn}:${lineNo}`,
);
}
- currentSection = currentSection.toUpperCase();
+ // Kept as written: provideSection() upper-cases for the lookup key, and
+ // the original spelling is what write-back emits.
const paramMatch = line.match(reParam);
if (paramMatch) {
const optName = paramMatch[1].toUpperCase();
@@ -629,6 +655,7 @@ export class Configuration {
}
const sec = this.provideSection(currentSection);
sec.entries[optName] = {
+ name: paramMatch[1],
value: val,
sourceFile: opts.filename ?? "<unknown>",
sourceLine: lineNo,
@@ -650,6 +677,7 @@ export class Configuration {
return this.sectionMap[secNorm];
}
const newSec: Section = {
+ name: section,
entries: {},
inaccessible: false,
};
@@ -679,6 +707,7 @@ export class Configuration {
setString(section: string, option: string, value: string): void {
const sec = this.provideSection(section);
sec.entries[option.toUpperCase()] = {
+ name: option,
value,
sourceLine: 0,
sourceFile: "<unknown>",
@@ -696,6 +725,7 @@ export class Configuration {
): void {
const sec = this.provideSection(section);
sec.entries[option.toUpperCase()] = {
+ name: option,
value,
sourceLine: 0,
sourceFile: "<unknown>",
@@ -949,6 +979,8 @@ export class Configuration {
}
for (const sectionName of Object.keys(this.sectionMap)) {
const sec = this.sectionMap[sectionName];
+ // Names are written the way the author spelled them, not upper-cased;
+ // write() overwrites the entry point config file in place.
let headerWritten = false;
for (const optionName of Object.keys(sec.entries)) {
const entry = this.sectionMap[sectionName].entries[optionName];
@@ -964,7 +996,7 @@ export class Configuration {
s += `# Secret section from ${sec.secretFilename}\n`;
s += `# Secret accessible: ${!sec.inaccessible}\n`;
}
- s += `[${sectionName}]\n`;
+ s += `[${sec.name}]\n`;
headerWritten = true;
}
if (entry !== undefined) {
@@ -980,7 +1012,7 @@ export class Configuration {
break;
}
}
- s += `${optionName} = ${entry.value}\n`;
+ s += `${entry.name} = ${serializeValue(entry.value)}\n`;
}
}
if (headerWritten) {