Skip to content

Commit 971ef47

Browse files
committed
fix: improve glob expressions matching
Closes #385
1 parent bbecd16 commit 971ef47

File tree

4 files changed

+189
-13
lines changed

4 files changed

+189
-13
lines changed

src/core/utils/glob.spec.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { globToRegExp } from "./glob";
1+
import { globToRegExp, isBalancedCurlyBrackets, isValidGlobExpression } from "./glob";
22

33
describe("globToRegExp()", () => {
44
it("glob = <empty>", () => {
@@ -24,19 +24,95 @@ describe("globToRegExp()", () => {
2424
expect(globToRegExp("/foo/*")).toBe("\\/foo\\/.*");
2525
});
2626

27-
it("glob = /*.{jpg}", () => {
28-
expect(globToRegExp("/*.{jpg}")).toBe("\\/.*(jpg)");
27+
it("glob = /*.{ext}", () => {
28+
expect(globToRegExp("/*.{ext}")).toBe("\\/.*(ext)");
2929
});
3030

31-
it("glob = /*.{jpg,gif}", () => {
32-
expect(globToRegExp("/*.{jpg,gif}")).toBe("\\/.*(jpg|gif)");
31+
it("glob = /*.{ext,gif}", () => {
32+
expect(globToRegExp("/*.{ext,gif}")).toBe("\\/.*(ext|gif)");
3333
});
3434

35-
it("glob = /foo/*.{jpg,gif}", () => {
36-
expect(globToRegExp("/foo/*.{jpg,gif}")).toBe("\\/foo\\/.*(jpg|gif)");
35+
it("glob = /foo/*.{ext,gif}", () => {
36+
expect(globToRegExp("/foo/*.{ext,gif}")).toBe("\\/foo\\/.*(ext|gif)");
3737
});
3838

3939
it("glob = {foo,bar}.json", () => {
4040
expect(globToRegExp("{foo,bar}.json")).toBe("(foo|bar).json");
4141
});
4242
});
43+
44+
// describe isValidGlobExpression
45+
46+
describe("isValidGlobExpression()", () => {
47+
// valid expressions
48+
["*", "/*", "/foo/*", "/foo/*.ext", "/*.ext", "*.ext", "/foo/*.ext", "/foo/*.{ext}", "/foo/*.{ext,ext}"].forEach((glob) => {
49+
describe("should be TRUE for the following values", () => {
50+
it(`glob = ${glob}`, () => {
51+
expect(isValidGlobExpression(glob)).toBe(true);
52+
});
53+
});
54+
});
55+
56+
// invalid expressions
57+
[
58+
undefined,
59+
"",
60+
"*.*",
61+
"**.*",
62+
"**.**",
63+
"**",
64+
"*/*",
65+
"*/*.ext",
66+
"*.ext/*",
67+
"*/*.ext*",
68+
"*.ext/*.ext",
69+
"/blog/*/management",
70+
"/foo/*.{ext,,,,}",
71+
"/foo/*.{ext,",
72+
"/foo/*.ext,}",
73+
"/foo/*.ext}",
74+
"/foo/*.{}",
75+
"/foo/*.",
76+
"/foo/.",
77+
].forEach((glob) => {
78+
describe("should be FALSE for the following values", () => {
79+
it(`glob = ${glob}`, () => {
80+
expect(isValidGlobExpression(glob)).toBe(false);
81+
});
82+
});
83+
});
84+
});
85+
86+
describe("isBalancedCurlyBrackets()", () => {
87+
it("should be true for {}", () => {
88+
expect(isBalancedCurlyBrackets("{,,,}")).toBe(true);
89+
});
90+
91+
it("should be true for {}{}{}", () => {
92+
expect(isBalancedCurlyBrackets("{,,,}{,,,}{,,,}")).toBe(true);
93+
});
94+
95+
it("should be true for {{}}", () => {
96+
expect(isBalancedCurlyBrackets("{,,,{,,,},,,}")).toBe(true);
97+
});
98+
99+
it("should be false for }{", () => {
100+
expect(isBalancedCurlyBrackets("},,,{")).toBe(false);
101+
});
102+
103+
it("should be false for }{}{", () => {
104+
expect(isBalancedCurlyBrackets("},,,{,,,},,,{")).toBe(false);
105+
});
106+
107+
it("should be false for {", () => {
108+
expect(isBalancedCurlyBrackets("{,,,")).toBe(false);
109+
});
110+
111+
it("should be false for }", () => {
112+
expect(isBalancedCurlyBrackets(",,,}")).toBe(false);
113+
});
114+
115+
it("should be false for {}}{{}", () => {
116+
expect(isBalancedCurlyBrackets("{,,,}},,,{{,,,}")).toBe(false);
117+
});
118+
});

src/core/utils/glob.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import chalk from "chalk";
22
import { logger } from "./logger";
33

44
/**
5-
* Turn expression into a valid regex
5+
* Turn expression into a valid regexp
6+
*
7+
* @param glob A string containing a valid wildcard expression
8+
* @returns a string containing a valid RegExp
69
*/
710
export function globToRegExp(glob: string | undefined) {
811
logger.silly(`turning glob expression into valid RegExp`);
@@ -25,3 +28,88 @@ export function globToRegExp(glob: string | undefined) {
2528

2629
return glob.replace(/\//g, "\\/").replace("*.", ".*").replace("/*", "/.*");
2730
}
31+
32+
/**
33+
* Check if the route rule contains a valid wildcard expression
34+
*
35+
* @param glob A string containing a valid wildcard expression
36+
* @returns true if the glob expression is valid, false otherwise
37+
* @see https://docs.microsoft.com/azure/static-web-apps/configuration#wildcards
38+
*/
39+
export function isValidGlobExpression(glob: string | undefined) {
40+
logger.silly(`checking if glob expression is valid`);
41+
logger.silly(` - glob: ${chalk.yellow(glob)}`);
42+
43+
if (!glob) {
44+
logger.silly(` - glob is empty. Return false`);
45+
return false;
46+
}
47+
48+
if (glob === "*") {
49+
logger.silly(` - glob is *`);
50+
return true;
51+
}
52+
53+
const hasWildcard = glob.includes("*");
54+
55+
if (hasWildcard) {
56+
const paths = glob.split("*");
57+
if (paths.length > 2) {
58+
logger.silly(` - glob has more than one wildcard. Return false`);
59+
return false;
60+
}
61+
62+
const pathBeforeWildcard = paths[0];
63+
if (pathBeforeWildcard && glob.endsWith("*")) {
64+
logger.silly(` - glob ends with *. Return true`);
65+
return true;
66+
}
67+
68+
const pathAfterWildcard = paths[1];
69+
if (pathAfterWildcard) {
70+
logger.silly(` - pathAfterWildcard: ${chalk.yellow(pathAfterWildcard)}`);
71+
72+
if (isBalancedCurlyBrackets(glob) === false) {
73+
logger.silly(` - pathAfterWildcard contains unbalanced { } syntax. Return false`);
74+
return false;
75+
}
76+
77+
// match exactly extensions of type:
78+
// --> /blog/*.html
79+
// --> /blog/*.{html,jpg}
80+
const filesExtensionMatch = pathAfterWildcard.match(/\.(\w+|\{\w+(,\w+)*\})$/);
81+
82+
if (filesExtensionMatch) {
83+
logger.silly(` - pathAfterWildcard match a file extension. Return true`);
84+
return true;
85+
} else {
86+
logger.silly(` - pathAfterWildcard doesn't match a file extension. Return false`);
87+
return false;
88+
}
89+
}
90+
}
91+
92+
return false;
93+
}
94+
95+
/**
96+
* Checks if a string expression has balanced curly brackets
97+
*
98+
* @param str the string expression to be checked
99+
* @returns true if the string expression has balanced curly brackets, false otherwise
100+
*/
101+
export function isBalancedCurlyBrackets(str: string) {
102+
const stack = [];
103+
for (let i = 0; i < str.length; i++) {
104+
const char = str[i];
105+
if (char === "{") {
106+
stack.push(char);
107+
} else if (char === "}") {
108+
if (stack.length === 0) {
109+
return false;
110+
}
111+
stack.pop();
112+
}
113+
}
114+
return stack.length === 0;
115+
}

src/msha/routes-engine/route-processor.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import chalk from "chalk";
33
import { DEFAULT_CONFIG } from "../../config";
44
import { logger } from "../../core";
55
import { AUTH_STATUS, SWA_CLI_APP_PROTOCOL } from "../../core/constants";
6-
import { globToRegExp } from "../../core/utils/glob";
6+
import { globToRegExp, isValidGlobExpression } from "../../core/utils/glob";
77
import { getIndexHtml } from "./rules/routes";
88

99
export function doesRequestPathMatchRoute(
@@ -46,7 +46,7 @@ export function doesRequestPathMatchRoute(
4646
return true;
4747
}
4848

49-
// Since this is a file request, return now, since tring to get a match by appending /index.html doesn't apply here
49+
// Since this is a file request, return now, since we are trying to get a match by appending /index.html doesn't apply here
5050
if (!route) {
5151
logger.silly(` - route: ${chalk.yellow(route || "<empty>")}`);
5252
logger.silly(` - match: ${chalk.yellow(false)}`);
@@ -100,11 +100,17 @@ function doesRequestPathMatchWildcardRoute(requestPath: string, requestPathFileW
100100
// before processing regexp which might be expensive
101101
// let's check first if both path and rule start with the same substring
102102
if (pathBeforeWildcard && requestPath.startsWith(pathBeforeWildcard) === false) {
103-
logger.silly(` - substring don't match. Exit`);
103+
logger.silly(` - base path doesn't match. Exit`);
104104

105105
return false;
106106
}
107107

108+
// also, let's check if the route rule doesn't contains a wildcard in the middle of the path
109+
if (isValidGlobExpression(requestPathFileWithWildcard) === false) {
110+
logger.silly(` - route rule contains a wildcard in the middle of the path. Exit`);
111+
return false;
112+
}
113+
108114
// we don't support full globs in the config file.
109115
// add this little utility to convert a wildcard into a valid glob pattern
110116
const regexp = new RegExp(`^${globToRegExp(requestPathFileWithWildcard)}$`);

src/msha/routes-engine/rules/navigation-fallback.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "fs";
33
import type http from "http";
44
import path from "path";
55
import { logger } from "../../../core";
6-
import { globToRegExp } from "../../../core/utils/glob";
6+
import { globToRegExp, isValidGlobExpression } from "../../../core/utils/glob";
77
import { AUTH_STATUS } from "../../../core/constants";
88
import { doesRequestPathMatchRoute } from "../route-processor";
99
import { getIndexHtml } from "./routes";
@@ -50,12 +50,18 @@ export function navigationFallback(req: http.IncomingMessage, res: http.ServerRe
5050

5151
// parse the exclusion rules and match at least one rule
5252
const isMatchedExcludeRule = navigationFallback?.exclude?.some((filter) => {
53+
if (isValidGlobExpression(filter) === false) {
54+
logger.silly(` - invalid rule ${chalk.yellow(filter)}`);
55+
logger.silly(` - mark as no match`);
56+
return false;
57+
}
58+
5359
// we don't support full globs in the config file.
5460
// add this little utility to convert a wildcard into a valid glob pattern
5561
const regexp = new RegExp(`^${globToRegExp(filter)}$`);
5662
const isMatch = regexp.test(originlUrl!);
5763

58-
logger.silly(` - exclude: ${chalk.yellow(filter)}`);
64+
logger.silly(` - rule: ${chalk.yellow(filter)}`);
5965
logger.silly(` - regexp: ${chalk.yellow(regexp)}`);
6066
logger.silly(` - isRegexpMatch: ${chalk.yellow(isMatch)}`);
6167

0 commit comments

Comments
 (0)