summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/talerconfig.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/talerconfig.ts')
-rw-r--r--packages/taler-util/src/talerconfig.ts324
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 }),
+ );
}
}