Sitelet https://github.com/swagger-api/swagger-parser/commit/b2a9955da02e62f4ee790e82d43e26ffcaac8c3b
Skip to content

Commit b2a9955

Browse files
authored
fix: Fix relative references inside external path items (#1948, #2066) (#2393)
* fix: Fix relative references inside external path items (#1948, #2066) * fix root-level invalid base filenames are joined incorrectly
1 parent f23d962 commit b2a9955

26 files changed

Lines changed: 761 additions & 48 deletions

File tree

‎modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java‎

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.swagger.v3.parser.processors;
22

33

4-
import java.net.URI;
54
import java.nio.file.Paths;
65
import java.util.Collection;
76
import java.util.Collections;
@@ -1007,32 +1006,7 @@ private void processRefLink(Link subRef, String externalFile) {
10071006

10081007
// visible for testing
10091008
public static String join(String source, String fragment) {
1010-
try {
1011-
boolean isRelative = false;
1012-
if(source.startsWith("/") || source.startsWith(".")) {
1013-
isRelative = true;
1014-
}
1015-
URI uri = new URI(source);
1016-
1017-
if(!source.endsWith("/") && (fragment.startsWith("./") && "".equals(uri.getPath()))) {
1018-
uri = new URI(source + "/");
1019-
}
1020-
else if("".equals(uri.getPath()) && !fragment.startsWith("/")) {
1021-
uri = new URI(source + "/");
1022-
}
1023-
URI f = new URI(fragment);
1024-
1025-
URI resolved = uri.resolve(f);
1026-
1027-
URI normalized = resolved.normalize();
1028-
if(Character.isAlphabetic(normalized.toString().charAt(0)) && isRelative) {
1029-
return "./" + normalized.toString();
1030-
}
1031-
return normalized.toString();
1032-
}
1033-
catch(Exception e) {
1034-
return source;
1035-
}
1009+
return ReferencePathUtils.resolve(source, fragment);
10361010
}
10371011

10381012

‎modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/PathsProcessor.java‎

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ protected void updateRefs(ApiResponse response, String pathRef) {
191191

192192
protected void updateRefs(Example example, String pathRef) {
193193
if(example.get$ref() != null) {
194-
example.set$ref(computeRef(example.get$ref(), pathRef));
194+
example.set$ref(computePreprocessedRef(example.get$ref(), pathRef));
195195
}
196196
}
197197

@@ -239,7 +239,7 @@ protected void updateRefs(RequestBody body, String pathRef) {
239239

240240
protected void updateRefs(Schema model, String pathRef) {
241241
if(model.get$ref() != null) {
242-
model.set$ref(computeRef(model.get$ref(), pathRef));
242+
model.set$ref(computePreprocessedRef(model.get$ref(), pathRef));
243243
}
244244
else if(model.getProperties() != null) {
245245
// process properties
@@ -297,42 +297,37 @@ else if(model instanceof ArraySchema) {
297297

298298

299299
protected boolean isLocalRef(String ref) {
300-
if(ref.startsWith("#")) {
301-
return true;
302-
}
303-
return false;
300+
return ref.startsWith("#");
304301
}
305302

306303
protected boolean isAbsoluteRef(String ref) {
307-
if(!ref.startsWith(".")) {
308-
return true;
309-
}
310-
return false;
304+
return ReferencePathUtils.isAbsolute(ref);
311305
}
312306

313307
private boolean isInternalSchemaRef(String $ref) {
314-
if($ref.startsWith("#/components/schemas")) {
315-
return true;
316-
}
317-
return false;
308+
return $ref.startsWith("#/components/schemas");
318309
}
319310

320311
protected String computeRef(String ref, String prefix) {
321312
if(isLocalRef(ref)&& !isInternalSchemaRef(ref)) return computeLocalRef(ref, prefix);
313+
if (ref.isEmpty()) return ref;
322314
if(isAbsoluteRef(ref)) return ref;
323315
if(isInternalSchemaRef(ref)) return ref;
324316
return computeRelativeRef(ref, prefix);
325317
}
326318

327319
protected String computeRelativeRef(String ref, String prefix) {
328-
if(ref.startsWith("./")) {
320+
return ReferencePathUtils.resolve(prefix, ref);
321+
}
322+
323+
private String computePreprocessedRef(String ref, String prefix) {
324+
if (isLocalRef(ref) && !isInternalSchemaRef(ref)) {
325+
return computeLocalRef(ref, prefix);
326+
}
327+
if (!ref.startsWith(".") || ref.startsWith("./") || isInternalSchemaRef(ref)) {
329328
return ref;
330329
}
331-
int iIdxOfSlash = prefix.lastIndexOf('/');
332-
if(iIdxOfSlash != -1) {
333-
return prefix.substring(0, iIdxOfSlash+1) + ref;
334-
}
335-
return prefix + ref;
330+
return ReferencePathUtils.resolve(prefix, ref);
336331
}
337332

338333
protected String computeLocalRef(String ref, String prefix) {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package io.swagger.v3.parser.processors;
2+
3+
import java.net.URI;
4+
import java.net.URISyntaxException;
5+
import java.util.ArrayDeque;
6+
import java.util.Deque;
7+
8+
final class ReferencePathUtils {
9+
10+
private ReferencePathUtils() {
11+
}
12+
13+
static boolean isAbsolute(String ref) {
14+
if (ref == null) {
15+
return false;
16+
}
17+
if (isAbsoluteFilePath(ref)) {
18+
return true;
19+
}
20+
try {
21+
return new URI(ref).isAbsolute();
22+
} catch (URISyntaxException e) {
23+
return hasScheme(ref);
24+
}
25+
}
26+
27+
static String resolve(String source, String ref) {
28+
if (source == null || ref == null) {
29+
return source;
30+
}
31+
try {
32+
URI sourceUri = new URI(source);
33+
if (!source.endsWith("/") && ref.startsWith("./") && "".equals(sourceUri.getPath())) {
34+
sourceUri = new URI(source + "/");
35+
} else if ("".equals(sourceUri.getPath()) && !ref.startsWith("/")) {
36+
sourceUri = new URI(source + "/");
37+
}
38+
39+
URI resolved = sourceUri.resolve(new URI(ref)).normalize();
40+
String resolvedRef = resolved.toString();
41+
if (source.startsWith("./") && !resolved.isAbsolute() &&
42+
!resolvedRef.startsWith(".") && !resolvedRef.startsWith("/")) {
43+
return "./" + resolvedRef;
44+
}
45+
return resolvedRef;
46+
} catch (URISyntaxException | IllegalArgumentException e) {
47+
return resolveRawPath(source, ref);
48+
}
49+
}
50+
51+
private static String resolveRawPath(String source, String ref) {
52+
if (ref.isEmpty()) {
53+
return stripFragment(source);
54+
}
55+
if (isAbsolute(ref)) {
56+
return ref;
57+
}
58+
if (ref.startsWith("#")) {
59+
return stripFragment(source) + ref;
60+
}
61+
if (ref.startsWith("?")) {
62+
return stripQueryAndFragment(source) + ref;
63+
}
64+
65+
String sourceFile = stripQueryAndFragment(source);
66+
int lastSeparator = Math.max(sourceFile.lastIndexOf('/'), sourceFile.lastIndexOf('\\'));
67+
if (lastSeparator == -1) {
68+
return normalizeRelativePath(ref);
69+
}
70+
71+
String resolved = sourceFile.substring(0, lastSeparator + 1) + ref;
72+
if (hasScheme(sourceFile)) {
73+
return resolved;
74+
}
75+
return normalizeRelativePath(resolved);
76+
}
77+
78+
private static String normalizeRelativePath(String ref) {
79+
int suffixStart = suffixStart(ref);
80+
String path = suffixStart == -1 ? ref : ref.substring(0, suffixStart);
81+
String suffix = suffixStart == -1 ? "" : ref.substring(suffixStart);
82+
boolean leadingDotSlash = path.startsWith("./");
83+
boolean leadingSlash = path.startsWith("/");
84+
Deque<String> segments = new ArrayDeque<>();
85+
86+
for (String segment : path.split("/")) {
87+
if (segment.isEmpty() || ".".equals(segment)) {
88+
continue;
89+
}
90+
if ("..".equals(segment)) {
91+
if (!segments.isEmpty() && !"..".equals(segments.peekLast())) {
92+
segments.removeLast();
93+
} else if (!leadingSlash) {
94+
segments.addLast(segment);
95+
}
96+
} else {
97+
segments.addLast(segment);
98+
}
99+
}
100+
101+
String normalized = String.join("/", segments);
102+
if (leadingSlash) {
103+
normalized = "/" + normalized;
104+
} else if (leadingDotSlash && !normalized.startsWith("..") && !normalized.isEmpty()) {
105+
normalized = "./" + normalized;
106+
}
107+
return normalized + suffix;
108+
}
109+
110+
private static boolean isAbsoluteFilePath(String ref) {
111+
return ref.startsWith("/") || ref.startsWith("\\") ||
112+
(ref.length() >= 3 && Character.isLetter(ref.charAt(0)) && ref.charAt(1) == ':' &&
113+
(ref.charAt(2) == '/' || ref.charAt(2) == '\\'));
114+
}
115+
116+
private static boolean hasScheme(String ref) {
117+
int colon = ref.indexOf(':');
118+
if (colon <= 0 || !Character.isLetter(ref.charAt(0))) {
119+
return false;
120+
}
121+
for (int i = 1; i < colon; i++) {
122+
char character = ref.charAt(i);
123+
if (!Character.isLetterOrDigit(character) && character != '+' && character != '-' && character != '.') {
124+
return false;
125+
}
126+
}
127+
return true;
128+
}
129+
130+
private static String stripFragment(String value) {
131+
int fragment = value.indexOf('#');
132+
return fragment == -1 ? value : value.substring(0, fragment);
133+
}
134+
135+
private static String stripQueryAndFragment(String value) {
136+
int suffixStart = suffixStart(value);
137+
return suffixStart == -1 ? value : value.substring(0, suffixStart);
138+
}
139+
140+
private static int suffixStart(String value) {
141+
int query = value.indexOf('?');
142+
int fragment = value.indexOf('#');
143+
if (query == -1) {
144+
return fragment;
145+
}
146+
if (fragment == -1) {
147+
return query;
148+
}
149+
return Math.min(query, fragment);
150+
}
151+
}

0 commit comments

Comments
 (0)