diff options
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r-- | packages/taler-wallet-core/src/util/http.ts | 7 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/talerconfig-test.ts | 124 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/talerconfig.ts | 151 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/timer.ts | 36 |
4 files changed, 312 insertions, 6 deletions
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index ad9f0293c..72de2ed1d 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -34,6 +34,7 @@ const logger = new Logger("http.ts"); */ export interface HttpResponse { requestUrl: string; + requestMethod: string; status: number; headers: Headers; json(): Promise<any>; @@ -118,6 +119,8 @@ export async function readSuccessResponseJsonOrErrorCode<T>( "Error response did not contain error code", { requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + httpStatusCode: httpResponse.status, }, ), ); @@ -188,7 +191,9 @@ export async function readSuccessResponseTextOrErrorCode<T>( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "Error response did not contain error code", { + httpStatusCode: httpResponse.status, requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, }, ), ); @@ -217,7 +222,9 @@ export async function checkSuccessResponseOrThrow( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "Error response did not contain error code", { + httpStatusCode: httpResponse.status, requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, }, ), ); diff --git a/packages/taler-wallet-core/src/util/talerconfig-test.ts b/packages/taler-wallet-core/src/util/talerconfig-test.ts new file mode 100644 index 000000000..71359fd38 --- /dev/null +++ b/packages/taler-wallet-core/src/util/talerconfig-test.ts @@ -0,0 +1,124 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Imports + */ +import test from "ava"; +import { pathsub, Configuration } from "./talerconfig"; + +test("pathsub", (t) => { + t.assert("foo" === pathsub("foo", () => undefined)); + + t.assert("fo${bla}o" === pathsub("fo${bla}o", () => undefined)); + + const d: Record<string, string> = { + w: "world", + f: "foo", + "1foo": "x", + "foo_bar": "quux", + }; + + t.is( + pathsub("hello ${w}!", (v) => d[v]), + "hello world!", + ); + + t.is( + pathsub("hello ${w} ${w}!", (v) => d[v]), + "hello world world!", + ); + + t.is( + pathsub("hello ${x:-blabla}!", (v) => d[v]), + "hello blabla!", + ); + + // No braces + t.is( + pathsub("hello $w!", (v) => d[v]), + "hello world!", + ); + t.is( + pathsub("hello $foo!", (v) => d[v]), + "hello $foo!", + ); + t.is( + pathsub("hello $1foo!", (v) => d[v]), + "hello $1foo!", + ); + t.is( + pathsub("hello $$ world!", (v) => d[v]), + "hello $$ world!", + ); + t.is( + pathsub("hello $$ world!", (v) => d[v]), + "hello $$ world!", + ); + + t.is( + pathsub("hello $foo_bar!", (v) => d[v]), + "hello quux!", + ); + + // Recursive lookup in default + t.is( + pathsub("hello ${x:-${w}}!", (v) => d[v]), + "hello world!", + ); + + // No variables in variable name part + t.is( + pathsub("hello ${${w}:-x}!", (v) => d[v]), + "hello ${${w}:-x}!", + ); + + // Missing closing brace + t.is( + pathsub("hello ${w!", (v) => d[v]), + "hello ${w!", + ); +}); + +test("path expansion", (t) => { + const config = new Configuration(); + config.setString("paths", "taler_home", "foo/bar"); + config.setString( + "paths", + "taler_data_home", + "$TALER_HOME/.local/share/taler/", + ); + config.setString( + "exchange", + "master_priv_file", + "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", + ); + t.is( + config.getPath("exchange", "MaStER_priv_file").required(), + "foo/bar/.local/share/taler//exchange/offline-keys/master.priv", + ); +}); + +test("recursive path resolution", (t) => { + console.log("recursive test"); + const config = new Configuration(); + config.setString("paths", "a", "x${b}"); + config.setString("paths", "b", "y${a}"); + config.setString("foo", "x", "z${a}"); + t.throws(() => { + config.getPath("foo", "a").required(); + }); +}); diff --git a/packages/taler-wallet-core/src/util/talerconfig.ts b/packages/taler-wallet-core/src/util/talerconfig.ts index ec08c352f..61bb6d206 100644 --- a/packages/taler-wallet-core/src/util/talerconfig.ts +++ b/packages/taler-wallet-core/src/util/talerconfig.ts @@ -25,6 +25,8 @@ */ import { AmountJson } from "./amounts"; import * as Amounts from "./amounts"; +import fs from "fs"; +import { acceptExchangeTermsOfService } from "../operations/exchanges"; export class ConfigError extends Error { constructor(message: string) { @@ -56,6 +58,89 @@ export class ConfigValue<T> { } } +/** + * 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 = {}; @@ -69,7 +154,6 @@ export class Configuration { const lines = s.split("\n"); for (const line of lines) { - console.log("parsing line", JSON.stringify(line)); if (reEmptyLine.test(line)) { continue; } @@ -79,15 +163,15 @@ export class Configuration { const secMatch = line.match(reSection); if (secMatch) { currentSection = secMatch[1]; - console.log("setting section to", currentSection); 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]; + const optName = paramMatch[1].toUpperCase(); let val = paramMatch[2]; if (val.startsWith('"') && val.endsWith('"')) { val = val.slice(1, val.length - 1); @@ -102,13 +186,44 @@ export class Configuration { "invalid configuration, expected section header or option assignment", ); } + } - console.log("parsed config", JSON.stringify(this.sectionMap, undefined, 2)); + setString(section: string, option: string, value: string): void { + const secNorm = section.toUpperCase(); + const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {}); + sec[option.toUpperCase()] = value; } getString(section: string, option: string): ConfigValue<string> { - const val = (this.sectionMap[section] ?? {})[option]; - return new ConfigValue(section, option, val, (x) => x); + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[section] ?? {})[optNorm]; + return new ConfigValue(secNorm, optNorm, val, (x) => x); + } + + getPath(section: string, option: string): ConfigValue<string> { + 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)), + ); + } + + lookupVariable(x: string, depth: number = 0): string | undefined { + console.log("looking up", x); + // 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<AmountJson> { @@ -117,4 +232,28 @@ export class Configuration { 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); + } } diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts index 8eab1399c..d652fdcda 100644 --- a/packages/taler-wallet-core/src/util/timer.ts +++ b/packages/taler-wallet-core/src/util/timer.ts @@ -34,6 +34,12 @@ const logger = new Logger("timer.ts"); */ export interface TimerHandle { clear(): void; + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void; } class IntervalHandle { @@ -42,6 +48,16 @@ class IntervalHandle { clear(): void { clearInterval(this.h); } + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void { + if (typeof this.h === "object") { + this.h.unref(); + } + } } class TimeoutHandle { @@ -50,6 +66,16 @@ class TimeoutHandle { clear(): void { clearTimeout(this.h); } + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void { + if (typeof this.h === "object") { + this.h.unref(); + } + } } /** @@ -92,6 +118,10 @@ const nullTimerHandle = { // do nothing return; }, + unref() { + // do nothing + return; + } }; /** @@ -141,6 +171,9 @@ export class TimerGroup { h.clear(); delete tm[myId]; }, + unref() { + h.unref(); + } }; } @@ -160,6 +193,9 @@ export class TimerGroup { h.clear(); delete tm[myId]; }, + unref() { + h.unref(); + } }; } } |