Skip to content

Commit 6f26d9b

Browse files
committed
fix(url): change url parsing behavior to actually respect RFC 6901
1 parent 6d4ebf6 commit 6f26d9b

File tree

16 files changed

+43
-46
lines changed

16 files changed

+43
-46
lines changed

.claude/settings.local.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
{
2-
"enableAllProjectMcpServers": false,
32
"permissions": {
4-
"allow": [
5-
"Bash(npx vitest:*)"
6-
]
7-
}
3+
"allow": ["Bash(npx vitest:*)", "Bash(bash:*)", "Bash(node test-debug.js:*)"]
4+
},
5+
"enableAllProjectMcpServers": false
86
}

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,15 @@ format. Maybe some of the files contain cross-references to each other.
2929
{
3030
"definitions": {
3131
"person": {
32-
// references an external file
3332
"$ref": "schemas/people/Bruce-Wayne.json"
3433
},
3534
"place": {
36-
// references a sub-schema in an external file
3735
"$ref": "schemas/places.yaml#/definitions/Gotham-City"
3836
},
3937
"thing": {
40-
// references a URL
4138
"$ref": "http://wayne-enterprises.com/things/batmobile"
4239
},
4340
"color": {
44-
// references a value in an external file via an internal reference
4541
"$ref": "#/definitions/thing/properties/colors/black-as-the-night"
4642
}
4743
}

lib/pointer.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ const tildes = /~/g;
1313
const escapedSlash = /~1/g;
1414
const escapedTilde = /~0/g;
1515

16-
const safeDecodeURIComponent = (encodedURIComponent: string): string => {
17-
try {
18-
return decodeURIComponent(encodedURIComponent);
19-
} catch {
20-
return encodedURIComponent;
21-
}
22-
};
23-
2416
/**
2517
* This class represents a single JSON pointer and its resolved value.
2618
*
@@ -124,7 +116,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
124116
// actually instead pointing to an existing `null` value then we should use that
125117
// `null` value.
126118
if (token in this.value && this.value[token] === null) {
127-
// We use a `null` symbol for internal tracking to differntiate between a general `null`
119+
// We use a `null` symbol for internal tracking to differentiate between a general `null`
128120
// value and our expected `null` value.
129121
this.value = nullSymbol;
130122
continue;
@@ -226,7 +218,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
226218

227219
// Decode each part, according to RFC 6901
228220
for (let i = 0; i < split.length; i++) {
229-
split[i] = safeDecodeURIComponent(split[i].replace(escapedSlash, "/").replace(escapedTilde, "~"));
221+
split[i] = split[i].replace(escapedSlash, "/").replace(escapedTilde, "~");
230222
}
231223

232224
if (split[0] !== "") {
@@ -254,7 +246,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
254246
for (let i = 0; i < tokens.length; i++) {
255247
const token = tokens[i];
256248
// Encode the token, according to RFC 6901
257-
base += "/" + encodeURIComponent(token.replace(tildes, "~0").replace(slashes, "~1"));
249+
// RFC 6901 only requires encoding ~ as ~0 and / as ~1
250+
// We do NOT use encodeURIComponent as it encodes characters like $ which should remain literal
251+
base += "/" + token.replace(tildes, "~0").replace(slashes, "~1");
258252
}
259253

260254
return base;

lib/ref.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
307307
typeof resolvedValue[key] === "object" &&
308308
resolvedValue[key] !== null
309309
) {
310-
merged[key] = deepMerge<typeof merged[keyof S]>(resolvedValue[key], merged[key]);
310+
merged[key] = deepMerge<(typeof merged)[keyof S]>(resolvedValue[key], merged[key]);
311311
}
312312
}
313313
}

lib/util/url.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ export function resolve(from: string, to: string) {
3333
if (resolvedUrl.hostname === "aaa.nonexistanturl.com") {
3434
// `from` is a relative URL.
3535
const { pathname, search, hash } = resolvedUrl;
36-
return pathname + search + hash + endSpaces;
36+
return pathname + search + decodeURIComponent(hash) + endSpaces;
3737
}
38-
return resolvedUrl.toString() + endSpaces;
38+
const resolved = resolvedUrl.toString() + endSpaces;
39+
// if there is a #, we want to split on the first one only, and decode the part after
40+
if (resolved.indexOf("#") >= 0) {
41+
const [base, hash] = resolved.split(/#(.+)/);
42+
return base + "#" + decodeURIComponent(hash);
43+
}
44+
return resolved;
3945
}
4046

4147
/**
@@ -467,16 +473,16 @@ export function toFileSystemPath(path: string | undefined, keepFileProtocol?: bo
467473
* @param pointer
468474
* @returns
469475
*/
470-
export function safePointerToPath(pointer: any) {
476+
export function safePointerToPath(pointer: string) {
471477
if (pointer.length <= 1 || pointer[0] !== "#" || pointer[1] !== "/") {
472478
return [];
473479
}
474480

475481
return pointer
476482
.slice(2)
477483
.split("/")
478-
.map((value: any) => {
479-
return decodeURIComponent(value).replace(jsonPointerSlash, "/").replace(jsonPointerTilde, "~");
484+
.map((value: string) => {
485+
return value.replace(jsonPointerSlash, "/").replace(jsonPointerTilde, "~");
480486
});
481487
}
482488

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"typecheck": "tsc --noEmit",
1313
"prettier": "prettier --write \"**/*.+(js|jsx|ts|tsx|har||json|css|md)\"",
1414
"test": "vitest --coverage",
15-
"test:specific": "vitest defs-encoding",
15+
"test:specific": "vitest invalid",
1616
"test:node": "yarn test",
1717
"test:browser": "cross-env BROWSER=\"true\" yarn test",
1818
"test:update": "vitest -u",

test/specs/absolute-root/bundled.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,29 @@ export default {
2222
minLength: 1,
2323
},
2424
string: {
25-
$ref: "#/definitions/required%20string/type",
25+
$ref: "#/definitions/required string/type",
2626
},
2727
name: {
2828
title: "name",
2929
type: "object",
3030
required: ["first", "last"],
3131
properties: {
3232
first: {
33-
$ref: "#/definitions/required%20string",
33+
$ref: "#/definitions/required string",
3434
},
3535
last: {
36-
$ref: "#/definitions/required%20string",
36+
$ref: "#/definitions/required string",
3737
},
3838
middle: {
3939
type: {
40-
$ref: "#/definitions/required%20string/type",
40+
$ref: "#/definitions/required string/type",
4141
},
4242
minLength: {
43-
$ref: "#/definitions/required%20string/minLength",
43+
$ref: "#/definitions/required string/minLength",
4444
},
4545
},
4646
prefix: {
47-
$ref: "#/definitions/required%20string",
47+
$ref: "#/definitions/required string",
4848
minLength: 3,
4949
},
5050
suffix: {

test/specs/absolute-root/definitions/definitions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"$ref": "required-string.yaml"
44
},
55
"string": {
6-
"$ref": "#/required%20string/type"
6+
"$ref": "#/required string/type"
77
},
88
"name": {
99
"$ref": "../definitions/name.yaml"

test/specs/absolute-root/parsed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default {
2626
$ref: "required-string.yaml",
2727
},
2828
string: {
29-
$ref: "#/required%20string/type",
29+
$ref: "#/required string/type",
3030
},
3131
name: {
3232
$ref: "../definitions/name.yaml",

test/specs/bundle-defs-encoding/bundle-defs-encoding.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ describe("Bundle $defs encoding", () => {
99
const schema = path.rel("test/specs/bundle-defs-encoding/parent.json");
1010
const bundled = await parser.bundle(schema);
1111

12+
// Debug: log the bundled schema
13+
console.log('Bundled schema:', JSON.stringify(bundled, null, 2));
14+
1215
// The bundled schema should have $defs section with myschema that references otherschema
1316
expect(bundled).toHaveProperty("$defs");
1417
expect(bundled.$defs).toHaveProperty("myschema");

0 commit comments

Comments
 (0)