Skip to content

Commit 5645d84

Browse files
committed
feat: change escapePointer-unescapePointer logic
1 parent 5c404d7 commit 5645d84

6 files changed

Lines changed: 53 additions & 31 deletions

File tree

packages/core/src/__tests__/ref-utils.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import outdent from 'outdent';
22
import { parseYamlToDocument } from '../../__tests__/utils.js';
3-
import { escapePointer, parseRef, refBaseName, unescapePointerFromURI } from '../ref-utils.js';
3+
import { escapePointer, parseRef, refBaseName, unescapePointer } from '../ref-utils.js';
44
import { lintDocument } from '../lint.js';
55
import { createConfig } from '../config/index.js';
66
import { BaseResolver } from '../resolve.js';
@@ -35,7 +35,7 @@ describe('ref-utils', () => {
3535
});
3636

3737
it(`should unescape complex urlencoded paths`, () => {
38-
const referene = 'somefile.yaml#/components/schemas/scope%2Fcomplex~name';
38+
const referene = 'somefile.yaml#/components/schemas/scope~1complex~0name';
3939
expect(parseRef(referene)).toMatchInlineSnapshot(`
4040
{
4141
"pointer": [
@@ -155,21 +155,17 @@ describe('ref-utils', () => {
155155

156156
it('should not URI-encode special characters when escaping pointers', () => {
157157
// escapePointer should only handle JSON Pointer escaping, not URI encoding
158-
expect(escapePointer('activity_level_%')).toStrictEqual('activity_level_%');
158+
expect(escapePointer('activity_level_%')).toStrictEqual('activity_level_%25');
159159
});
160160
});
161161

162-
describe('unescapePointerFromURI', () => {
162+
describe('unescapePointer', () => {
163163
it('should unescape a pointer with a percent sign correctly', () => {
164-
expect(unescapePointerFromURI('activity_level_%25')).toStrictEqual('activity_level_%');
165-
});
166-
167-
it('should unescape a pointer with a number correctly', () => {
168-
expect(unescapePointerFromURI('123')).toStrictEqual('123');
164+
expect(unescapePointer('activity_level_%25')).toStrictEqual('activity_level_%');
169165
});
170166

171167
it('should unescape a pointer correctly', () => {
172-
expect(unescapePointerFromURI('scope~1complex~0name')).toStrictEqual('scope/complex~name');
168+
expect(unescapePointer('scope~1complex~0name')).toStrictEqual('scope/complex~name');
173169
});
174170
});
175171
});

packages/core/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export { YamlParseError } from './errors/yaml-parse-error.js';
5353
export { parseYaml, stringifyYaml } from './js-yaml/index.js';
5454
export {
5555
unescapePointer,
56-
unescapePointerFromURI,
5756
isRef,
5857
isAbsoluteUrl,
5958
escapePointer,

packages/core/src/ref-utils.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,35 @@ export class Location {
3636
}
3737

3838
get absolutePointer() {
39-
return this.source.absoluteRef + (this.pointer === '#/' ? '' : encodeURI(this.pointer));
39+
return this.source.absoluteRef + (this.pointer === '#/' ? '' : this.pointer);
4040
}
4141
}
4242

4343
export function unescapePointer(fragment: string): string {
44-
return fragment.replace(/~1/g, '/').replace(/~0/g, '~');
45-
}
46-
47-
export function unescapePointerFromURI(fragment: string): string {
48-
return unescapePointer(decodeURIComponent(fragment));
44+
return decodeURIComponent(fragment).replaceAll('~1', '/').replaceAll('~0', '~');
4945
}
5046

5147
export function escapePointer<T extends string | number>(fragment: T): T {
5248
if (typeof fragment === 'number') return fragment;
53-
return fragment.replace(/~/g, '~0').replace(/\//g, '~1') as T;
49+
return encodeURIFragmentIdentifier(fragment.replaceAll('~', '~0').replaceAll('/', '~1')) as T;
50+
}
51+
52+
// RFC 6901: https://datatracker.ietf.org/doc/html/rfc6901#section-6
53+
function encodeURIFragmentIdentifier(fragment: string): string {
54+
return fragment
55+
.replaceAll('%', '%25')
56+
.replaceAll('^', '%5E')
57+
.replaceAll('|', '%7C')
58+
.replaceAll('\\\\', '%5C')
59+
.replaceAll('\\', '%22')
60+
.replaceAll(' ', '%20');
5461
}
5562

5663
export function parseRef(ref: string): { uri: string | null; pointer: string[] } {
5764
const [uri, pointer = ''] = ref.split('#/');
5865
return {
5966
uri: (uri.endsWith('#') ? uri.slice(0, -1) : uri) || null,
60-
pointer: pointer.split('/').map(unescapePointerFromURI).filter(isTruthy),
67+
pointer: parsePointer(pointer),
6168
};
6269
}
6370

packages/core/src/rules/oas3/__tests__/spec-components-invalid-map-name.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('Oas3 spec-components-invalid-map-name', () => {
4343
{
4444
"location": [
4545
{
46-
"pointer": "#/components/parameters/my Param",
46+
"pointer": "#/components/parameters/my%20Param",
4747
"reportOnKey": true,
4848
"source": Source {
4949
"absoluteRef": "",
@@ -79,7 +79,7 @@ describe('Oas3 spec-components-invalid-map-name', () => {
7979
{
8080
"location": [
8181
{
82-
"pointer": "#/components/schemas/first schema",
82+
"pointer": "#/components/schemas/first%20schema",
8383
"reportOnKey": true,
8484
"source": Source {
8585
"absoluteRef": "",
@@ -115,7 +115,7 @@ describe('Oas3 spec-components-invalid-map-name', () => {
115115
{
116116
"location": [
117117
{
118-
"pointer": "#/components/responses/400 status",
118+
"pointer": "#/components/responses/400%20status",
119119
"reportOnKey": true,
120120
"source": Source {
121121
"absoluteRef": "",
@@ -151,7 +151,7 @@ describe('Oas3 spec-components-invalid-map-name', () => {
151151
{
152152
"location": [
153153
{
154-
"pointer": "#/components/examples/invalid identifier",
154+
"pointer": "#/components/examples/invalid%20identifier",
155155
"reportOnKey": true,
156156
"source": Source {
157157
"absoluteRef": "",
@@ -251,7 +251,7 @@ describe('Oas3 spec-components-invalid-map-name', () => {
251251
{
252252
"location": [
253253
{
254-
"pointer": "#/components/parameters/my Param",
254+
"pointer": "#/components/parameters/my%20Param",
255255
"reportOnKey": true,
256256
"source": Source {
257257
"absoluteRef": "",

tests/e2e/lint/no-invalid-schema-examples-oas3.1-error/openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ paths:
3939
examples:
4040
- test
4141
- 25
42+
three_%:
43+
type: number
44+
example: wrong
45+
4246
responses:
4347
'200':
4448
description: My 200 response

tests/e2e/lint/no-invalid-schema-examples-oas3.1-error/snapshot.txt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,38 @@ Error was generated by the no-invalid-schema-examples rule.
2020

2121
Example value must conform to the schema: type must be string.
2222

23-
39 | examples:
24-
40 | - test
25-
41 | - 25
26-
| ^^
27-
42 | responses:
28-
43 | '200':
23+
39 | examples:
24+
40 | - test
25+
41 | - 25
26+
| ^^
27+
42 | three_%:
28+
43 | type: number
2929

3030
referenced from openapi.yaml:36:19 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/two
3131

3232
Error was generated by the no-invalid-schema-examples rule.
3333

3434

35+
[3] openapi.yaml:44:28 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/three_%25/example
36+
37+
Example value must conform to the schema: type must be number.
38+
39+
42 | three_%:
40+
43 | type: number
41+
44 | example: wrong
42+
| ^^^^^
43+
45 |
44+
46 | responses:
45+
46+
referenced from openapi.yaml:43:19 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/three_%25
47+
48+
Error was generated by the no-invalid-schema-examples rule.
49+
50+
3551

3652
validating openapi.yaml using lint rules for api 'main'...
3753
openapi.yaml: validated in <test>ms
3854

39-
❌ Validation failed with 2 errors.
55+
❌ Validation failed with 3 errors.
4056
run `redocly lint --generate-ignore-file` to add all problems to the ignore file.
4157

0 commit comments

Comments
 (0)