@kensio/yulin 1.20.11 → 1.20.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /** One pattern and the flags to compile it with. */
2
+ export interface SimAthenaLiftedPattern {
3
+ readonly pattern: string;
4
+ readonly flags: string;
5
+ }
6
+ /**
7
+ * One pattern with the flags Java writes inside it lifted out beside it.
8
+ *
9
+ * Athena's regular expressions are Joni, which reads `(?i)` at the head of a
10
+ * pattern as a flag over the whole of it. JavaScript reads the same text as a
11
+ * group and refuses it. `(?i)`, `(?m)` and `(?s)` map onto flags of the same
12
+ * letter, and lifting them is all it takes.
13
+ *
14
+ * A flag Joni has and JavaScript has not, `(?x)` among them, stays in the
15
+ * pattern for `RegExp` to turn down. So does a group written anywhere but the
16
+ * head, since JavaScript has no way to turn a flag on part way through. The
17
+ * scoped form `(?i:...)` needs nothing done to it and already runs.
18
+ *
19
+ * A flag group captures nothing in Java, so taking one off the front leaves
20
+ * the capture groups numbered as the statement wrote them.
21
+ */
22
+ export declare function simAthenaLiftedPattern(pattern: string, flags: string): SimAthenaLiftedPattern;
@@ -0,0 +1,34 @@
1
+ /** One of Java's inline flag groups at the head of a pattern, as `(?i)`. */
2
+ const leadingFlag = /^\(\?[ims]+\)/u;
3
+ /**
4
+ * One pattern with the flags Java writes inside it lifted out beside it.
5
+ *
6
+ * Athena's regular expressions are Joni, which reads `(?i)` at the head of a
7
+ * pattern as a flag over the whole of it. JavaScript reads the same text as a
8
+ * group and refuses it. `(?i)`, `(?m)` and `(?s)` map onto flags of the same
9
+ * letter, and lifting them is all it takes.
10
+ *
11
+ * A flag Joni has and JavaScript has not, `(?x)` among them, stays in the
12
+ * pattern for `RegExp` to turn down. So does a group written anywhere but the
13
+ * head, since JavaScript has no way to turn a flag on part way through. The
14
+ * scoped form `(?i:...)` needs nothing done to it and already runs.
15
+ *
16
+ * A flag group captures nothing in Java, so taking one off the front leaves
17
+ * the capture groups numbered as the statement wrote them.
18
+ */
19
+ export function simAthenaLiftedPattern(pattern, flags) {
20
+ const letters = new Set();
21
+ let rest = pattern;
22
+ let head = leadingFlag.exec(rest);
23
+ while (head !== null) {
24
+ // Everything the group holds between its `(?` and its `)`.
25
+ const written = head[0].slice(2, -1);
26
+ for (const letter of written) {
27
+ letters.add(letter);
28
+ }
29
+ rest = rest.slice(head[0].length);
30
+ head = leadingFlag.exec(rest);
31
+ }
32
+ const added = [...letters].filter((letter) => !flags.includes(letter));
33
+ return { pattern: rest, flags: flags + added.join("") };
34
+ }
@@ -1,4 +1,5 @@
1
1
  import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
2
+ import { simAthenaLiftedPattern } from "./sim-athena-regexp-flags.js";
2
3
  import { isExplicitNull, shimNumber, shimText, simAthenaScalarShim, } from "./sim-athena-shim-registry.js";
3
4
  /**
4
5
  * Trino's regular expression functions.
@@ -79,10 +80,11 @@ function expressionFor(pattern, flags = "u") {
79
80
  if (pattern === undefined) {
80
81
  return undefined;
81
82
  }
83
+ const lifted = simAthenaLiftedPattern(pattern, flags);
82
84
  try {
83
85
  // The pattern is the query's own, which is the whole point of the function.
84
86
  // oxlint-disable-next-line security/detect-non-literal-regexp
85
- return new RegExp(pattern, flags);
87
+ return new RegExp(lifted.pattern, lifted.flags);
86
88
  }
87
89
  catch {
88
90
  return undefined;
@@ -0,0 +1,24 @@
1
+ /** One URI reference split the way Java's `URI` splits it. */
2
+ export interface SimAthenaUrlParts {
3
+ readonly protocol: string;
4
+ readonly host: string;
5
+ readonly port: number | null;
6
+ readonly path: string;
7
+ readonly query: string;
8
+ readonly fragment: string;
9
+ }
10
+ /**
11
+ * One URI reference split into its parts, or nothing where it is no reference.
12
+ *
13
+ * Trino reads a URL with `new URI(text)`, which takes a reference with no
14
+ * scheme of its own. `/reports/august?tenant=acme` is one, and it is the shape
15
+ * a CloudFront access log holds, since the log carries the path and the query
16
+ * in columns of their own and no whole URL anywhere.
17
+ *
18
+ * The parts are read off the text rather than resolved against a base. That is
19
+ * what `URI` does, and it keeps a relative path as the text wrote it.
20
+ *
21
+ * A part the reference leaves out comes back as the empty string, which is
22
+ * what Trino answers for one. The port has no empty form and answers null.
23
+ */
24
+ export declare function simAthenaUrlParts(text: string): SimAthenaUrlParts | undefined;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A URI reference, split as RFC 3986 writes the expression for it.
3
+ *
4
+ * This is the expression the RFC prints in its own appendix, character for
5
+ * character. Its groups are optional and never nested, so what a reference
6
+ * that does not match costs in backtracking is bounded by the text's length.
7
+ */
8
+ const reference =
9
+ // oxlint-disable-next-line security/detect-unsafe-regex
10
+ /^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/u;
11
+ /** The characters RFC 2396 allows, which is what Java's `URI` takes. */
12
+ const disallowed = /[^A-Za-z0-9;/?:@&=+$,\-_.!~*'()%[\]#]/u;
13
+ /** A percent naming no byte, which Java's `URI` reads as malformed. */
14
+ const malformedEscape = /%(?![0-9A-Fa-f]{2})/u;
15
+ /** A scheme starts with a letter and carries no other punctuation. */
16
+ const schemeName = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
17
+ const digits = /^\d+$/u;
18
+ /**
19
+ * One URI reference split into its parts, or nothing where it is no reference.
20
+ *
21
+ * Trino reads a URL with `new URI(text)`, which takes a reference with no
22
+ * scheme of its own. `/reports/august?tenant=acme` is one, and it is the shape
23
+ * a CloudFront access log holds, since the log carries the path and the query
24
+ * in columns of their own and no whole URL anywhere.
25
+ *
26
+ * The parts are read off the text rather than resolved against a base. That is
27
+ * what `URI` does, and it keeps a relative path as the text wrote it.
28
+ *
29
+ * A part the reference leaves out comes back as the empty string, which is
30
+ * what Trino answers for one. The port has no empty form and answers null.
31
+ */
32
+ export function simAthenaUrlParts(text) {
33
+ if (disallowed.test(text) || malformedEscape.test(text)) {
34
+ return undefined;
35
+ }
36
+ const found = reference.exec(text);
37
+ if (found === null) {
38
+ return undefined;
39
+ }
40
+ const scheme = found.at(1);
41
+ if (scheme !== undefined && !schemeName.test(scheme)) {
42
+ return undefined;
43
+ }
44
+ const authority = found.at(2);
45
+ const path = found.at(3) ?? "";
46
+ // A scheme followed by anything but a slash is an opaque URI, and Java reads
47
+ // neither a path nor a query out of one. `mailto:a@b` is the case.
48
+ const opaque = scheme !== undefined && authority === undefined && !path.startsWith("/");
49
+ return {
50
+ protocol: scheme ?? "",
51
+ ...hostAndPort(authority),
52
+ path: opaque ? "" : path,
53
+ query: opaque ? "" : (found.at(4) ?? ""),
54
+ fragment: found.at(5) ?? "",
55
+ };
56
+ }
57
+ /**
58
+ * The host and the port one authority names.
59
+ *
60
+ * The user's own credentials sit before an `@` and are no part of the host, so
61
+ * the digits in `user:1234@rain.example` are no port. A colon with anything
62
+ * but digits after it belongs to the host, which is what keeps the one inside
63
+ * an IPv6 literal out of the port.
64
+ */
65
+ function hostAndPort(authority) {
66
+ if (authority === undefined) {
67
+ return { host: "", port: null };
68
+ }
69
+ const written = authority.slice(authority.lastIndexOf("@") + 1);
70
+ const colon = written.lastIndexOf(":");
71
+ const port = written.slice(colon + 1);
72
+ if (colon === -1 || !digits.test(port)) {
73
+ return { host: written, port: null };
74
+ }
75
+ return { host: written.slice(0, colon), port: Number(port) };
76
+ }
@@ -2,8 +2,9 @@ import type { DatabaseSync } from "node:sqlite";
2
2
  /**
3
3
  * Trino's URL functions, which a query over access logs reaches for.
4
4
  *
5
- * Trino fails a query over text that is no URL and these answer null, the same
6
- * forgiving direction the rest of the engine takes.
5
+ * The extract functions answer null over text that is no URI reference, and so
6
+ * does Trino. Each one is `neverFails` there and answers null off a `URI` that
7
+ * would not parse.
7
8
  *
8
9
  * Trino answers with an empty string for a part the URL leaves out, apart from
9
10
  * the port, which has no empty form and answers null.
@@ -1,17 +1,20 @@
1
1
  import { shimText, simAthenaScalarShim } from "./sim-athena-shim-registry.js";
2
- /** What each `url_extract` function reads off a parsed URL. */
2
+ import { simAthenaUrlParts, } from "./sim-athena-url-parts.js";
3
+ /** What each `url_extract` function reads off a split reference. */
3
4
  const parts = new Map([
4
- ["url_extract_host", (url) => url.hostname],
5
- ["url_extract_path", (url) => url.pathname],
6
- ["url_extract_protocol", (url) => url.protocol.replace(":", "")],
7
- ["url_extract_fragment", (url) => url.hash.replace("#", "")],
8
- ["url_extract_query", (url) => url.search.replace("?", "")],
5
+ ["url_extract_host", (url) => url.host],
6
+ ["url_extract_path", (url) => url.path],
7
+ ["url_extract_protocol", (url) => url.protocol],
8
+ ["url_extract_fragment", (url) => url.fragment],
9
+ ["url_extract_query", (url) => url.query],
10
+ ["url_extract_port", (url) => url.port],
9
11
  ]);
10
12
  /**
11
13
  * Trino's URL functions, which a query over access logs reaches for.
12
14
  *
13
- * Trino fails a query over text that is no URL and these answer null, the same
14
- * forgiving direction the rest of the engine takes.
15
+ * The extract functions answer null over text that is no URI reference, and so
16
+ * does Trino. Each one is `neverFails` there and answers null off a `URI` that
17
+ * would not parse.
15
18
  *
16
19
  * Trino answers with an empty string for a part the URL leaves out, apart from
17
20
  * the port, which has no empty form and answers null.
@@ -19,39 +22,68 @@ const parts = new Map([
19
22
  export function simAthenaInstallUrlShims(database) {
20
23
  for (const [name, read] of parts) {
21
24
  simAthenaScalarShim(database, name, (value) => {
22
- const url = parsedUrl(shimText(value));
25
+ const url = readParts(shimText(value));
23
26
  return url === undefined ? null : read(url);
24
27
  });
25
28
  }
26
- simAthenaScalarShim(database, "url_extract_port", (value) => writtenPort(shimText(value)));
27
29
  simAthenaScalarShim(database, "url_extract_parameter", (value, name) => {
28
30
  const wanted = shimText(name);
29
- const url = parsedUrl(shimText(value));
31
+ const url = readParts(shimText(value));
30
32
  if (url === undefined || wanted === undefined) {
31
33
  return null;
32
34
  }
33
- return url.searchParams.get(wanted);
35
+ return new URLSearchParams(url.query).get(wanted);
34
36
  });
37
+ simAthenaScalarShim(database, "url_decode", (value) => decoded(shimText(value)));
38
+ simAthenaScalarShim(database, "url_encode", (value) => encoded(shimText(value)));
35
39
  }
36
- /** Everything between the scheme and the path, which is where a port is written. */
37
- const authority = /^[A-Za-z][\w+.-]*:\/\/[^/?#]*/u;
38
40
  /**
39
- * The port one URL names, or nothing where it names none.
41
+ * One value with its escapes read back, the way `url_decode` reads them.
40
42
  *
41
- * Read off the text rather than off the parsed URL, because the parser drops a
42
- * port that is the scheme's own default. Trino answers with the port a URL was
43
- * written with, so `http://rain.example:80/a` is eighty rather than nothing.
43
+ * Trino runs `URLDecoder.decode` over UTF-8, which reads `+` as a space and
44
+ * `%2B` as a plus. Replacing the plus before decoding is what keeps the two
45
+ * apart.
46
+ *
47
+ * Trino raises over an escape that names no byte, and writes a replacement
48
+ * character where the bytes it names are no UTF-8. `decodeURIComponent` throws
49
+ * over both, and this answers null for both, the same forgiving direction the
50
+ * rest of the file takes.
44
51
  */
45
- function writtenPort(value) {
46
- if (value === undefined || parsedUrl(value) === undefined) {
52
+ function decoded(value) {
53
+ if (value === undefined) {
54
+ return null;
55
+ }
56
+ try {
57
+ return decodeURIComponent(value.replaceAll("+", " "));
58
+ }
59
+ catch {
47
60
  return null;
48
61
  }
49
- const written = /:(\d+)$/u.exec(authority.exec(value)?.[0] ?? "")?.[1];
50
- return written === undefined ? null : Number(written);
51
62
  }
52
- function parsedUrl(value) {
63
+ /**
64
+ * One value escaped for a query string, the way `url_encode` escapes it.
65
+ *
66
+ * Trino runs Guava's form-parameter escaper, which keeps `-`, `_`, `.` and `*`
67
+ * alone and writes a space as `+`. `encodeURIComponent` keeps five characters
68
+ * beyond those four, and each of those is escaped here.
69
+ *
70
+ * No escape written here carries a character a later pass looks for, and a
71
+ * space is the only character `encodeURIComponent` writes as `%20`, since a
72
+ * percent in the value has already become `%25` by then.
73
+ */
74
+ function encoded(value) {
53
75
  if (value === undefined) {
54
- return undefined;
76
+ return null;
55
77
  }
56
- return URL.parse(value) ?? undefined;
78
+ return encodeURIComponent(value)
79
+ .replaceAll("!", "%21")
80
+ .replaceAll("'", "%27")
81
+ .replaceAll("(", "%28")
82
+ .replaceAll(")", "%29")
83
+ .replaceAll("~", "%7E")
84
+ .replaceAll("%20", "+");
85
+ }
86
+ /** One value split into its URL parts, or nothing where it is null or no URL. */
87
+ function readParts(value) {
88
+ return value === undefined ? undefined : simAthenaUrlParts(value);
57
89
  }
@@ -0,0 +1,17 @@
1
+ import type { SimAthenaSqlToken } from "./sim-athena-sql-tokens.js";
2
+ /**
3
+ * The columns a `NOT` in this query could have negated.
4
+ *
5
+ * A negated value names the partition a query does not want, so a column under
6
+ * a `NOT` is left unconstrained. A query filtering bots out of a day's logs
7
+ * negates nothing about the day, and reading how far each `NOT` reaches is
8
+ * what tells the two apart.
9
+ *
10
+ * An infix `NOT`, as in `day NOT IN ('a')`, is read as reaching nothing. Its
11
+ * own column keeps whatever else the statement constrains it to, and that is
12
+ * always a superset of what Athena reads, since `day = 'a' AND day NOT IN (x)`
13
+ * can only ever narrow. The query applies the exclusion itself once the rows
14
+ * are in. Naming the column here would drop its filter and widen the scan to
15
+ * every partition.
16
+ */
17
+ export declare function simAthenaNegatedColumns(tokens: readonly SimAthenaSqlToken[]): ReadonlySet<string>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The columns a `NOT` in this query could have negated.
3
+ *
4
+ * A negated value names the partition a query does not want, so a column under
5
+ * a `NOT` is left unconstrained. A query filtering bots out of a day's logs
6
+ * negates nothing about the day, and reading how far each `NOT` reaches is
7
+ * what tells the two apart.
8
+ *
9
+ * An infix `NOT`, as in `day NOT IN ('a')`, is read as reaching nothing. Its
10
+ * own column keeps whatever else the statement constrains it to, and that is
11
+ * always a superset of what Athena reads, since `day = 'a' AND day NOT IN (x)`
12
+ * can only ever narrow. The query applies the exclusion itself once the rows
13
+ * are in. Naming the column here would drop its filter and widen the scan to
14
+ * every partition.
15
+ */
16
+ export function simAthenaNegatedColumns(tokens) {
17
+ const negated = new Set();
18
+ for (const [position, token] of tokens.entries()) {
19
+ if (token.kind === "word" && token.text === "not") {
20
+ for (const name of namesUnder(tokens, position)) {
21
+ negated.add(name.toLowerCase());
22
+ }
23
+ }
24
+ }
25
+ return negated;
26
+ }
27
+ /** The words a `NOT` reaches no further than, `OR` aside. */
28
+ const boundary = new Set([
29
+ "and",
30
+ "except",
31
+ "from",
32
+ "group",
33
+ "having",
34
+ "intersect",
35
+ "limit",
36
+ "offset",
37
+ "order",
38
+ "union",
39
+ "where",
40
+ "window",
41
+ ]);
42
+ /**
43
+ * The identifiers one `NOT` reaches.
44
+ *
45
+ * `NOT` binds tighter than `AND`, so its reach ends at the next of the words
46
+ * above, outside any brackets, and at the bracket the `NOT` itself sits in.
47
+ * `OR` is absent from that list because a query carrying one is left
48
+ * unfiltered before this runs.
49
+ *
50
+ * Nothing here parses, so a name a `NOT` could not really have reached is
51
+ * dropped anyway. That direction only ever leaves a column unfiltered, which
52
+ * is the answer a query with no filter gets.
53
+ */
54
+ function namesUnder(tokens, position) {
55
+ const names = [];
56
+ let depth = 0;
57
+ let cursor = position + 1;
58
+ while (cursor < tokens.length) {
59
+ const token = tokens.at(cursor);
60
+ if (token?.kind === "symbol" && token.text === "(") {
61
+ depth += 1;
62
+ }
63
+ else if (token?.kind === "symbol" && token.text === ")") {
64
+ if (depth === 0) {
65
+ return names;
66
+ }
67
+ depth -= 1;
68
+ }
69
+ else if (depth === 0 &&
70
+ token?.kind === "word" &&
71
+ boundary.has(token.text)) {
72
+ return names;
73
+ }
74
+ else if (token?.kind === "word" || token?.kind === "quoted") {
75
+ names.push(token.text);
76
+ }
77
+ cursor += 1;
78
+ }
79
+ return names;
80
+ }
@@ -2,10 +2,13 @@
2
2
  * The partition values a query's `WHERE` clause pins down.
3
3
  *
4
4
  * Only two shapes are read, `column = 'value'` and `column IN ('a', 'b')`, and
5
- * they are read wherever they appear. A query carrying `OR` or `NOT` anywhere
6
- * is left unfiltered. A value under one arm of an `OR` constrains nothing on
7
- * its own, and a negated one names the partition the query does not want, so
8
- * reading either as a constraint would answer from the wrong prefixes.
5
+ * they are read wherever they appear. A query carrying `OR` anywhere is left
6
+ * unfiltered, since a value under one arm constrains nothing on its own.
7
+ *
8
+ * A `NOT` leaves the columns it reaches unconstrained and the rest alone. A
9
+ * negated value names the partition the query does not want, and reading it as
10
+ * a constraint would answer from the wrong prefixes. A negation on any other
11
+ * column leaves the partition constraints as true as they were.
9
12
  *
10
13
  * Two constraints on one column are intersected, since a query carrying both
11
14
  * wants the rows they agree on.
@@ -1,12 +1,16 @@
1
+ import { simAthenaNegatedColumns } from "./sim-athena-negated-columns.js";
1
2
  import { simAthenaSqlTokens } from "./sim-athena-sql-tokens.js";
2
3
  /**
3
4
  * The partition values a query's `WHERE` clause pins down.
4
5
  *
5
6
  * Only two shapes are read, `column = 'value'` and `column IN ('a', 'b')`, and
6
- * they are read wherever they appear. A query carrying `OR` or `NOT` anywhere
7
- * is left unfiltered. A value under one arm of an `OR` constrains nothing on
8
- * its own, and a negated one names the partition the query does not want, so
9
- * reading either as a constraint would answer from the wrong prefixes.
7
+ * they are read wherever they appear. A query carrying `OR` anywhere is left
8
+ * unfiltered, since a value under one arm constrains nothing on its own.
9
+ *
10
+ * A `NOT` leaves the columns it reaches unconstrained and the rest alone. A
11
+ * negated value names the partition the query does not want, and reading it as
12
+ * a constraint would answer from the wrong prefixes. A negation on any other
13
+ * column leaves the partition constraints as true as they were.
10
14
  *
11
15
  * Two constraints on one column are intersected, since a query carrying both
12
16
  * wants the rows they agree on.
@@ -52,6 +56,9 @@ export function simAthenaPartitionFilters(sql) {
52
56
  }
53
57
  }
54
58
  }
59
+ for (const name of simAthenaNegatedColumns(tokens)) {
60
+ byColumn.delete(name);
61
+ }
55
62
  return new SimAthenaPartitionFilters(byColumn);
56
63
  }
57
64
  /**
@@ -68,7 +75,7 @@ function narrow(byColumn, columnName, values) {
68
75
  byColumn.set(name, already.filter((value) => wanted.has(value)));
69
76
  }
70
77
  function isUnreadableTerm(token) {
71
- return token.kind === "word" && (token.text === "or" || token.text === "not");
78
+ return token.kind === "word" && token.text === "or";
72
79
  }
73
80
  /**
74
81
  * Read `('a', 'b')` starting at its opening bracket.
@@ -1,7 +1,7 @@
1
1
  import { SimWafIpSet, requiredSimWafIpAddressVersion, } from "../../ip-set/sim-waf-ip-set.js";
2
2
  import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
3
3
  import { SimWafPage } from "../sim-wafv2-page.js";
4
- import { refuseSimWafTags, requiredSimWafName } from "../sim-wafv2-input.js";
4
+ import { checkedSimWafDescription, refuseSimWafTags, requiredSimWafName, } from "../sim-wafv2-input.js";
5
5
  import { requireSimWafResource } from "../sim-wafv2-resource-lookup.js";
6
6
  /**
7
7
  * The commands that make, read, list and remove IP sets.
@@ -29,7 +29,7 @@ export class SimWafIpSetCommands {
29
29
  name: requiredSimWafName(input.Name),
30
30
  scope: requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName),
31
31
  accountRegionScope: this.#accountRegionScope,
32
- description: input.Description,
32
+ description: checkedSimWafDescription(input.Description),
33
33
  ipAddressVersion: requiredSimWafIpAddressVersion(input.IPAddressVersion),
34
34
  addresses: input.Addresses ?? [],
35
35
  });
@@ -59,10 +59,11 @@ export class SimWafIpSetCommands {
59
59
  */
60
60
  updateIpSet(command, options) {
61
61
  const { input } = command;
62
+ const description = checkedSimWafDescription(input.Description);
62
63
  const ipSet = this.require(input, "wafv2:UpdateIPSet", options);
63
64
  ipSet.replaceAddresses({
64
65
  addresses: input.Addresses ?? [],
65
- description: input.Description,
66
+ description,
66
67
  lockToken: input.LockToken,
67
68
  });
68
69
  return { $metadata: {}, NextLockToken: ipSet.lockToken };
@@ -1,7 +1,7 @@
1
1
  import { SimWafRegexPatternSet } from "../../regex-pattern-set/sim-waf-regex-pattern-set.js";
2
2
  import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
3
3
  import { SimWafPage } from "../sim-wafv2-page.js";
4
- import { refuseSimWafTags, requiredSimWafName } from "../sim-wafv2-input.js";
4
+ import { checkedSimWafDescription, refuseSimWafTags, requiredSimWafName, } from "../sim-wafv2-input.js";
5
5
  import { requireSimWafResource } from "../sim-wafv2-resource-lookup.js";
6
6
  /**
7
7
  * The commands that make, read, list and remove regex pattern sets.
@@ -29,7 +29,7 @@ export class SimWafRegexPatternSetCommands {
29
29
  name: requiredSimWafName(input.Name),
30
30
  scope: requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName),
31
31
  accountRegionScope: this.#accountRegionScope,
32
- description: input.Description,
32
+ description: checkedSimWafDescription(input.Description),
33
33
  regularExpressions: input.RegularExpressionList ?? [],
34
34
  });
35
35
  this.#authorizer.authorizeResource("wafv2:CreateRegexPatternSet", patternSet.arn, options?.caller);
@@ -66,10 +66,11 @@ export class SimWafRegexPatternSetCommands {
66
66
  */
67
67
  updateRegexPatternSet(command, options) {
68
68
  const { input } = command;
69
+ const description = checkedSimWafDescription(input.Description);
69
70
  const patternSet = this.require(input, "wafv2:UpdateRegexPatternSet", options);
70
71
  patternSet.replaceExpressions({
71
72
  regularExpressions: input.RegularExpressionList ?? [],
72
- description: input.Description,
73
+ description,
73
74
  lockToken: input.LockToken,
74
75
  });
75
76
  return { $metadata: {}, NextLockToken: patternSet.lockToken };
@@ -24,3 +24,18 @@ export declare function refuseSimWafTags(tags: readonly unknown[] | undefined, o
24
24
  * by ARN, and neither has anything else to fall back on.
25
25
  */
26
26
  export declare function requiredSimWafArn(arn: string | undefined, parameter: string): string;
27
+ /**
28
+ * Read the description a write gave, refusing one WAFv2 will not store.
29
+ *
30
+ * The empty string is the one worth catching. Code that reads a resource,
31
+ * changes part of it and writes the rest back hands the description straight
32
+ * through, and AWS answers `""` for some resources nobody has described. WAFv2
33
+ * refuses that write. A simulation that took it would leave the failure for
34
+ * the account to report.
35
+ *
36
+ * The two constraints are checked apart because AWS checks them apart. The
37
+ * pattern matches three characters at the shortest. `ab` is long enough for
38
+ * the length and still refused, and `""` fails both at once, which is how one
39
+ * message comes to report two errors.
40
+ */
41
+ export declare function checkedSimWafDescription(description: string | undefined): string | undefined;
@@ -1,4 +1,4 @@
1
- import { SimWafInvalidParameterException, SimWafUnsimulatedInputException, } from "../error/sim-wafv2.error.js";
1
+ import { SimWafInvalidParameterException, SimWafUnsimulatedInputException, SimWafValidationException, } from "../error/sim-wafv2.error.js";
2
2
  /**
3
3
  * Read the name a request named, refusing a request with none.
4
4
  */
@@ -47,3 +47,68 @@ export function requiredSimWafArn(arn, parameter) {
47
47
  }
48
48
  return arn;
49
49
  }
50
+ /**
51
+ * The shape WAFv2 documents for the description of a web ACL, an IP set and a
52
+ * regex pattern set.
53
+ *
54
+ * The refusal below quotes this expression back, the way WAFv2 quotes its own.
55
+ * WAFv2 writes the dots inside the character classes escaped and this does
56
+ * not, which is the same set of characters written two ways.
57
+ */
58
+ const descriptionExpression = /^[\w+=:#@/\-,.][\w+=:#@/\-,.\s]+[\w+=:#@/\-,.]$/;
59
+ const descriptionMaxLength = 256;
60
+ /**
61
+ * Read the description a write gave, refusing one WAFv2 will not store.
62
+ *
63
+ * The empty string is the one worth catching. Code that reads a resource,
64
+ * changes part of it and writes the rest back hands the description straight
65
+ * through, and AWS answers `""` for some resources nobody has described. WAFv2
66
+ * refuses that write. A simulation that took it would leave the failure for
67
+ * the account to report.
68
+ *
69
+ * The two constraints are checked apart because AWS checks them apart. The
70
+ * pattern matches three characters at the shortest. `ab` is long enough for
71
+ * the length and still refused, and `""` fails both at once, which is how one
72
+ * message comes to report two errors.
73
+ */
74
+ export function checkedSimWafDescription(description) {
75
+ if (description === undefined) {
76
+ return undefined;
77
+ }
78
+ const failures = [
79
+ ...lengthFailures(description),
80
+ ...patternFailures(description),
81
+ ];
82
+ if (failures.length === 0) {
83
+ return description;
84
+ }
85
+ throw new SimWafValidationException(`${failures.length} validation error${failures.length === 1 ? "" : "s"} detected: ${failures
86
+ .map((failure) => `Value '${description}' at 'description' failed to satisfy ` +
87
+ `constraint: ${failure}`)
88
+ .join("; ")}`);
89
+ }
90
+ /**
91
+ * What the length of a description falls foul of, if anything.
92
+ */
93
+ function lengthFailures(description) {
94
+ if (description.length === 0) {
95
+ return ["Member must have length greater than or equal to 1"];
96
+ }
97
+ if (description.length > descriptionMaxLength) {
98
+ return [
99
+ `Member must have length less than or equal to ${descriptionMaxLength}`,
100
+ ];
101
+ }
102
+ return [];
103
+ }
104
+ /**
105
+ * What the shape of a description falls foul of, if anything.
106
+ */
107
+ function patternFailures(description) {
108
+ if (descriptionExpression.test(description)) {
109
+ return [];
110
+ }
111
+ return [
112
+ `Member must satisfy regular expression pattern: ${descriptionExpression.source}`,
113
+ ];
114
+ }
@@ -2,7 +2,7 @@ import { SimWafAssociatedItemException } from "../../error/sim-wafv2.error.js";
2
2
  import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
3
3
  import { SimWafWebAcl } from "../../web-acl/sim-waf-web-acl.js";
4
4
  import { SimWafPage } from "../sim-wafv2-page.js";
5
- import { requiredSimWafName } from "../sim-wafv2-input.js";
5
+ import { checkedSimWafDescription, requiredSimWafName, } from "../sim-wafv2-input.js";
6
6
  import { requireSimWafResource, } from "../sim-wafv2-resource-lookup.js";
7
7
  import { refuseUnsimulatedSimWafWebAclInput } from "./sim-wafv2-unsimulated-web-acl-input.js";
8
8
  import { simWafWebAclOutput } from "./sim-waf-web-acl-output.js";
@@ -38,12 +38,13 @@ export class SimWafWebAclCommands {
38
38
  const name = requiredSimWafName(input.Name);
39
39
  const scope = requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName);
40
40
  refuseUnsimulatedSimWafWebAclInput(input, "CreateWebACL");
41
+ const configuration = configurationOf(input);
41
42
  const webAcl = new SimWafWebAcl({
42
43
  name,
43
44
  scope,
44
45
  accountRegionScope: this.#accountRegionScope,
45
- description: input.Description,
46
- configuration: configurationOf(input),
46
+ description: configuration.description,
47
+ configuration,
47
48
  regexPatternSets: this.#regexPatternSets,
48
49
  managedRules: this.#managedRules,
49
50
  clock: this.#clock,
@@ -68,8 +69,9 @@ export class SimWafWebAclCommands {
68
69
  updateWebAcl(command, options) {
69
70
  const { input } = command;
70
71
  refuseUnsimulatedSimWafWebAclInput(input, "UpdateWebACL");
72
+ const configuration = configurationOf(input);
71
73
  const webAcl = this.require(input, "wafv2:UpdateWebACL", options);
72
- webAcl.reconfigure(configurationOf(input), input.LockToken);
74
+ webAcl.reconfigure(configuration, input.LockToken);
73
75
  return { $metadata: {}, NextLockToken: webAcl.lockToken };
74
76
  }
75
77
  /**
@@ -134,6 +136,6 @@ function configurationOf(input) {
134
136
  rules: input.Rules,
135
137
  customResponseBodies: input.CustomResponseBodies,
136
138
  visibilityConfig: input.VisibilityConfig,
137
- description: input.Description,
139
+ description: checkedSimWafDescription(input.Description),
138
140
  };
139
141
  }
@@ -89,6 +89,17 @@ export declare class SimWafAssociatedItemException extends SimWafError {
89
89
  readonly name = "WAFAssociatedItemException";
90
90
  constructor(message: string);
91
91
  }
92
+ /**
93
+ * Simulated WAFv2 ValidationException error.
94
+ *
95
+ * WAFv2 checks the length and shape of a few members before the request
96
+ * reaches the rest of the API, and reports every failure in one message. A
97
+ * description of `""` fails two of those checks at once.
98
+ */
99
+ export declare class SimWafValidationException extends SimWafError {
100
+ readonly name = "ValidationException";
101
+ constructor(message: string);
102
+ }
92
103
  /**
93
104
  * A match declared against this simulation that it cannot answer with.
94
105
  *
@@ -100,6 +100,19 @@ export class SimWafAssociatedItemException extends SimWafError {
100
100
  super(message, { httpStatusCode: 400 });
101
101
  }
102
102
  }
103
+ /**
104
+ * Simulated WAFv2 ValidationException error.
105
+ *
106
+ * WAFv2 checks the length and shape of a few members before the request
107
+ * reaches the rest of the API, and reports every failure in one message. A
108
+ * description of `""` fails two of those checks at once.
109
+ */
110
+ export class SimWafValidationException extends SimWafError {
111
+ name = "ValidationException";
112
+ constructor(message) {
113
+ super(message, { httpStatusCode: 400 });
114
+ }
115
+ }
103
116
  /**
104
117
  * A match declared against this simulation that it cannot answer with.
105
118
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.20.11",
3
+ "version": "1.20.12",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",