


























@@ -273,19 +273,13 @@ export function isValidOcPath(input: unknown): input is string {
273273}
274274275275/**
276- * Positional tokens resolve to one concrete index/key based on
277- * container size at lookup time. Unlike `*`/`**`, they pick exactly
278- * one element so they don't trigger the wildcard guard.
279- *
280- * `$first` — index 0 / first-declared key
281- * `$last` — last index / last-declared key
282- * `-N` — Nth from end (indexable only); `-1` = last
276+ * Positional token: `$last` resolves to the last index / last-declared
277+ * key. Picks exactly one element, so it doesn't trigger wildcard guards.
283278 */
284-export const POS_FIRST = "$first";
285279export const POS_LAST = "$last";
286280287281export function isPositionalSeg(seg: string): boolean {
288-return seg === POS_FIRST || seg === POS_LAST || /^-\d+$/.test(seg);
282+return seg === POS_LAST;
289283}
290284291285/**
@@ -309,28 +303,11 @@ export interface PositionalContainer {
309303readonly keys?: readonly string[];
310304}
311305312-/** Resolve `$first`/`$last`/`-N` against a container; null when out of range. */
306+// Resolve `$last` against a container; null when empty.
313307export function resolvePositionalSeg(seg: string, container: PositionalContainer): string | null {
314-if (seg === POS_FIRST) {
315-if (container.size === 0) return null;
316-if (!container.indexable) return container.keys?.[0] ?? null;
317-return "0";
318-}
319-if (seg === POS_LAST) {
320-if (container.size === 0) return null;
321-if (!container.indexable) return container.keys?.[container.keys.length - 1] ?? null;
322-return String(container.size - 1);
323-}
324-if (/^-\d+$/.test(seg)) {
325-if (!container.indexable) return null;
326-// Guard against absurd magnitudes — `-9999999999` would coerce
327-// through Number into a big negative that wraps to a positive index.
328-const raw = Number(seg);
329-if (!Number.isInteger(raw) || Math.abs(raw) > 1e9) return null;
330-const n = container.size + raw;
331-return n >= 0 && n < container.size ? String(n) : null;
332-}
333-return null;
308+if (seg !== POS_LAST || container.size === 0) return null;
309+if (!container.indexable) return container.keys?.[container.keys.length - 1] ?? null;
310+return String(container.size - 1);
334311}
335312336313/**
@@ -378,13 +355,12 @@ export function parseUnionSeg(seg: string): readonly string[] | null {
378355}
379356380357/**
381- * Value predicate `[key<op>value]` filters by sibling-field comparison.
382- * Operators: `=` `!=` `*=` `^=` `$=` (string), `<` `<=` `>` `>=` (numeric).
383- * Multi-char operators are tried before single-char so `<=` beats `<`.
358+ * Value predicate `[key<op>value]`. Operators: `=` `!=` (string),
359+ * `<` `<=` `>` `>=` (numeric). Multi-char tried before single-char.
384360 */
385-export type PredicateOp = "=" | "!=" | "*=" | "^=" | "$=" | "<" | "<=" | ">" | ">=";
361+export type PredicateOp = "=" | "!=" | "<" | "<=" | ">" | ">=";
386362387-const PREDICATE_OPS: readonly PredicateOp[] = ["!=", "*=", "^=", "$=", "<=", ">=", "<", ">", "="];
363+const PREDICATE_OPS: readonly PredicateOp[] = ["!=", "<=", ">=", "<", ">", "="];
388364389365export function isPredicateSeg(seg: string): boolean {
390366if (seg.length < 4 || !seg.startsWith("[") || !seg.endsWith("]")) return false;
@@ -413,47 +389,27 @@ export function parsePredicateSeg(seg: string): PredicateSpec | null {
413389return null;
414390}
415391416-/**
417- * Evaluate a predicate against a string-coerced leaf value. Numeric
418- * operators require both sides to coerce to finite numbers via
419- * `Number()`. Returns false for null (non-leaf children).
420- */
392+// Numeric ops require both sides to coerce to finite numbers.
421393export function evaluatePredicate(actual: string | null, pred: PredicateSpec): boolean {
422394if (actual === null) return false;
423395switch (pred.op) {
424396case "=":
425397return actual === pred.value;
426398case "!=":
427399return actual !== pred.value;
428-case "*=":
429-return actual.includes(pred.value);
430-case "^=":
431-return actual.startsWith(pred.value);
432-case "$=":
433-return actual.endsWith(pred.value);
434400case "<":
435401case "<=":
436402case ">":
437403case ">=": {
438404const a = Number(actual);
439405const b = Number(pred.value);
440-if (!Number.isFinite(a) || !Number.isFinite(b)) {
441-return false;
442-}
443-switch (pred.op) {
444-case "<":
445-return a < b;
446-case "<=":
447-return a <= b;
448-case ">":
449-return a > b;
450-case ">=":
451-return a >= b;
452-}
453-return false;
406+if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
407+if (pred.op === "<") return a < b;
408+if (pred.op === "<=") return a <= b;
409+if (pred.op === ">") return a > b;
410+return a >= b;
454411}
455412}
456-return false;
457413}
458414459415/**
@@ -519,12 +475,9 @@ function extractSession(queryPart: string): string | undefined {
519475return undefined;
520476}
521477522-/**
523- * Walk `s` respecting `[...]`/`{...}`/`"..."` regions. Calls `onChar`
524- * for every character with `atTop` indicating depth-0 status.
525- * `onChar` returns `"stop"` to short-circuit. On unbalanced
526- * brackets/braces/quotes, calls `onUnbalanced` (which must throw).
527- */
478+// Walk `s` respecting `[...]`/`{...}`/`"..."` regions. Quoted regions
479+// are byte-literal. `onChar` returns "stop" to short-circuit;
480+// `onUnbalanced` (must throw) fires on bracket/brace/quote imbalance.
528481type ScanCallback = (c: string, i: number, atTop: boolean) => "stop" | void;
529482function scanBracketAware(s: string, onChar: ScanCallback, onUnbalanced: () => never): void {
530483let depthBracket = 0;
@@ -533,13 +486,6 @@ function scanBracketAware(s: string, onChar: ScanCallback, onUnbalanced: () => n
533486for (let i = 0; i < s.length; i++) {
534487const c = s[i];
535488if (inQuote) {
536-// `\\` / `\"` consume the next char.
537-if (c === "\\" && i + 1 < s.length) {
538-if (onChar(c, i, false) === "stop") return;
539-if (onChar(s[i + 1], i + 1, false) === "stop") return;
540-i++;
541-continue;
542-}
543489if (c === '"') inQuote = false;
544490if (onChar(c, i, false) === "stop") return;
545491continue;
@@ -610,33 +556,22 @@ export function isQuotedSeg(seg: string): boolean {
610556return seg.length >= 2 && seg.startsWith('"') && seg.endsWith('"');
611557}
612558613-/** Strip surrounding quotes and unescape `\\`/`\"`. No-op on bare segments. */
559+/** Strip surrounding quotes. Content is byte-literal. */
614560export function unquoteSeg(seg: string): string {
615-if (!isQuotedSeg(seg)) return seg;
616-const inner = seg.slice(1, -1);
617-let out = "";
618-for (let i = 0; i < inner.length; i++) {
619-const c = inner[i];
620-if (c === "\\" && i + 1 < inner.length) {
621-const next = inner[i + 1];
622-if (next === "\\" || next === '"') {
623-out += next;
624-i++;
625-continue;
626-}
627-}
628-out += c;
629-}
630-return out;
561+return isQuotedSeg(seg) ? seg.slice(1, -1) : seg;
631562}
632563633-/** Quote a literal for path inclusion if it contains any grammar character. */
564+// Refuses values with `"` or `\` — no escape mechanism.
634565export function quoteSeg(value: string): string {
635566if (value.length === 0) return '""';
636-const needsQuote = /[/.[\]{}?&%"\s]/.test(value);
637-if (!needsQuote) return value;
638-const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
639-return `"${escaped}"`;
567+if (value.includes('"') || value.includes("\\")) {
568+fail(
569+`Cannot quote value containing '"' or '\\\\': ${printable(value)}`,
570+value,
571+"OC_PATH_UNQUOTABLE",
572+);
573+}
574+return /[/.[\]{}?&%\s]/.test(value) ? `"${value}"` : value;
640575}
641576642577// Defense-in-depth — the splitter validates segments it splits; this
@@ -670,8 +605,18 @@ function validateSubSegment(sub: string, input: string): void {
670605"OC_PATH_CONTROL_CHAR",
671606);
672607}
673-// Quoting opts out of identifier-shape rules — content is verbatim.
674-if (isQuotedSeg(sub)) return;
608+// Quoted content is byte-literal but can't contain `"` or `\`.
609+if (isQuotedSeg(sub)) {
610+const inner = sub.slice(1, -1);
611+if (inner.includes('"') || inner.includes("\\")) {
612+fail(
613+`Quoted segment cannot contain '"' or '\\\\': ${printable(sub)}`,
614+input,
615+"OC_PATH_UNQUOTABLE",
616+);
617+}
618+return;
619+}
675620676621// Reserved characters used by the path grammar itself (`?`/`&`/`%`).
677622// Allowed inside predicate / union segments — those are content.
@@ -711,7 +656,7 @@ function validateSubSegment(sub: string, input: string): void {
711656"OC_PATH_MALFORMED_PREDICATE",
712657);
713658}
714-const hasOp = ["!=", "*=", "^=", "$=", "<=", ">=", "<", ">", "="].some((op) =>
659+const hasOp = ["!=", "<=", ">=", "<", ">", "="].some((op) =>
715660inner.includes(op),
716661);
717662if (hasOp) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。