commit e27e5c05841338635c551b4f135f65d2ba78f525
parent 26acdb9af3ac22fc575841ea97719e410a16e419
Author: Florian Dold <dold@taler.net>
Date: Tue, 21 Jul 2026 19:16:07 +0200
util: load config directories in sorted order
Later files override earlier ones, so an unsorted directory listing made
the effective configuration of a conf.d directory depend on the filesystem.
Sorting is by UTF-8 byte order, matching the C's strcmp.
Diffstat:
1 file changed, 33 insertions(+), 7 deletions(-)
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
@@ -258,7 +258,11 @@ export function pathsub(
l = start + r.length;
continue;
} else if (defaultValue !== undefined) {
- const resolvedDefault = pathsub(defaultValue, lookup, recursionDepth + 1);
+ const resolvedDefault = pathsub(
+ defaultValue,
+ lookup,
+ recursionDepth + 1,
+ );
s = s.substring(0, start) + resolvedDefault + s.substring(p + 1);
l = start + resolvedDefault.length;
continue;
@@ -370,6 +374,26 @@ function which(name: string): string | undefined {
return undefined;
}
+const utf8Encoder = new TextEncoder();
+
+/**
+ * Order two file names the way the C reference does. It sorts glob and
+ * directory-scan results with strcmp, i.e. by UTF-8 byte order, whereas
+ * JavaScript's default sort compares UTF-16 code units; the two disagree once
+ * a name contains an astral character.
+ */
+function compareFilenames(a: string, b: string): number {
+ const ba = utf8Encoder.encode(a);
+ const bb = utf8Encoder.encode(b);
+ const n = Math.min(ba.length, bb.length);
+ for (let i = 0; i < n; i++) {
+ if (ba[i] !== bb[i]) {
+ return ba[i] - bb[i];
+ }
+ }
+ return ba.length - bb.length;
+}
+
export class Configuration {
private sectionMap: SectionMap = {};
@@ -448,12 +472,14 @@ export class Configuration {
const head = nodejs_path.dirname(fullFileglob);
const tail = nodejs_path.basename(fullFileglob);
- const files = nodejs_fs.readdirSync(head);
+ // Later files override earlier ones, so the order has to be the same
+ // everywhere; the C reference qsorts its glob results for this reason.
+ const files = nodejs_fs
+ .readdirSync(head)
+ .filter((f) => globMatch(tail, f))
+ .sort(compareFilenames);
for (const f of files) {
- if (globMatch(tail, f)) {
- const fullPath = nodejs_path.join(head, f);
- this.loadFromFilename(fullPath, isDefaultSource);
- }
+ this.loadFromFilename(nodejs_path.join(head, f), isDefaultSource);
}
}
@@ -753,7 +779,7 @@ export class Configuration {
private loadDefaultsFromDir(dirname: string): void {
const exists = nodejs_fs.existsSync(dirname);
if (!exists) return;
- const files = nodejs_fs.readdirSync(dirname);
+ const files = nodejs_fs.readdirSync(dirname).sort(compareFilenames);
for (const f of files) {
const fn = nodejs_path.join(dirname, f);
this.loadFromFilename(fn, true);