Skip to content

Commit dde296d

Browse files
committed
Fix for [Issue#2208](#2208)
1 parent 3ecd46e commit dde296d

9 files changed

Lines changed: 207 additions & 1 deletion

File tree

src/main/java/org/kohsuke/github/GitHubRequest.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,15 @@ static URL getApiURL(String apiUrl, String tailApiUrl) {
589589
// backward compatibility
590590
apiUrl = GitHubClient.GITHUB_URL;
591591
}
592-
return new URI(apiUrl + tailApiUrl).toURL();
592+
593+
String fullApiUrl = apiUrl + tailApiUrl;
594+
try {
595+
return new URI(fullApiUrl).toURL();
596+
} catch (URISyntaxException e) {
597+
// Some API URL fields include unescaped square brackets in path segments.
598+
// Keep existing escaping as-is while making the URL URI-safe for connectors.
599+
return new URI(encodeSquareBrackets(fullApiUrl)).toURL();
600+
}
593601
} catch (Exception e) {
594602
// The data going into constructing this URL should be controlled by the GitHub API framework,
595603
// so a malformed URL here is a framework runtime error.
@@ -598,6 +606,35 @@ static URL getApiURL(String apiUrl, String tailApiUrl) {
598606
throw new GHException("Unable to build GitHub API URL", e);
599607
}
600608
}
609+
610+
@Nonnull
611+
private static String encodeSquareBrackets(@Nonnull String url) {
612+
URL parsedUrl;
613+
try {
614+
parsedUrl = new URL(url);
615+
} catch (MalformedURLException e) {
616+
// Preserve the original input when URL parsing fails so existing error behavior is unchanged.
617+
return url;
618+
}
619+
620+
String path = parsedUrl.getPath();
621+
String query = parsedUrl.getQuery();
622+
String ref = parsedUrl.getRef();
623+
624+
StringBuilder encodedUrl = new StringBuilder();
625+
encodedUrl.append(parsedUrl.getProtocol()).append("://").append(parsedUrl.getAuthority());
626+
if (path != null) {
627+
encodedUrl.append(path.replace("[", "%5B").replace("]", "%5D"));
628+
}
629+
if (query != null) {
630+
encodedUrl.append('?').append(query.replace("[", "%5B").replace("]", "%5D"));
631+
}
632+
if (ref != null) {
633+
encodedUrl.append('#').append(ref.replace("[", "%5B").replace("]", "%5D"));
634+
}
635+
636+
return encodedUrl.toString();
637+
}
601638
/**
602639
* Create a new {@link Builder}.
603640
*

src/test/java/org/kohsuke/github/GitHubStaticTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,13 @@ public void testGitHubRequest_getApiURL() {
282282
equalTo("ftp://whoa.github.com/endpoint"));
283283
assertThat(GitHubRequest.getApiURL(null, "ftp://api.test.github.com/endpoint").toString(),
284284
equalTo("ftp://api.test.github.com/endpoint"));
285+
assertThat(
286+
GitHubRequest
287+
.getApiURL(null,
288+
"https://api.github.com/repositories/694641495/contents/Alfred.alfredpreferences/workflows/menubar-search%20[3rd-Party]/menu/Package.swift?ref=073e7b4493d088fdf1995a74cf5da201a5795181")
289+
.toString(),
290+
equalTo(
291+
"https://api.github.com/repositories/694641495/contents/Alfred.alfredpreferences/workflows/menubar-search%20%5B3rd-Party%5D/menu/Package.swift?ref=073e7b4493d088fdf1995a74cf5da201a5795181"));
285292

286293
GHException e;
287294
e = Assert.assertThrows(GHException.class,

src/test/java/org/kohsuke/github/GitHubTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.kohsuke.github.example.dataobject.ReadOnlyObjects;
77

88
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
910
import java.util.*;
1011

1112
import static org.hamcrest.Matchers.*;
@@ -305,6 +306,31 @@ public void searchContent() throws Exception {
305306
assertThat(e.getMessage(), equalTo("qualifier cannot be null or empty"));
306307
}
307308

309+
/**
310+
* Search content where the API item URL contains special characters in path segments.
311+
*
312+
* @throws Exception
313+
* the exception
314+
*/
315+
@Test
316+
public void searchContentSpecialCharactersInUrl() throws Exception {
317+
GHContent content = gitHub.searchContent()
318+
.q("filename:Package.swift repo:chrisgrieser/.config")
319+
.list()
320+
.iterator()
321+
.next();
322+
323+
assertThat(content.getPath(),
324+
equalTo("Alfred.alfredpreferences/workflows/menubar-search [3rd-Party]/menu/Package.swift"));
325+
326+
byte[] data;
327+
try (java.io.InputStream inputStream = content.read()) {
328+
data = inputStream.readAllBytes();
329+
}
330+
331+
assertThat(new String(data, StandardCharsets.UTF_8), equalTo("// content with special path chars\n"));
332+
}
333+
308334
/**
309335
* Search content with forks.
310336
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"login": "bitwiseman",
3+
"id": 1958953,
4+
"node_id": "MDQ6VXNlcjE5NTg5NTM=",
5+
"avatar_url": "https://avatars.githubusercontent.com/u/1958953?v=4",
6+
"gravatar_id": "",
7+
"url": "https://api.github.com/users/bitwiseman",
8+
"html_url": "https://github.com/bitwiseman",
9+
"followers_url": "https://api.github.com/users/bitwiseman/followers",
10+
"following_url": "https://api.github.com/users/bitwiseman/following{/other_user}",
11+
"gists_url": "https://api.github.com/users/bitwiseman/gists{/gist_id}",
12+
"starred_url": "https://api.github.com/users/bitwiseman/starred{/owner}{/repo}",
13+
"subscriptions_url": "https://api.github.com/users/bitwiseman/subscriptions",
14+
"organizations_url": "https://api.github.com/users/bitwiseman/orgs",
15+
"repos_url": "https://api.github.com/users/bitwiseman/repos",
16+
"events_url": "https://api.github.com/users/bitwiseman/events{/privacy}",
17+
"received_events_url": "https://api.github.com/users/bitwiseman/received_events",
18+
"type": "User",
19+
"site_admin": false,
20+
"name": "Liam Newman"
21+
}
22+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"total_count": 1,
3+
"incomplete_results": false,
4+
"items": [
5+
{
6+
"name": "Package.swift",
7+
"path": "Alfred.alfredpreferences/workflows/menubar-search [3rd-Party]/menu/Package.swift",
8+
"sha": "f13c7ef5735f02330063eb233273ccf0bb37f332",
9+
"url": "https://api.github.com/repositories/694641495/contents/Alfred.alfredpreferences/workflows/menubar-search%20[3rd-Party]/menu/Package.swift?ref=073e7b4493d088fdf1995a74cf5da201a5795181",
10+
"git_url": "https://api.github.com/repositories/694641495/git/blobs/f13c7ef5735f02330063eb233273ccf0bb37f332",
11+
"html_url": "https://github.com/chrisgrieser/.config/blob/073e7b4493d088fdf1995a74cf5da201a5795181/Alfred.alfredpreferences/workflows/menubar-search%20%5B3rd-Party%5D/menu/Package.swift",
12+
"repository": {
13+
"id": 694641495,
14+
"name": ".config",
15+
"full_name": "chrisgrieser/.config",
16+
"private": false,
17+
"owner": {
18+
"login": "chrisgrieser",
19+
"id": 1106439,
20+
"type": "User",
21+
"site_admin": false
22+
}
23+
},
24+
"score": 1
25+
}
26+
]
27+
}
28+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "Package.swift",
3+
"path": "Alfred.alfredpreferences/workflows/menubar-search [3rd-Party]/menu/Package.swift",
4+
"sha": "f13c7ef5735f02330063eb233273ccf0bb37f332",
5+
"size": 35,
6+
"url": "https://api.github.com/repositories/694641495/contents/Alfred.alfredpreferences/workflows/menubar-search%20[3rd-Party]/menu/Package.swift?ref=073e7b4493d088fdf1995a74cf5da201a5795181",
7+
"html_url": "https://github.com/chrisgrieser/.config/blob/073e7b4493d088fdf1995a74cf5da201a5795181/Alfred.alfredpreferences/workflows/menubar-search%20%5B3rd-Party%5D/menu/Package.swift",
8+
"git_url": "https://api.github.com/repositories/694641495/git/blobs/f13c7ef5735f02330063eb233273ccf0bb37f332",
9+
"download_url": "https://raw.githubusercontent.com/chrisgrieser/.config/073e7b4493d088fdf1995a74cf5da201a5795181/Alfred.alfredpreferences/workflows/menubar-search%20%5B3rd-Party%5D/menu/Package.swift",
10+
"type": "file",
11+
"content": "Ly8gY29udGVudCB3aXRoIHNwZWNpYWwgcGF0aCBjaGFycwo=",
12+
"encoding": "base64"
13+
}
14+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": "e173c8a1-fa95-4d0c-8b5c-b18f3ecc8bab",
3+
"name": "user",
4+
"request": {
5+
"url": "/user",
6+
"method": "GET",
7+
"headers": {
8+
"Accept": {
9+
"equalTo": "application/vnd.github+json"
10+
}
11+
}
12+
},
13+
"response": {
14+
"status": 200,
15+
"bodyFileName": "1-user.json",
16+
"headers": {
17+
"Content-Type": "application/json; charset=utf-8"
18+
}
19+
},
20+
"uuid": "e173c8a1-fa95-4d0c-8b5c-b18f3ecc8bab",
21+
"persistent": true,
22+
"insertionIndex": 1
23+
}
24+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": "4ecf8d7f-5a76-4f31-9e2c-2f770fa64eb3",
3+
"name": "search_code",
4+
"request": {
5+
"url": "/search/code?q=filename%3APackage.swift+repo%3Achrisgrieser%2F.config",
6+
"method": "GET",
7+
"headers": {
8+
"Accept": {
9+
"equalTo": "application/vnd.github+json"
10+
}
11+
}
12+
},
13+
"response": {
14+
"status": 200,
15+
"bodyFileName": "2-search_code.json",
16+
"headers": {
17+
"Content-Type": "application/json; charset=utf-8"
18+
}
19+
},
20+
"uuid": "4ecf8d7f-5a76-4f31-9e2c-2f770fa64eb3",
21+
"persistent": true,
22+
"insertionIndex": 2
23+
}
24+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": "be779a3f-c503-4d48-9f35-0f8d39bb70a8",
3+
"name": "repositories_694641495_contents_package_swift",
4+
"request": {
5+
"url": "/repositories/694641495/contents/Alfred.alfredpreferences/workflows/menubar-search%20%5B3rd-Party%5D/menu/Package.swift?ref=073e7b4493d088fdf1995a74cf5da201a5795181",
6+
"method": "GET",
7+
"headers": {
8+
"Accept": {
9+
"equalTo": "application/vnd.github+json"
10+
}
11+
}
12+
},
13+
"response": {
14+
"status": 200,
15+
"bodyFileName": "3-repositories_694641495_contents_package.swift.json",
16+
"headers": {
17+
"Content-Type": "application/json; charset=utf-8"
18+
}
19+
},
20+
"uuid": "be779a3f-c503-4d48-9f35-0f8d39bb70a8",
21+
"persistent": true,
22+
"insertionIndex": 3
23+
}
24+

0 commit comments

Comments
 (0)