From 07cdfb2e4ec761021477271776b81f33af0e731d Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 17 Mar 2021 17:56:37 +0100 Subject: towards wallet-core / util split --- packages/taler-util/src/talerconfig.ts | 318 +++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 packages/taler-util/src/talerconfig.ts (limited to 'packages/taler-util/src/talerconfig.ts') diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts new file mode 100644 index 000000000..fa8c2d40f --- /dev/null +++ b/packages/taler-util/src/talerconfig.ts @@ -0,0 +1,318 @@ +/* + This file is part of GNU Taler + (C) 2020 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 + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Utilities to handle Taler-style configuration files. + * + * @author Florian Dold + */ + +/** + * Imports + */ +import { AmountJson } from "./amounts"; +import * as Amounts from "./amounts"; +import fs from "fs"; + +export class ConfigError extends Error { + constructor(message: string) { + super(); + Object.setPrototypeOf(this, ConfigError.prototype); + this.name = "ConfigError"; + this.message = message; + } +} + +type OptionMap = { [optionName: string]: string }; +type SectionMap = { [sectionName: string]: OptionMap }; + +export class ConfigValue { + constructor( + private sectionName: string, + private optionName: string, + private val: string | undefined, + private converter: (x: string) => T, + ) {} + + required(): T { + if (!this.val) { + throw new ConfigError( + `required option [${this.sectionName}]/${this.optionName} not found`, + ); + } + return this.converter(this.val); + } + + orUndefined(): T | undefined { + if (this.val !== undefined) { + return this.converter(this.val); + } else { + return undefined; + } + } + + orDefault(v: T): T | undefined { + if (this.val !== undefined) { + return this.converter(this.val); + } else { + return v; + } + } + + isDefined(): boolean { + return this.val !== undefined; + } +} + +/** + * Shell-style path substitution. + * + * Supported patterns: + * "$x" (look up "x") + * "${x}" (look up "x") + * "${x:-y}" (look up "x", fall back to expanded y) + */ +export function pathsub( + x: string, + lookup: (s: string, depth: number) => string | undefined, + depth = 0, +): string { + if (depth >= 10) { + throw Error("recursion in path substitution"); + } + let s = x; + let l = 0; + while (l < s.length) { + if (s[l] === "$") { + if (s[l + 1] === "{") { + let depth = 1; + const start = l; + let p = start + 2; + let insideNamePart = true; + let hasDefault = false; + for (; p < s.length; p++) { + if (s[p] == "}") { + insideNamePart = false; + depth--; + } else if (s[p] === "$" && s[p + 1] === "{") { + insideNamePart = false; + depth++; + } + if (insideNamePart && s[p] === ":" && s[p + 1] === "-") { + hasDefault = true; + } + if (depth == 0) { + break; + } + } + if (depth == 0) { + const inner = s.slice(start + 2, p); + let varname: string; + let defaultValue: string | undefined; + if (hasDefault) { + [varname, defaultValue] = inner.split(":-", 2); + } else { + varname = inner; + defaultValue = undefined; + } + + const r = lookup(inner, depth + 1); + if (r !== undefined) { + s = s.substr(0, start) + r + s.substr(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); + l = start + resolvedDefault.length; + continue; + } + } + l = p; + continue; + } 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); + if (r !== undefined) { + s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length); + l = l + r.length; + continue; + } + } + } + } + l++; + } + return s; +} + +export class Configuration { + private sectionMap: SectionMap = {}; + + loadFromString(s: string): void { + const reComment = /^\s*#.*$/; + const reSection = /^\s*\[\s*([^\]]*)\s*\]\s*$/; + const reParam = /^\s*([^=]+?)\s*=\s*(.*?)\s*$/; + const reEmptyLine = /^\s*$/; + + let currentSection: string | undefined = undefined; + + const lines = s.split("\n"); + for (const line of lines) { + if (reEmptyLine.test(line)) { + continue; + } + if (reComment.test(line)) { + continue; + } + const secMatch = line.match(reSection); + if (secMatch) { + currentSection = secMatch[1]; + continue; + } + if (currentSection === undefined) { + throw Error("invalid configuration, expected section header"); + } + currentSection = currentSection.toUpperCase(); + const paramMatch = line.match(reParam); + if (paramMatch) { + const optName = paramMatch[1].toUpperCase(); + let val = paramMatch[2]; + if (val.startsWith('"') && val.endsWith('"')) { + val = val.slice(1, val.length - 1); + } + const sec = this.sectionMap[currentSection] ?? {}; + this.sectionMap[currentSection] = Object.assign(sec, { + [optName]: val, + }); + continue; + } + throw Error( + "invalid configuration, expected section header or option assignment", + ); + } + } + + setString(section: string, option: string, value: string): void { + const secNorm = section.toUpperCase(); + const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {}); + sec[option.toUpperCase()] = value; + } + + /** + * Get lower-cased section names. + */ + getSectionNames(): string[] { + return Object.keys(this.sectionMap).map((x) => x.toLowerCase()); + } + + getString(section: string, option: string): ConfigValue { + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[secNorm] ?? {})[optNorm]; + return new ConfigValue(secNorm, optNorm, val, (x) => x); + } + + getPath(section: string, option: string): ConfigValue { + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[secNorm] ?? {})[optNorm]; + return new ConfigValue(secNorm, optNorm, val, (x) => + pathsub(x, (v, d) => this.lookupVariable(v, d + 1)), + ); + } + + getYesNo(section: string, option: string): ConfigValue { + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[secNorm] ?? {})[optNorm]; + const convert = (x: string): boolean => { + x = x.toLowerCase(); + if (x === "yes") { + return true; + } else if (x === "no") { + return false; + } + throw Error( + `invalid config value for [${secNorm}]/${optNorm}, expected yes/no`, + ); + }; + return new ConfigValue(secNorm, optNorm, val, convert); + } + + getNumber(section: string, option: string): ConfigValue { + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[secNorm] ?? {})[optNorm]; + const convert = (x: string): number => { + try { + return Number.parseInt(x, 10); + } catch (e) { + throw Error( + `invalid config value for [${secNorm}]/${optNorm}, expected number`, + ); + } + }; + return new ConfigValue(secNorm, optNorm, val, convert); + } + + lookupVariable(x: string, depth: number = 0): string | undefined { + // We loop up options in PATHS in upper case, as option names + // are case insensitive + const val = (this.sectionMap["PATHS"] ?? {})[x.toUpperCase()]; + 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; + } + return; + } + + getAmount(section: string, option: string): ConfigValue { + const val = (this.sectionMap[section] ?? {})[option]; + return new ConfigValue(section, option, val, (x) => + Amounts.parseOrThrow(x), + ); + } + + static load(filename: string): Configuration { + const s = fs.readFileSync(filename, "utf-8"); + const cfg = new Configuration(); + cfg.loadFromString(s); + return cfg; + } + + write(filename: string): void { + let s = ""; + for (const sectionName of Object.keys(this.sectionMap)) { + s += `[${sectionName}]\n`; + for (const optionName of Object.keys( + this.sectionMap[sectionName] ?? {}, + )) { + const val = this.sectionMap[sectionName][optionName]; + if (val !== undefined) { + s += `${optionName} = ${val}\n`; + } + } + s += "\n"; + } + fs.writeFileSync(filename, s); + } +} -- cgit v1.2.3