fix: count a pct-triplet as one character under every operator

Addresses Copilot's round-11 review on #2035, which corrects my own round-9
fix rather than the original code.

That fix grouped pct-triplets only under `+` and `#`, on the reasoning that
every other operator escapes a `%` anyway, so `%61` is three characters
rather than one. Wrong, in a way the narrower rule made visible one operator
over -- measured on 2e06b369:

  x/{v:1}  v = "%61%62"  =>  "x/%25"     truncated to a `%` that was never
                                          a character of the value

RFC 6570 2.4.1 counts characters and says a pct-encoded triplet counts as a
single one, without reference to the operator. Grouping per-operator also
made the same input mean two different things: `%61` indivisible under `+`,
three loose characters under `{v}`. What the operator decides is how a
retained unit is *encoded* afterwards -- a simple expansion still escapes
the kept triplet's `%`, so `%61` goes out as `%2561` -- not what counts as
one character.

`prefixUnits` no longer takes an operator, and the test that codified the
old split now asserts the uniform rule across simple, named, and reserved
expansion.

npm run ci passes.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-17 00:03:10 -04:00
commit c6fb627142

View file

@ -714,11 +714,25 @@ describe("prefix modifier truncation", () => {
);
});
it("counts a triplet as three characters where the encoder escapes it", () => {
// Under a non-reserved operator `%` is escaped anyway (`%61` -> `%2561`),
// so the three characters really are three and grouping them would
// mis-count the prefix.
expect(expandUriTemplate("x/{v:3}", { v: "%61%62" })).toBe("x/%2561");
it.each([
// The counting rule is operator-INDEPENDENT: a triplet is one character
// wherever it appears. An earlier revision grouped only under `+`/`#`,
// which made the same input mean two things and truncated `{v:1}` to a `%`
// that was never a character of the value (it emitted "%25").
["x/{v:1}", "x/%2561"],
["x/{v:2}", "x/%2561%2562"],
["x/{v:3}", "x/%2561%2562"],
])(
"counts a triplet as one character under a simple expansion (%s)",
(template, expected) => {
// What the operator decides is the *encoding* afterwards -- a simple
// expansion still escapes the retained triplet's `%`, so `%61` -> `%2561`.
expect(expandUriTemplate(template, { v: "%61%62" })).toBe(expected);
},
);
it("counts a triplet as one character under a named operator too", () => {
expect(expandUriTemplate("x{?v:1}", { v: "%61%62" })).toBe("x?v=%2561");
});
it("truncates by code point, not UTF-16 code unit", () => {