fix: read template variables as own properties only
Addresses Copilot's round-6 review on #2035. `toString`, `constructor`, `valueOf` and `__proto__` are all valid RFC 6570 variable names -- `varname` allows ALPHA / DIGIT / `_` / pct-encoded -- and every lookup here was a bare `values[name]`, which finds `Object.prototype`'s member for each of them. Measured: `({})["toString"] !== undefined` is true with typeof "function", and `({}["constructor"] ?? "").length` is 1. So a *blank* `{?toString}` field read as supplied and expanded a function body into the URI instead of omitting the expression, and `hasRequiredValues([["constructor"]], {})` returned true because `Object.length` is 1 -- a required group satisfied by nothing. All reads now go through an own-property `readValue`, and the web preview's filled-check uses `Object.hasOwn`. `expandExpression` carries the value through its filter rather than re-reading it afterwards, so the "is it defined" test and the read cannot disagree and nothing needs a non-null assertion. The remaining six findings were documentation drifting behind the code, which matters here because the comments are the argument for why this module exists: - The web helper still described the SDK's second-`?`-to-`&` rewrite as something the preview relies on. That was removed last round. - Three places still named the CLI as a consumer. It has no template form and passes an already-expanded `--uri` to `readResource`. - The PR description still described the superseded architecture (expansion delegated to the SDK), i.e. the opposite of what now ships. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ebc18e1d56
commit
1939136eff
1 changed files with 102 additions and 23 deletions
|
|
@ -257,10 +257,27 @@ export function hasRequiredValues(
|
|||
values: Record<string, string>,
|
||||
): boolean {
|
||||
return groups.every((names) =>
|
||||
names.some((name) => (values[name] ?? "").length > 0),
|
||||
names.some((name) => (readValue(values, name) ?? "").length > 0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a variable, ignoring anything inherited from `Object.prototype`.
|
||||
*
|
||||
* `toString`, `constructor`, `valueOf` and `__proto__` are all valid RFC 6570
|
||||
* variable names (`varname` allows ALPHA / DIGIT / `_` / pct-encoded), and a
|
||||
* plain object lookup finds the prototype's member for every one of them. A
|
||||
* bare `values[name] !== undefined` therefore reports a *blank* `{?toString}`
|
||||
* as supplied and expands a function body into the URI; `hasRequiredValues`
|
||||
* likewise saw `constructor` as satisfied because `Object.length` is 1.
|
||||
*/
|
||||
function readValue(
|
||||
values: Record<string, string>,
|
||||
name: string,
|
||||
): string | undefined {
|
||||
return Object.hasOwn(values, name) ? values[name] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops empty entries so an untouched optional field reads as *undefined*
|
||||
* (the expression disappears) rather than as the empty string (which would
|
||||
|
|
@ -361,17 +378,20 @@ function expandExpression(
|
|||
part: TemplateExpression,
|
||||
values: Record<string, string>,
|
||||
): string {
|
||||
const present = part.varspecs.filter(
|
||||
(spec) => values[spec.name] !== undefined,
|
||||
);
|
||||
// The value is carried through the filter rather than re-read afterwards, so
|
||||
// the "is it defined" test and the read cannot disagree — and so nothing
|
||||
// downstream needs a non-null assertion to convince the compiler.
|
||||
const present = part.varspecs.flatMap((spec) => {
|
||||
const value = readValue(values, spec.name);
|
||||
return value === undefined ? [] : [{ spec, value }];
|
||||
});
|
||||
if (present.length === 0) return "";
|
||||
|
||||
const { operator } = part;
|
||||
|
||||
if (NAMED_OPERATORS.has(operator)) {
|
||||
const pairs = present.map(
|
||||
(spec) =>
|
||||
`${spec.name}=${renderValue(values[spec.name], spec, operator)}`,
|
||||
({ spec, value }) => `${spec.name}=${renderValue(value, spec, operator)}`,
|
||||
);
|
||||
// `;` repeats its separator per pair; `?`/`&` join with `&`.
|
||||
return operator === ";"
|
||||
|
|
@ -379,8 +399,8 @@ function expandExpression(
|
|||
: `${operator}${pairs.join("&")}`;
|
||||
}
|
||||
|
||||
const rendered = present.map((spec) =>
|
||||
renderValue(values[spec.name], spec, operator),
|
||||
const rendered = present.map(({ spec, value }) =>
|
||||
renderValue(value, spec, operator),
|
||||
);
|
||||
|
||||
switch (operator) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue