taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/talerconfig.ts | 40+++++++++++++++++++++++++++++++++-------
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);