diff options
Diffstat (limited to 'packages/taler-util/src/talerconfig.ts')
-rw-r--r-- | packages/taler-util/src/talerconfig.ts | 324 |
1 files changed, 217 insertions, 107 deletions
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts index 37aace10a..83c0044be 100644 --- a/packages/taler-util/src/talerconfig.ts +++ b/packages/taler-util/src/talerconfig.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2020 Taler Systems S.A. + (C) 2020-2023 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -25,51 +25,13 @@ */ import { AmountJson } from "./amounts.js"; import { Amounts } from "./amounts.js"; +import { Logger } from "./logging.js"; -const nodejs_fs = (function () { - let fs: typeof import("fs"); - return function () { - if (!fs) { - /** - * need to use an expression when doing a require if we want - * webpack not to find out about the requirement - */ - const _r = "require"; - fs = module[_r]("fs"); - } - return fs; - }; -})(); - -const nodejs_path = (function () { - let path: typeof import("path"); - return function () { - if (!path) { - /** - * need to use an expression when doing a require if we want - * webpack not to find out about the requirement - */ - const _r = "require"; - path = module[_r]("path"); - } - return path; - }; -})(); - -const nodejs_os = (function () { - let os: typeof import("os"); - return function () { - if (!os) { - /** - * need to use an expression when doing a require if we want - * webpack not to find out about the requirement - */ - const _r = "require"; - os = module[_r]("os"); - } - return os; - }; -})(); +import nodejs_path from "path"; +import nodejs_os from "os"; +import nodejs_fs from "fs"; + +const logger = new Logger("talerconfig.ts"); export class ConfigError extends Error { constructor(message: string) { @@ -80,10 +42,30 @@ export class ConfigError extends Error { } } +enum EntryOrigin { + /** + * From a default file. + */ + DefaultFile = 1, + /** + * From a system/installation specific default value. + */ + DefaultSystem = 2, + /** + * Loaded from file or string + */ + Loaded = 3, + /** + * Changed after loading + */ + Changed = 4, +} + interface Entry { value: string; sourceLine: number; sourceFile: string; + origin: EntryOrigin; } interface Section { @@ -98,7 +80,7 @@ export class ConfigValue<T> { constructor( private sectionName: string, private optionName: string, - public value: string | undefined, + private value: string | undefined, private converter: (x: string) => T, ) {} @@ -130,6 +112,10 @@ export class ConfigValue<T> { isDefined(): boolean { return this.value !== undefined; } + + getValue(): string | undefined { + return this.value; + } } /** @@ -138,10 +124,10 @@ export class ConfigValue<T> { */ export function expandPath(path: string): string { if (path[0] === "~") { - path = nodejs_path().join(nodejs_os().homedir(), path.slice(1)); + path = nodejs_path.join(nodejs_os.homedir(), path.slice(1)); } if (path[0] !== "/") { - path = nodejs_path().join(process.cwd(), path); + path = nodejs_path.join(process.cwd(), path); } return path; } @@ -157,9 +143,9 @@ export function expandPath(path: string): string { export function pathsub( x: string, lookup: (s: string, depth: number) => string | undefined, - depth = 0, + recursionDepth = 0, ): string { - if (depth >= 10) { + if (recursionDepth >= 128) { throw Error("recursion in path substitution"); } let s = x; @@ -198,14 +184,14 @@ export function pathsub( defaultValue = undefined; } - const r = lookup(inner, depth + 1); + const r = lookup(varname, depth + 1); if (r !== undefined) { - s = s.substr(0, start) + r + s.substr(p + 1); + s = s.substring(0, start) + r + s.substring(p + 1); l = start + r.length; continue; } else if (defaultValue !== undefined) { const resolvedDefault = pathsub(defaultValue, lookup, depth + 1); - s = s.substr(0, start) + resolvedDefault + s.substr(p + 1); + s = s.substring(0, start) + resolvedDefault + s.substring(p + 1); l = start + resolvedDefault.length; continue; } @@ -215,9 +201,9 @@ export function pathsub( } else { const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1)); if (m && m[0]) { - const r = lookup(m[0], depth + 1); + const r = lookup(m[0], recursionDepth + 1); if (r !== undefined) { - s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length); + s = s.substring(0, l) + r + s.substring(l + 1 + m[0].length); l = l + r.length; continue; } @@ -236,6 +222,7 @@ export interface LoadOptions { export interface StringifyOptions { diagnostics?: boolean; + excludeDefaults?: boolean; } export interface LoadedFile { @@ -288,15 +275,15 @@ function normalizeInlineFilename(parentFile: string, f: string): string { if (f[0] === "/") { return f; } - const resolvedParentDir = nodejs_path().dirname( - nodejs_fs().realpathSync(parentFile), + const resolvedParentDir = nodejs_path.dirname( + nodejs_fs.realpathSync(parentFile), ); - return nodejs_path().join(resolvedParentDir, f); + return nodejs_path.join(resolvedParentDir, f); } /** * Crude implementation of the which(1) shell command. - * + * * Tries to locate the location of an executable based on the * "PATH" environment variable. */ @@ -306,8 +293,8 @@ function which(name: string): string | undefined { return undefined; } for (const path of paths) { - const filename = nodejs_path().join(path, name); - if (nodejs_fs().existsSync(filename)) { + const filename = nodejs_path.join(path, name); + if (nodejs_fs.existsSync(filename)) { return filename; } } @@ -323,7 +310,11 @@ export class Configuration { private nestLevel = 0; - private loadFromFilename(filename: string, opts: LoadOptions = {}): void { + private loadFromFilename( + filename: string, + isDefaultSource: boolean, + opts: LoadOptions = {}, + ): void { filename = expandPath(filename); const checkCycle = () => { @@ -342,7 +333,7 @@ export class Configuration { checkCycle(); - const s = nodejs_fs().readFileSync(filename, "utf-8"); + const s = nodejs_fs.readFileSync(filename, "utf-8"); this.loadedFiles.push({ filename: filename, level: this.nestLevel, @@ -350,7 +341,7 @@ export class Configuration { const oldNestLevel = this.nestLevel; this.nestLevel += 1; try { - this.loadFromString(s, { + this.internalLoadFromString(s, isDefaultSource, { ...opts, filename: filename, }); @@ -359,43 +350,51 @@ export class Configuration { } } - private loadGlob(parentFilename: string, fileglob: string): void { - const resolvedParent = nodejs_fs().realpathSync(parentFilename); - const parentDir = nodejs_path().dirname(resolvedParent); + private loadGlob( + parentFilename: string, + isDefaultSource: boolean, + fileglob: string, + ): void { + const resolvedParent = nodejs_fs.realpathSync(parentFilename); + const parentDir = nodejs_path.dirname(resolvedParent); let fullFileglob: string; if (fileglob.startsWith("/")) { fullFileglob = fileglob; } else { - fullFileglob = nodejs_path().join(parentDir, fileglob); + fullFileglob = nodejs_path.join(parentDir, fileglob); } fullFileglob = expandPath(fullFileglob); - const head = nodejs_path().dirname(fullFileglob); - const tail = nodejs_path().basename(fullFileglob); + const head = nodejs_path.dirname(fullFileglob); + const tail = nodejs_path.basename(fullFileglob); - const files = nodejs_fs().readdirSync(head); + const files = nodejs_fs.readdirSync(head); for (const f of files) { if (globMatch(tail, f)) { - const fullPath = nodejs_path().join(head, f); - this.loadFromFilename(fullPath); + const fullPath = nodejs_path.join(head, f); + this.loadFromFilename(fullPath, isDefaultSource); } } } - private loadSecret(sectionName: string, filename: string): void { + private loadSecret( + sectionName: string, + filename: string, + isDefaultSource: boolean, + ): void { const sec = this.provideSection(sectionName); sec.secretFilename = filename; const otherCfg = new Configuration(); try { - nodejs_fs().accessSync(filename, nodejs_fs().constants.R_OK); + nodejs_fs.accessSync(filename, nodejs_fs.constants.R_OK); } catch (err) { sec.inaccessible = true; return; } - otherCfg.loadFromFilename(filename, { + otherCfg.loadFromFilename(filename, isDefaultSource, { banDirectives: true, }); const otherSec = otherCfg.provideSection(sectionName); @@ -404,7 +403,11 @@ export class Configuration { } } - loadFromString(s: string, opts: LoadOptions = {}): void { + private internalLoadFromString( + s: string, + isDefaultSource: boolean, + opts: LoadOptions = {}, + ): void { let lineNo = 0; const fn = opts.filename ?? "<input>"; const reComment = /^\s*#.*$/; @@ -440,7 +443,10 @@ export class Configuration { ); } const arg = directiveMatch[2].trim(); - this.loadFromFilename(normalizeInlineFilename(opts.filename, arg)); + this.loadFromFilename( + normalizeInlineFilename(opts.filename, arg), + isDefaultSource, + ); break; } case "inline-secret": { @@ -460,7 +466,7 @@ export class Configuration { opts.filename, sp[1], ); - this.loadSecret(sp[0], secretFilename); + this.loadSecret(sp[0], secretFilename, isDefaultSource); break; } case "inline-matching": { @@ -470,7 +476,7 @@ export class Configuration { `invalid configuration, @inline-matching@ directive in ${fn}:${lineNo} can only be used from a file`, ); } - this.loadGlob(opts.filename, arg); + this.loadGlob(opts.filename, isDefaultSource, arg); break; } default: @@ -503,6 +509,9 @@ export class Configuration { value: val, sourceFile: opts.filename ?? "<unknown>", sourceLine: lineNo, + origin: isDefaultSource + ? EntryOrigin.DefaultFile + : EntryOrigin.Loaded, }; continue; } @@ -512,6 +521,10 @@ export class Configuration { } } + loadFromString(s: string, opts: LoadOptions = {}): void { + return this.internalLoadFromString(s, false, opts); + } + private provideSection(section: string): Section { const secNorm = section.toUpperCase(); if (this.sectionMap[secNorm]) { @@ -537,6 +550,24 @@ export class Configuration { value, sourceLine: 0, sourceFile: "<unknown>", + origin: EntryOrigin.Changed, + }; + } + + /** + * Set a string value to a value from default locations. + */ + private setStringSystemDefault( + section: string, + option: string, + value: string, + ): void { + const sec = this.provideSection(section); + sec.entries[option.toUpperCase()] = { + value, + sourceLine: 0, + sourceFile: "<unknown>", + origin: EntryOrigin.DefaultSystem, }; } @@ -604,11 +635,14 @@ export class Configuration { if (val !== undefined) { return pathsub(val, (v, d) => this.lookupVariable(v, d), depth); } + // Environment variables can be case sensitive, respect that. const envVal = process.env[x]; if (envVal !== undefined) { return envVal; } + + logger.warn(`unable to resolve variable '${x}'`); return; } @@ -619,30 +653,79 @@ export class Configuration { ); } - loadFrom(dirname: string): void { - const files = nodejs_fs().readdirSync(dirname); + loadDefaultsFromDir(dirname: string): void { + const files = nodejs_fs.readdirSync(dirname); for (const f of files) { - const fn = nodejs_path().join(dirname, f); - this.loadFromFilename(fn); + const fn = nodejs_path.join(dirname, f); + this.loadFromFilename(fn, true); } } private loadDefaults(): void { - let bc = process.env["TALER_BASE_CONFIG"]; - if (!bc) { + let baseConfigDir = process.env["TALER_BASE_CONFIG"]; + if (!baseConfigDir) { /* Try to locate the configuration based on the location * of the taler-config binary. */ const path = which("taler-config"); if (path) { - bc = nodejs_fs().realpathSync( - nodejs_path().dirname(path) + "/../share/taler/config.d", + baseConfigDir = nodejs_fs.realpathSync( + nodejs_path.dirname(path) + "/../share/taler/config.d", ); } } - if (!bc) { - bc = "/usr/share/taler/config.d"; + if (!baseConfigDir) { + baseConfigDir = "/usr/share/taler/config.d"; } - this.loadFrom(bc); + + let installPrefix = process.env["TALER_PREFIX"]; + if (!installPrefix) { + /* Try to locate install path based on the location + * of the taler-config binary. */ + const path = which("taler-config"); + if (path) { + installPrefix = nodejs_fs.realpathSync( + nodejs_path.dirname(path) + "/..", + ); + } + } + if (!installPrefix) { + installPrefix = "/usr"; + } + + this.setStringSystemDefault( + "PATHS", + "LIBEXECDIR", + `${installPrefix}/taler/libexec/`, + ); + this.setStringSystemDefault( + "PATHS", + "DOCDIR", + `${installPrefix}/share/doc/taler/`, + ); + this.setStringSystemDefault( + "PATHS", + "ICONDIR", + `${installPrefix}/share/icons/`, + ); + this.setStringSystemDefault( + "PATHS", + "LOCALEDIR", + `${installPrefix}/share/locale/`, + ); + this.setStringSystemDefault("PATHS", "PREFIX", `${installPrefix}/`); + this.setStringSystemDefault("PATHS", "BINDIR", `${installPrefix}/bin`); + this.setStringSystemDefault( + "PATHS", + "LIBDIR", + `${installPrefix}/lib/taler/`, + ); + this.setStringSystemDefault( + "PATHS", + "DATADIR", + `${installPrefix}/share/taler/`, + ); + + this.loadDefaultsFromDir(baseConfigDir); } getDefaultConfigFilename(): string | undefined { @@ -650,19 +733,19 @@ export class Configuration { const home = process.env["HOME"]; let fn: string | undefined; if (xdg) { - fn = nodejs_path().join(xdg, "taler.conf"); + fn = nodejs_path.join(xdg, "taler.conf"); } else if (home) { - fn = nodejs_path().join(home, ".config/taler.conf"); + fn = nodejs_path.join(home, ".config/taler.conf"); } - if (fn && nodejs_fs().existsSync(fn)) { + if (fn && nodejs_fs.existsSync(fn)) { return fn; } const etc1 = "/etc/taler.conf"; - if (nodejs_fs().existsSync(etc1)) { + if (nodejs_fs.existsSync(etc1)) { return etc1; } const etc2 = "/etc/taler/taler.conf"; - if (nodejs_fs().existsSync(etc2)) { + if (nodejs_fs.existsSync(etc2)) { return etc2; } return undefined; @@ -672,11 +755,13 @@ export class Configuration { const cfg = new Configuration(); cfg.loadDefaults(); if (filename) { - cfg.loadFromFilename(filename); + cfg.loadFromFilename(filename, false); } else { const fn = cfg.getDefaultConfigFilename(); if (fn) { - cfg.loadFromFilename(fn); + // It's the default filename for the main config file, + // but we don't consider the values default values. + cfg.loadFromFilename(fn, false); } } cfg.hintEntrypoint = filename; @@ -698,26 +783,51 @@ export class Configuration { } for (const sectionName of Object.keys(this.sectionMap)) { const sec = this.sectionMap[sectionName]; - if (opts.diagnostics && sec.secretFilename) { - s += `# Secret section from ${sec.secretFilename}\n`; - s += `# Secret accessible: ${!sec.inaccessible}\n`; - } - s += `[${sectionName}]\n`; + let headerWritten = false; for (const optionName of Object.keys(sec.entries)) { const entry = this.sectionMap[sectionName].entries[optionName]; + if ( + opts.excludeDefaults && + (entry.origin === EntryOrigin.DefaultSystem || + entry.origin === EntryOrigin.DefaultFile) + ) { + continue; + } + if (!headerWritten) { + if (opts.diagnostics && sec.secretFilename) { + s += `# Secret section from ${sec.secretFilename}\n`; + s += `# Secret accessible: ${!sec.inaccessible}\n`; + } + s += `[${sectionName}]\n`; + headerWritten = true; + } if (entry !== undefined) { if (opts.diagnostics) { - s += `# ${entry.sourceFile}:${entry.sourceLine}\n`; + switch (entry.origin) { + case EntryOrigin.DefaultFile: + case EntryOrigin.Changed: + case EntryOrigin.Loaded: + s += `# ${entry.sourceFile}:${entry.sourceLine}\n`; + break; + case EntryOrigin.DefaultSystem: + s += `# (system/installation default)\n`; + break; + } } s += `${optionName} = ${entry.value}\n`; } } - s += "\n"; + if (headerWritten) { + s += "\n"; + } } return s; } - write(filename: string): void { - nodejs_fs().writeFileSync(filename, this.stringify()); + write(filename: string, opts: { excludeDefaults?: boolean } = {}): void { + nodejs_fs.writeFileSync( + filename, + this.stringify({ excludeDefaults: opts.excludeDefaults }), + ); } } |