diff options
author | Florian Dold <florian@dold.me> | 2021-08-20 12:31:35 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-08-20 13:18:51 +0200 |
commit | 45f134699076b7708ff23b16e233e67dc866175e (patch) | |
tree | 29b4012bbe36a3e173849a0886fa7df729d0cb5c /packages | |
parent | a576fdfbf8eeb2c5ba53569c6ea09c998e68c57f (diff) |
minimatch
Signed-off-by: Florian Dold <florian@dold.me>
Diffstat (limited to 'packages')
-rw-r--r-- | packages/taler-util/src/globbing/balanced-match.ts | 93 | ||||
-rw-r--r-- | packages/taler-util/src/globbing/brace-expansion.ts | 249 | ||||
-rw-r--r-- | packages/taler-util/src/globbing/minimatch.ts | 1004 | ||||
-rw-r--r-- | packages/taler-util/src/index.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-cli/package.json | 2 | ||||
-rw-r--r-- | packages/taler-wallet-cli/src/integrationtests/testrunner.ts | 7 |
6 files changed, 1351 insertions, 5 deletions
diff --git a/packages/taler-util/src/globbing/balanced-match.ts b/packages/taler-util/src/globbing/balanced-match.ts new file mode 100644 index 000000000..0dc35a569 --- /dev/null +++ b/packages/taler-util/src/globbing/balanced-match.ts @@ -0,0 +1,93 @@ +/* +Original work Copyright (C) 2013 Julian Gruber <julian@juliangruber.com> +Modified work Copyright (C) 2021 Taler Systems S.A. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +*/ + +export function balanced(a: RegExp | string, b: RegExp | string, str: string) { + let myA: string; + let myB: string; + if (a instanceof RegExp) myA = maybeMatch(a, str)!; + else myA = a; + if (b instanceof RegExp) myB = maybeMatch(b, str)!; + else myB = b; + + const r = range(myA, myB, str); + + return ( + r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0]! + myA!.length, r[1]), + post: str.slice(r[1]! + myB!.length), + } + ); +} + +/** + * @param {RegExp} reg + * @param {string} str + */ +function maybeMatch(reg: RegExp, str: string) { + const m = str.match(reg); + return m ? m[0] : null; +} + +balanced.range = range; + +/** + * @param {string} a + * @param {string} b + * @param {string} str + */ +function range(a: string, b: string, str: string) { + let begs: number[]; + let beg: number; + let left, right, result; + let ai = str.indexOf(a); + let bi = str.indexOf(b, ai + 1); + let i = ai; + + if (ai >= 0 && bi > 0) { + if (a === b) { + return [ai, bi]; + } + begs = []; + left = str.length; + + while (i >= 0 && !result) { + if (i === ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length === 1) { + result = [begs.pop(), bi]; + } else { + beg = begs.pop()!; + if (beg < left) { + left = beg; + right = bi; + } + + bi = str.indexOf(b, i + 1); + } + + i = ai < bi && ai >= 0 ? ai : bi; + } + + if (begs.length) { + result = [left, right]; + } + } + + return result; +} diff --git a/packages/taler-util/src/globbing/brace-expansion.ts b/packages/taler-util/src/globbing/brace-expansion.ts new file mode 100644 index 000000000..342253ebc --- /dev/null +++ b/packages/taler-util/src/globbing/brace-expansion.ts @@ -0,0 +1,249 @@ +/* +Original work Copyright (C) 2013 Julian Gruber <julian@juliangruber.com> +Modified work Copyright (C) 2021 Taler Systems S.A. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +*/ + +import { balanced } from "./balanced-match.js"; + +var escSlash = "\0SLASH" + Math.random() + "\0"; +var escOpen = "\0OPEN" + Math.random() + "\0"; +var escClose = "\0CLOSE" + Math.random() + "\0"; +var escComma = "\0COMMA" + Math.random() + "\0"; +var escPeriod = "\0PERIOD" + Math.random() + "\0"; + +/** + * @return {number} + */ +function numeric(str: string): number { + return parseInt(str, 10).toString() == str + ? parseInt(str, 10) + : str.charCodeAt(0); +} + +/** + * @param {string} str + */ +function escapeBraces(str: string) { + return str + .split("\\\\") + .join(escSlash) + .split("\\{") + .join(escOpen) + .split("\\}") + .join(escClose) + .split("\\,") + .join(escComma) + .split("\\.") + .join(escPeriod); +} + +/** + * @param {string} str + */ +function unescapeBraces(str: string) { + return str + .split(escSlash) + .join("\\") + .split(escOpen) + .join("{") + .split(escClose) + .join("}") + .split(escComma) + .join(",") + .split(escPeriod) + .join("."); +} + +/** + * Basically just str.split(","), but handling cases + * where we have nested braced sections, which should be + * treated as individual members, like {a,{b,c},d} + * @param {string} str + */ +function parseCommaParts(str: string) { + if (!str) return [""]; + + var parts: string[] = []; + var m = balanced("{", "}", str); + + if (!m) return str.split(","); + + var pre = m.pre; + var body = m.body; + var post = m.post; + var p = pre.split(","); + + p[p.length - 1] += "{" + body + "}"; + var postParts = parseCommaParts(post); + if (post.length) { + p[p.length - 1] += postParts.shift(); + p.push.apply(p, postParts); + } + + parts.push.apply(parts, p); + + return parts; +} + +/** + * @param {string} str + */ +function expandTop(str: string) { + if (!str) return []; + + // I don't know why Bash 4.3 does this, but it does. + // Anything starting with {} will have the first two bytes preserved + // but *only* at the top level, so {},a}b will not expand to anything, + // but a{},b}c will be expanded to [a}c,abc]. + // One could argue that this is a bug in Bash, but since the goal of + // this module is to match Bash's rules, we escape a leading {} + if (str.substr(0, 2) === "{}") { + str = "\\{\\}" + str.substr(2); + } + + return expand(escapeBraces(str), true).map(unescapeBraces); +} + +/** + * @param {string} str + */ +function embrace(str: string) { + return "{" + str + "}"; +} +/** + * @param {string} el + */ +function isPadded(el: string) { + return /^-?0\d/.test(el); +} + +/** + * @param {number} i + * @param {number} y + */ +function lte(i: number, y: number) { + return i <= y; +} +/** + * @param {number} i + * @param {number} y + */ +function gte(i: number, y: number) { + return i >= y; +} + +/** + * @param {string} str + * @param {boolean} [isTop] + */ +export function expand(str: string, isTop?: boolean): any { + /** @type {string[]} */ + var expansions: string[] = []; + + var m = balanced("{", "}", str); + if (!m) return [str]; + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + var post = m.post.length ? expand(m.post, false) : [""]; + + if (/\$$/.test(m.pre)) { + for (var k = 0; k < post.length; k++) { + var expansion = pre + "{" + m.body + "}" + post[k]; + expansions.push(expansion); + } + } else { + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(",") >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,.*\}/)) { + str = m.pre + "{" + m.body + escClose + m.post; + return expand(str); + } + return [str]; + } + + var n: string[]; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], false).map(embrace); + if (n.length === 1) { + return post.map(function (p: string) { + return m!.pre + n[0] + p; + }); + } + } + } + + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + var N: string[]; + + if (isSequence) { + var x = numeric(n[0]); + var y = numeric(n[1]); + var width = Math.max(n[0].length, n[1].length); + var incr = n.length == 3 ? Math.abs(numeric(n[2])) : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); + + N = []; + + for (var i = x; test(i, y); i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === "\\") c = ""; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join("0"); + if (i < 0) c = "-" + z + c.slice(1); + else c = z + c; + } + } + } + N.push(c); + } + } else { + N = []; + + for (var j = 0; j < n.length; j++) { + N.push.apply(N, expand(n[j], false)); + } + } + + for (var j = 0; j < N.length; j++) { + for (var k = 0; k < post.length; k++) { + var expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) expansions.push(expansion); + } + } + } + + return expansions; +} diff --git a/packages/taler-util/src/globbing/minimatch.ts b/packages/taler-util/src/globbing/minimatch.ts new file mode 100644 index 000000000..54779623a --- /dev/null +++ b/packages/taler-util/src/globbing/minimatch.ts @@ -0,0 +1,1004 @@ +/* +Original work Copyright (c) Isaac Z. Schlueter and Contributors +Modified work Copyright (c) 2021 Taler Systems S.A. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +*/ + + +import { expand } from "./brace-expansion.js"; + +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; + }; +})(); + +let path = { sep: "/" }; +try { + path.sep = nodejs_path().sep; +} catch (er) {} + +const GLOBSTAR = {}; + +const plTypes = { + "!": { open: "(?:(?!(?:", close: "))[^/]*?)" }, + "?": { open: "(?:", close: ")?" }, + "+": { open: "(?:", close: ")+" }, + "*": { open: "(?:", close: ")*" }, + "@": { open: "(?:", close: ")" }, +}; + +// any single thing other than / +// don't need to escape / when using new RegExp() +const qmark = "[^/]"; + +// * => any number of characters +const star = qmark + "*?"; + +// ** when dots are allowed. Anything goes, except .. and . +// not (^ or / followed by one or two dots followed by $ or /), +// followed by anything, any number of times. +const twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"; + +// not a ^ or / followed by a dot, +// followed by anything, any number of times. +const twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?"; + +// characters that need to be escaped in RegExp. +const reSpecials = charSet("().*{}+?[]^$\\!"); + +// "abc" -> { a:true, b:true, c:true } +function charSet(s: string) { + return s.split("").reduce(function (set: any, c) { + set[c] = true; + return set; + }, {}); +} + +// normalizes slashes. +var slashSplit = /\/+/; + +minimatch.filter = filter; +function filter(pattern: any, options: {}) { + options = options || {}; + return function (p: any, i: any, list: any) { + return minimatch(p, pattern, options); + }; +} + +interface IOptions { + /** + * Dump a ton of stuff to stderr. + * + * @default false + */ + debug?: boolean; + + /** + * Do not expand {a,b} and {1..3} brace sets. + * + * @default false + */ + nobrace?: boolean; + + /** + * Disable ** matching against multiple folder names. + * + * @default false + */ + noglobstar?: boolean; + + /** + * Allow patterns to match filenames starting with a period, + * even if the pattern does not explicitly have a period in that spot. + * + * @default false + */ + dot?: boolean; + + /** + * Disable "extglob" style patterns like +(a|b). + * + * @default false + */ + noext?: boolean; + + /** + * Perform a case-insensitive match. + * + * @default false + */ + nocase?: boolean; + + /** + * When a match is not found by minimatch.match, + * return a list containing the pattern itself if this option is set. + * Otherwise, an empty list is returned if there are no matches. + * + * @default false + */ + nonull?: boolean; + + /** + * If set, then patterns without slashes will be matched against + * the basename of the path if it contains slashes. + * + * @default false + */ + matchBase?: boolean; + + /** + * Suppress the behavior of treating # + * at the start of a pattern as a comment. + * + * @default false + */ + nocomment?: boolean; + + /** + * Suppress the behavior of treating a leading ! character as negation. + * + * @default false + */ + nonegate?: boolean; + + /** + * Returns from negate expressions the same as if they were not negated. + * (Ie, true on a hit, false on a miss.) + * + * @default false + */ + flipNegate?: boolean; +} + +export function minimatch(p: string, pattern: string, options?: IOptions) { + if (typeof pattern !== "string") { + throw new TypeError("glob pattern string required"); + } + + if (!options) options = {}; + + // shortcut: comments match nothing. + if (!options.nocomment && pattern.charAt(0) === "#") { + return false; + } + + // "" only matches "" + if (pattern.trim() === "") return p === ""; + + return new Minimatch(pattern, options).match(p); +} + +export class Minimatch { + options: IOptions; + pattern: string; + set: string[] | string = []; + regexp: RegExp | null | boolean = null; + negate: boolean = false; + comment: boolean = false; + empty: boolean = false; + made: boolean = false; + _made: boolean = false; + globSet: any; + globParts: any; + constructor(pattern: string, options: IOptions) { + if (typeof pattern !== "string") { + throw new TypeError("glob pattern string required"); + } + + if (!options) options = {}; + pattern = pattern.trim(); + + // windows support: need to use /, not \ + if (path.sep !== "/") { + pattern = pattern.split(path.sep).join("/"); + } + + this.options = options; + this.pattern = pattern; + + // make the set of regexps etc. + this.make(); + } + + debug(...args: any[]) {} + + make() { + // don't do it more than once. + if (this._made) return; + + const pattern = this.pattern; + const options = this.options; + + // empty patterns and comments match nothing. + if (!options.nocomment && pattern.charAt(0) === "#") { + this.comment = true; + return; + } + if (!pattern) { + this.empty = true; + return; + } + + // step 1: figure out negation, etc. + this.parseNegate(); + + // step 2: expand braces + var set = (this.globSet = this.braceExpand()); + + if (options.debug) this.debug = console.error; + + this.debug(this.pattern, set); + + // step 3: now we have a set, so turn each one into a series of path-portion + // matching patterns. + // These will be regexps, except in the case of "**", which is + // set to the GLOBSTAR object for globstar behavior, + // and will not contain any / characters + set = this.globParts = set.map((s: any) => s.split(slashSplit)); + + this.debug(this.pattern, set); + + // glob --> regexps + set = set.map((s: any[]) => { + return s.map((x) => this.parse(x), this); + }, this); + + this.debug(this.pattern, set); + + // filter out everything that didn't compile properly. + set = set.filter(function (s: any) { + return s.indexOf(false) === -1; + }); + + this.debug(this.pattern, set); + + this.set = set; + } + + parseNegate() { + var pattern = this.pattern; + var negate = false; + var options = this.options; + var negateOffset = 0; + + if (options.nonegate) return; + + for ( + var i = 0, l = pattern.length; + i < l && pattern.charAt(i) === "!"; + i++ + ) { + negate = !negate; + negateOffset++; + } + + if (negateOffset) this.pattern = pattern.substr(negateOffset); + this.negate = negate; + } + + braceExpand(pattern?: string, options?: IOptions) { + if (!options) { + if (this instanceof Minimatch) { + options = this.options; + } else { + options = {}; + } + } + + pattern = typeof pattern === "undefined" ? this.pattern : pattern; + + if (typeof pattern === "undefined") { + throw new TypeError("undefined pattern"); + } + + if (options.nobrace || !pattern.match(/\{.*\}/)) { + // shortcut. no need to expand. + return [pattern]; + } + + return expand(pattern); + } + + // parse a component of the expanded set. + // At this point, no pattern may contain "/" in it + // so we're going to return a 2d array, where each entry is the full + // pattern, split on '/', and then turned into a regular expression. + // A regexp is made at the end which joins each array with an + // escaped /, and another full one which joins each regexp with |. + // + // Following the lead of Bash 4.1, note that "**" only has special meaning + // when it is the *only* thing in a path portion. Otherwise, any series + // of * is equivalent to a single *. Globstar behavior is enabled by + // default, and can be disabled by setting options.noglobstar. + parse(pattern: string, isSub?: boolean): RegExp | string | {} { + if (pattern.length > 1024 * 64) { + throw new TypeError("pattern is too long"); + } + + var options = this.options; + + // shortcuts + if (!options.noglobstar && pattern === "**") return GLOBSTAR; + if (pattern === "") return ""; + + var re = ""; + var hasMagic = !!options.nocase; + var escaping = false; + // ? => one single character + var patternListStack: { + open: string; + close: string; + type: any; + start: number; + reStart: number; + reEnd?: number; + }[] = []; + var negativeLists = []; + let stateChar: string | boolean | undefined = undefined; + var inClass = false; + var reClassStart = -1; + var classStart = -1; + // . and .. never match anything that doesn't start with ., + // even when options.dot is set. + var patternStart = + pattern.charAt(0) === "." + ? "" // anything + : // not (start or / followed by . or .. followed by / or end) + options.dot + ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))" + : "(?!\\.)"; + var self = this; + + function clearStateChar() { + if (stateChar) { + // we had some state-tracking character + // that wasn't consumed by this pass. + switch (stateChar) { + case "*": + re += star; + hasMagic = true; + break; + case "?": + re += qmark; + hasMagic = true; + break; + default: + re += "\\" + stateChar; + break; + } + self.debug("clearStateChar %j %j", stateChar, re); + stateChar = false; + } + } + + for ( + var i = 0, len = pattern.length, c; + i < len && (c = pattern.charAt(i)); + i++ + ) { + this.debug("%s\t%s %s %j", pattern, i, re, c); + + // skip over any that are escaped. + if (escaping && reSpecials[c]) { + re += "\\" + c; + escaping = false; + continue; + } + + switch (c) { + case "/": + // completely not allowed, even escaped. + // Should already be path-split by now. + return false; + + case "\\": + clearStateChar(); + escaping = true; + continue; + + // the various stateChar values + // for the "extglob" stuff. + case "?": + case "*": + case "+": + case "@": + case "!": + this.debug("%s\t%s %s %j <-- stateChar", pattern, i, re, c); + + // all of those are literals inside a class, except that + // the glob [!a] means [^a] in regexp + if (inClass) { + this.debug(" in class"); + if (c === "!" && i === classStart + 1) c = "^"; + re += c; + continue; + } + + // if we already have a stateChar, then it means + // that there was something like ** or +? in there. + // Handle the stateChar, then proceed with this one. + self.debug("call clearStateChar %j", stateChar); + clearStateChar(); + stateChar = c; + // if extglob is disabled, then +(asdf|foo) isn't a thing. + // just clear the statechar *now*, rather than even diving into + // the patternList stuff. + if (options.noext) clearStateChar(); + continue; + + case "(": + if (inClass) { + re += "("; + continue; + } + + if (!stateChar) { + re += "\\("; + continue; + } + + patternListStack.push({ + type: stateChar, + start: i - 1, + reStart: re.length, + // @ts-ignore + open: plTypes[stateChar].open, + // @ts-ignore + close: plTypes[stateChar].close, + }); + // negation is (?:(?!js)[^/]*) + re += stateChar === "!" ? "(?:(?!(?:" : "(?:"; + this.debug("plType %j %j", stateChar, re); + stateChar = false; + continue; + + case ")": + if (inClass || !patternListStack.length) { + re += "\\)"; + continue; + } + + clearStateChar(); + hasMagic = true; + var pl = patternListStack.pop(); + // negation is (?:(?!js)[^/]*) + // The others are (?:<pattern>)<type> + re += pl!.close; + if (pl!.type === "!") { + negativeLists.push(pl); + } + pl!.reEnd = re.length; + continue; + + case "|": + if (inClass || !patternListStack.length || escaping) { + re += "\\|"; + escaping = false; + continue; + } + + clearStateChar(); + re += "|"; + continue; + + // these are mostly the same in regexp and glob + case "[": + // swallow any state-tracking char before the [ + clearStateChar(); + + if (inClass) { + re += "\\" + c; + continue; + } + + inClass = true; + classStart = i; + reClassStart = re.length; + re += c; + continue; + + case "]": + // a right bracket shall lose its special + // meaning and represent itself in + // a bracket expression if it occurs + // first in the list. -- POSIX.2 2.8.3.2 + if (i === classStart + 1 || !inClass) { + re += "\\" + c; + escaping = false; + continue; + } + + // handle the case where we left a class open. + // "[z-a]" is valid, equivalent to "\[z-a\]" + if (inClass) { + // split where the last [ was, make sure we don't have + // an invalid re. if so, re-walk the contents of the + // would-be class to re-translate any characters that + // were passed through as-is + // TODO: It would probably be faster to determine this + // without a try/catch and a new RegExp, but it's tricky + // to do safely. For now, this is safe and works. + var cs = pattern.substring(classStart + 1, i); + try { + RegExp("[" + cs + "]"); + } catch (er) { + // not a valid class! + var sp = this.parse(cs, true); + re = re.substr(0, reClassStart) + "\\[" + (sp as any)[0] + "\\]"; + hasMagic = hasMagic || (sp as any)[1]; + inClass = false; + continue; + } + } + + // finish up the class. + hasMagic = true; + inClass = false; + re += c; + continue; + + default: + // swallow any state char that wasn't consumed + clearStateChar(); + + if (escaping) { + // no need + escaping = false; + } else if (reSpecials[c] && !(c === "^" && inClass)) { + re += "\\"; + } + + re += c; + } // switch + } // for + + // handle the case where we left a class open. + // "[abc" is valid, equivalent to "\[abc" + if (inClass) { + // split where the last [ was, and escape it + // this is a huge pita. We now have to re-walk + // the contents of the would-be class to re-translate + // any characters that were passed through as-is + cs = pattern.substr(classStart + 1); + sp = this.parse(cs, true); + re = re.substr(0, reClassStart) + "\\[" + (sp as any)[0]; + hasMagic = hasMagic || (sp as any)[1]; + } + + // handle the case where we had a +( thing at the *end* + // of the pattern. + // each pattern list stack adds 3 chars, and we need to go through + // and escape any | chars that were passed through as-is for the regexp. + // Go through and escape them, taking care not to double-escape any + // | chars that were already escaped. + for (pl = patternListStack.pop(); pl; pl = patternListStack.pop()) { + var tail = re.slice(pl.reStart + pl.open.length); + this.debug("setting tail", re, pl); + // maybe some even number of \, then maybe 1 \, followed by a | + tail = tail.replace(/((?:\\{2}){0,64})(\\?)\|/g, function (_, $1, $2) { + if (!$2) { + // the | isn't already escaped, so escape it. + $2 = "\\"; + } + + // need to escape all those slashes *again*, without escaping the + // one that we need for escaping the | character. As it works out, + // escaping an even number of slashes can be done by simply repeating + // it exactly after itself. That's why this trick works. + // + // I am sorry that you have to see this. + return $1 + $1 + $2 + "|"; + }); + + this.debug("tail=%j\n %s", tail, tail, pl, re); + var t = pl.type === "*" ? star : pl.type === "?" ? qmark : "\\" + pl.type; + + hasMagic = true; + re = re.slice(0, pl.reStart) + t + "\\(" + tail; + } + + // handle trailing things that only matter at the very end. + clearStateChar(); + if (escaping) { + // trailing \\ + re += "\\\\"; + } + + // only need to apply the nodot start if the re starts with + // something that could conceivably capture a dot + var addPatternStart = false; + switch (re.charAt(0)) { + case ".": + case "[": + case "(": + addPatternStart = true; + } + + // Hack to work around lack of negative lookbehind in JS + // A pattern like: *.!(x).!(y|z) needs to ensure that a name + // like 'a.xyz.yz' doesn't match. So, the first negative + // lookahead, has to look ALL the way ahead, to the end of + // the pattern. + for (var n = negativeLists.length - 1; n > -1; n--) { + var nl = negativeLists[n]; + + var nlBefore = re.slice(0, nl!.reStart); + var nlFirst = re.slice(nl!.reStart, nl!.reEnd! - 8); + var nlLast = re.slice(nl!.reEnd! - 8, nl!.reEnd); + var nlAfter = re.slice(nl!.reEnd); + + nlLast += nlAfter; + + // Handle nested stuff like *(*.js|!(*.json)), where open parens + // mean that we should *not* include the ) in the bit that is considered + // "after" the negated section. + var openParensBefore = nlBefore.split("(").length - 1; + var cleanAfter = nlAfter; + for (i = 0; i < openParensBefore; i++) { + cleanAfter = cleanAfter.replace(/\)[+*?]?/, ""); + } + nlAfter = cleanAfter; + + var dollar = ""; + if (nlAfter === "" && !isSub) { + dollar = "$"; + } + var newRe = nlBefore + nlFirst + nlAfter + dollar + nlLast; + re = newRe; + } + + // if the re is not "" at this point, then we need to make sure + // it doesn't match against an empty path part. + // Otherwise a/* will match a/, which it should not. + if (re !== "" && hasMagic) { + re = "(?=.)" + re; + } + + if (addPatternStart) { + re = patternStart + re; + } + + // parsing just a piece of a larger pattern. + if (isSub) { + return [re, hasMagic]; + } + + // skip the regexp for non-magical patterns + // unescape anything in it, though, so that it'll be + // an exact match against a file etc. + if (!hasMagic) { + return globUnescape(pattern); + } + + var flags = options.nocase ? "i" : ""; + try { + var regExp = new RegExp("^" + re + "$", flags); + } catch (er) { + // If it was an invalid regular expression, then it can't match + // anything. This trick looks for a character after the end of + // the string, which is of course impossible, except in multi-line + // mode, but it's not a /m regex. + return new RegExp("$."); + } + + (regExp as any)._glob = pattern; + (regExp as any)._src = re; + + return regExp; + } + + // set partial to true to test if, for example, + // "/a/b" matches the start of "/*/b/*/d" + // Partial means, if you run out of file before you run + // out of pattern, then that's fine, as long as all + // the parts match. + matchOne(file: string | any[], pattern: string | any[], partial: any) { + var options = this.options; + + this.debug("matchOne", { this: this, file: file, pattern: pattern }); + + this.debug("matchOne", file.length, pattern.length); + + for ( + var fi = 0, pi = 0, fl = file.length, pl = pattern.length; + fi < fl && pi < pl; + fi++, pi++ + ) { + this.debug("matchOne loop"); + var p = pattern[pi]; + var f = file[fi]; + + this.debug(pattern, p, f); + + // should be impossible. + // some invalid regexp stuff in the set. + if (p === false) return false; + + if (p === GLOBSTAR) { + this.debug("GLOBSTAR", [pattern, p, f]); + + // "**" + // a/**/b/**/c would match the following: + // a/b/x/y/z/c + // a/x/y/z/b/c + // a/b/x/b/x/c + // a/b/c + // To do this, take the rest of the pattern after + // the **, and see if it would match the file remainder. + // If so, return success. + // If not, the ** "swallows" a segment, and try again. + // This is recursively awful. + // + // a/**/b/**/c matching a/b/x/y/z/c + // - a matches a + // - doublestar + // - matchOne(b/x/y/z/c, b/**/c) + // - b matches b + // - doublestar + // - matchOne(x/y/z/c, c) -> no + // - matchOne(y/z/c, c) -> no + // - matchOne(z/c, c) -> no + // - matchOne(c, c) yes, hit + var fr = fi; + var pr = pi + 1; + if (pr === pl) { + this.debug("** at the end"); + // a ** at the end will just swallow the rest. + // We have found a match. + // however, it will not swallow /.x, unless + // options.dot is set. + // . and .. are *never* matched by **, for explosively + // exponential reasons. + for (; fi < fl; fi++) { + if ( + file[fi] === "." || + file[fi] === ".." || + (!options.dot && file[fi].charAt(0) === ".") + ) + return false; + } + return true; + } + + // ok, let's see if we can swallow whatever we can. + while (fr < fl) { + var swallowee = file[fr]; + + this.debug("\nglobstar while", file, fr, pattern, pr, swallowee); + + // XXX remove this slice. Just pass the start index. + if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { + this.debug("globstar found match!", fr, fl, swallowee); + // found a match. + return true; + } else { + // can't swallow "." or ".." ever. + // can only swallow ".foo" when explicitly asked. + if ( + swallowee === "." || + swallowee === ".." || + (!options.dot && swallowee.charAt(0) === ".") + ) { + this.debug("dot detected!", file, fr, pattern, pr); + break; + } + + // ** swallows a segment, and continue. + this.debug("globstar swallow a segment, and continue"); + fr++; + } + } + + // no match was found. + // However, in partial mode, we can't say this is necessarily over. + // If there's more *pattern* left, then + if (partial) { + // ran out of file + this.debug("\n>>> no match, partial?", file, fr, pattern, pr); + if (fr === fl) return true; + } + return false; + } + + // something other than ** + // non-magic patterns just have to match exactly + // patterns with magic have been turned into regexps. + var hit; + if (typeof p === "string") { + if (options.nocase) { + hit = f.toLowerCase() === p.toLowerCase(); + } else { + hit = f === p; + } + this.debug("string match", p, f, hit); + } else { + hit = f.match(p); + this.debug("pattern match", p, f, hit); + } + + if (!hit) return false; + } + + // Note: ending in / means that we'll get a final "" + // at the end of the pattern. This can only match a + // corresponding "" at the end of the file. + // If the file ends in /, then it can only match a + // a pattern that ends in /, unless the pattern just + // doesn't have any more for it. But, a/b/ should *not* + // match "a/b/*", even though "" matches against the + // [^/]*? pattern, except in partial mode, where it might + // simply not be reached yet. + // However, a/b/ should still satisfy a/* + + // now either we fell off the end of the pattern, or we're done. + if (fi === fl && pi === pl) { + // ran out of pattern and filename at the same time. + // an exact hit! + return true; + } else if (fi === fl) { + // ran out of file, but still had pattern left. + // this is ok if we're doing the match as part of + // a glob fs traversal. + return partial; + } else if (pi === pl) { + // ran out of pattern, still have file left. + // this is only acceptable if we're on the very last + // empty segment of a file with a trailing slash. + // a/* should match a/b/ + var emptyFileEnd = fi === fl - 1 && file[fi] === ""; + return emptyFileEnd; + } + + // should be unreachable. + throw new Error("wtf?"); + } + + static makeRe(pattern: string, options?: IOptions) { + return new Minimatch(pattern, options || {}).makeRe(); + } + + makeRe() { + if (this.regexp || this.regexp === false) return this.regexp; + + // at this point, this.set is a 2d array of partial + // pattern strings, or "**". + // + // It's better to use .match(). This function shouldn't + // be used, really, but it's pretty convenient sometimes, + // when you just want to work with a regex. + var set = this.set; + + if (!set.length) { + this.regexp = false; + return this.regexp; + } + var options = this.options; + + var twoStar = options.noglobstar + ? star + : options.dot + ? twoStarDot + : twoStarNoDot; + var flags = options.nocase ? "i" : ""; + + var re = (set as any) + .map(function (pattern: string[]) { + return pattern + .map(function (p) { + return p === GLOBSTAR + ? twoStar + : typeof p === "string" + ? regExpEscape(p) + : (p as any)._src; + }) + .join("\\/"); + }) + .join("|"); + + // must match entire pattern + // ending in a * or ** will make it less strict. + re = "^(?:" + re + ")$"; + + // can match anything, as long as it's not this. + if (this.negate) re = "^(?!" + re + ").*$"; + + try { + this.regexp = new RegExp(re, flags); + } catch (ex) { + this.regexp = false; + } + return this.regexp; + } + + static match(list: string[], pattern: string, options: IOptions) { + options = options || {}; + var mm = new Minimatch(pattern, options); + list = list.filter(function (f) { + return mm.match(f); + }); + if (mm.options.nonull && !list.length) { + list.push(pattern); + } + return list; + } + + /** + * Return true if the filename matches the pattern, or false otherwise. + */ + match(f: string, partial?: boolean): boolean { + this.debug("match", f, this.pattern); + // short-circuit in the case of busted things. + // comments, etc. + if (this.comment) return false; + if (this.empty) return f === ""; + + if (f === "/" && partial) return true; + + var options = this.options; + + // windows: need to use /, not \ + if (path.sep !== "/") { + f = f.split(path.sep).join("/"); + } + + // treat the test path as a set of pathparts. + const files = f.split(slashSplit); + this.debug(this.pattern, "split", files); + + // just ONE of the pattern sets in this.set needs to match + // in order for it to be valid. If negating, then just one + // match means that we have failed. + // Either way, return on the first hit. + + var set = this.set; + this.debug(this.pattern, "set", set); + + // Find the basename of the path by looking for the last non-empty segment + var filename: string | undefined; + var i; + for (i = f.length - 1; i >= 0; i--) { + filename = f[i]; + if (filename) break; + } + + for (i = 0; i < set.length; i++) { + var pattern = set[i]; + var file: (string | undefined)[] = files; + if (options.matchBase && pattern.length === 1) { + file = [filename]; + } + var hit = this.matchOne(file, pattern, partial); + if (hit) { + if (options.flipNegate) return true; + return !this.negate; + } + } + + // didn't get any hits. this is success if it's a negative + // pattern, failure otherwise. + if (options.flipNegate) return false; + return this.negate; + } +} + +// replace stuff like \* with * +function globUnescape(s: string) { + return s.replace(/\\(.)/g, "$1"); +} + +function regExpEscape(s: string) { + return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 3e49cea10..e45e5dc02 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -20,3 +20,4 @@ export * from "./walletTypes.js"; export * from "./i18n.js"; export * from "./logging.js"; export * from "./url.js"; +export * from "./globbing/minimatch.js";
\ No newline at end of file diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json index 1c391e1de..52a8f817e 100644 --- a/packages/taler-wallet-cli/package.json +++ b/packages/taler-wallet-cli/package.json @@ -46,10 +46,8 @@ "dependencies": { "@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*", - "@types/minimatch": "^3.0.3", "axios": "^0.21.1", "cancellationtoken": "^2.2.0", - "minimatch": "^3.0.4", "source-map-support": "^0.5.19", "tslib": "^2.1.0" } diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index e155bd3d3..e258330d6 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -15,7 +15,9 @@ */ import { - delayMs, + minimatch +} from "@gnu-taler/taler-util"; +import { GlobalTestState, runTestWithState, shouldLingerInTest, @@ -53,7 +55,6 @@ import { runWallettestingTest } from "./test-wallettesting"; import { runTestWithdrawalManualTest } from "./test-withdrawal-manual"; import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated"; -import M from "minimatch"; import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion"; import { runLibeufinBasicTest } from "./test-libeufin-basic"; import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation"; @@ -231,7 +232,7 @@ export async function runTests(spec: TestRunSpec) { for (const [n, testCase] of allTests.entries()) { const testName = getTestName(testCase); - if (spec.includePattern && !M(testName, spec.includePattern)) { + if (spec.includePattern && !minimatch(testName, spec.includePattern)) { continue; } |