Skip to content

Commit 3599a06

Browse files
authored
Merge pull request #145 from vim-denops/fix-bufname
๐Ÿ› Fix "URI malformed" error on `bufname` module when a `expr` contains percent encoded characters
2 parents 27edd51 + 0fe4e4c commit 3599a06

File tree

6 files changed

+123
-31
lines changed

6 files changed

+123
-31
lines changed

โ€Ž.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ jobs:
7171
- vim: "v8.2.3452"
7272
nvim: "v0.6.0"
7373
runs-on: ${{ matrix.runner }}
74+
timeout-minutes: 5
7475
steps:
7576
- run: git config --global core.autocrlf false
7677
if: runner.os == 'Windows'

โ€Ždenops_std/bufname/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ assertEquals(
116116
scheme: "denops",
117117
expr: path.toFileUrl("C:\\Users\John Titor\test.git").pathname,
118118
}),
119-
"denops:///C:/Users/John%20Titor/test.git",
119+
"denops:///C:/Users/John%2520Titor/test.git",
120120
);
121121
```
122122

@@ -183,10 +183,10 @@ import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
183183
import * as path from "https://deno.land/std/path/mod.ts";
184184
import { parse } from "../bufname/mod.ts";
185185

186-
const bufname = parse("denops:///C:/Users/John%20Titor/test.git");
186+
const bufname = parse("denops:///C:/Users/John%2520Titor/test.git");
187187
assertEquals(bufname, {
188188
scheme: "denops",
189-
expr: "/C:/Users/John Titor/test.git",
189+
expr: "/C:/Users/John%20Titor/test.git",
190190
});
191191
// NOTE:
192192
// Works only on Windows (Use path.win32.fromFileUrl instead on other platforms)

โ€Ždenops_std/bufname/bufname.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,9 @@ export function format(
8585
`Scheme '${scheme}' contains unusable characters. Only alphabets are allowed.`,
8686
);
8787
}
88-
const encodedPath = encode(expr).replaceAll(";", "%3B").replaceAll(
89-
"#",
90-
"%23",
91-
);
88+
const encodedPath = encode(expr.replaceAll("%", "%25"))
89+
.replaceAll(";", "%3B")
90+
.replaceAll("#", "%23");
9291
const suffix1 = params
9392
? `;${encode(toURLSearchParams(params).toString())}`
9493
: "";
@@ -123,13 +122,14 @@ export function parse(bufname: string): Bufname {
123122
);
124123
}
125124
const remain = decode(bufname.substring(`${scheme}://`.length), [
125+
"%25", // %
126126
"%3B", // ;
127127
"%23", // #
128128
]);
129129
const m2 = remain.match(exprPattern)!;
130130
const expr = decode(m2[1]);
131131
const params = m2[2]
132-
? fromURLSearchParams(new URLSearchParams(decode(m2[2])))
132+
? fromURLSearchParams(new URLSearchParams(decode(m2[2], ["%25"])))
133133
: undefined;
134134
const fragment = m2[3] ? decode(m2[3]) : undefined;
135135
return {

โ€Ždenops_std/bufname/bufname_test.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,40 @@ Deno.test("format pass example in README.md", () => {
191191
"denops:///Users/John Titor/test.git;foo=foo&bar=bar1&bar=bar2#README.md",
192192
);
193193

194+
const fileUrl = path.win32.toFileUrl("C:\\Users\\John Titor\\test.git");
195+
assertEquals(
196+
fileUrl.pathname,
197+
"/C:/Users/John%20Titor/test.git",
198+
);
194199
assertEquals(
195200
format({
196201
scheme: "denops",
197-
expr: path.win32.toFileUrl("C:\\Users\\John Titor\\test.git").pathname,
202+
expr: fileUrl.pathname,
198203
}),
199-
"denops:///C:/Users/John%20Titor/test.git",
204+
"denops:///C:/Users/John%2520Titor/test.git",
200205
);
201206
});
207+
Deno.test("format encodes '%' in 'expr'", () => {
208+
const src = {
209+
scheme: "denops",
210+
expr: "/hello%world",
211+
};
212+
const dst = format(src);
213+
const exp = "denops:///hello%25world";
214+
assertEquals(dst, exp);
215+
});
216+
Deno.test("format encodes '%' in 'params'", () => {
217+
const src = {
218+
scheme: "denops",
219+
expr: "/absolute/path/to/worktree",
220+
params: {
221+
foo: "%foo",
222+
},
223+
};
224+
const dst = format(src);
225+
const exp = "denops:///absolute/path/to/worktree;foo=%25foo";
226+
assertEquals(dst, exp);
227+
});
202228

203229
Deno.test("parse throws exception when 'expr' contains unusable characters", () => {
204230
const src = "denops:///<>|?*";
@@ -375,16 +401,52 @@ Deno.test("parse pass example in README.md", () => {
375401
},
376402
);
377403

378-
const bufname = parse("denops:///C:/Users/John%20Titor/test.git");
404+
const bufname = parse("denops:///C:/Users/John%2520Titor/test.git");
379405
assertEquals(
380406
bufname,
381407
{
382408
scheme: "denops",
383-
expr: "/C:/Users/John Titor/test.git",
409+
expr: "/C:/Users/John%20Titor/test.git",
384410
},
385411
);
386412
assertEquals(
387413
path.win32.fromFileUrl(`file://${bufname.expr}`),
388414
"C:\\Users\\John Titor\\test.git",
389415
);
390416
});
417+
Deno.test("parse decode percent-encoded characters ('%') in 'expr'", () => {
418+
const src = "denops:///hello%25world";
419+
const dst = parse(src);
420+
const exp = {
421+
scheme: "denops",
422+
expr: "/hello%world",
423+
};
424+
assertEquals(dst, exp);
425+
});
426+
Deno.test("parse decode percent-encoded characters ('%') in 'params'", () => {
427+
const src = "denops:///absolute/path/to/worktree;foo=%25foo";
428+
const dst = parse(src);
429+
const exp = {
430+
scheme: "denops",
431+
expr: "/absolute/path/to/worktree",
432+
params: {
433+
foo: "%foo",
434+
},
435+
};
436+
assertEquals(dst, exp);
437+
});
438+
Deno.test("parse decode bufname that cause 'URI malformed' on denops-std v3.8.1", () => {
439+
const src =
440+
"ginlog:///Users/alisue/ghq/github.com/lambdalisue/gin.vim;pretty=%22%25C%28yellow%29%25h%25C%28reset%29+%25C%28magenta%29%5B%25ad%5D%25C%28reset%29%25C%28auto%29%25d%25C%28reset%29+%25s+%25C%28cyan%29%40%25an%25C%28reset%29%22#[]$";
441+
const dst = parse(src);
442+
const exp = {
443+
scheme: "ginlog",
444+
expr: "/Users/alisue/ghq/github.com/lambdalisue/gin.vim",
445+
params: {
446+
pretty:
447+
'"%C(yellow)%h%C(reset) %C(magenta)[%ad]%C(reset)%C(auto)%d%C(reset) %s %C(cyan)@%an%C(reset)"',
448+
},
449+
fragment: "[]$",
450+
};
451+
assertEquals(dst, exp);
452+
});

โ€Ždenops_std/bufname/utils.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
// Vim on Windows does not support following characters in bufname
22
export const bufnameUnusablePattern = /["<>\|\?\*]/g;
33

4-
// % encoded character
5-
const encodedCharacterPattern = /(?:%[0-9a-fA-F]{2})+/g;
6-
74
/**
8-
* Encode unusable characters to percent-encoded characters.
5+
* Encode only unusable characters to percent-encoded characters.
6+
*
7+
* This function does not encode the percent character itself to avoid
8+
* multiple encoding.
9+
* Users must encode the percent character themselves before using this
10+
* function like:
11+
*
12+
* ```
13+
* const expr = "%Hello world%";
14+
* const encoded = encode(expr.replaceAll("%", "%25"));
15+
* ```
16+
*
17+
* Note that this function is not the inverse of `decode`.
18+
* The `decode` function decodes all percent-encoded characters, while this function
19+
* encodes a few characters.
920
*/
1021
export function encode(expr: string): string {
1122
expr = expr.replaceAll(
@@ -17,17 +28,17 @@ export function encode(expr: string): string {
1728

1829
/**
1930
* Decode all percent-encoded characters.
31+
*
32+
* Note that this function is not the inverse of `encode`.
33+
* The `encode` function only encodes a few characters, while this function
34+
* decodes all percent-encoded characters.
2035
*/
21-
export function decode(expr: string, excludes?: string[]): string {
22-
excludes = (excludes ?? []).map((v) => v.toUpperCase());
23-
expr = expr.replaceAll(
24-
encodedCharacterPattern,
25-
(m) => {
26-
if (excludes?.includes(m.toUpperCase())) {
27-
return m;
28-
}
29-
return decodeURIComponent(m);
30-
},
36+
export function decode(expr: string, excludes: string[] = []): string {
37+
excludes = excludes.map((v) => v.toUpperCase());
38+
const prefix = excludes.length ? `(?!${excludes.join("|")})` : "";
39+
const pattern = new RegExp(
40+
`(?:${prefix}%[0-9a-fA-F]{2})+`,
41+
"g",
3142
);
32-
return expr;
43+
return expr.replaceAll(pattern, decodeURIComponent);
3344
}

โ€Ždenops_std/bufname/utils_test.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ Deno.test("encode does nothing on numeric characters", () => {
1313
const exp = src;
1414
assertEquals(dst, exp);
1515
});
16+
Deno.test("encode does nothing on the percent character", () => {
17+
const src = "%";
18+
const dst = encode(src);
19+
const exp = "%";
20+
assertEquals(dst, exp);
21+
});
1622
Deno.test('encode encodes some symbol characters ("<>|?*)', () => {
1723
const src = " !\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~";
1824
const dst = encode(src);
1925
const exp = " !%22#$%&'()%2A+,-./:;%3C=%3E%3F@[\\]^`{%7C}~";
2026
assertEquals(dst, exp);
2127
});
2228
Deno.test("encode does nothing on ๆ—ฅๆœฌ่ชž", () => {
23-
const src = "ใ„ใ‚ใฏใซใธใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
29+
const src = "ใ„ใ‚ใฏใซใปใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
2430
const dst = encode(src);
2531
const exp = src;
2632
assertEquals(dst, exp);
@@ -44,14 +50,26 @@ Deno.test("decode does nothing on numeric characters", () => {
4450
const exp = src;
4551
assertEquals(dst, exp);
4652
});
53+
Deno.test("decode decodes encoded the percent character", () => {
54+
const src = "%25";
55+
const dst = decode(src);
56+
const exp = "%";
57+
assertEquals(dst, exp);
58+
});
59+
Deno.test("decode decodes encoded the percent character (only once)", () => {
60+
const src = "%2520";
61+
const dst = decode(src);
62+
const exp = "%20";
63+
assertEquals(dst, exp);
64+
});
4765
Deno.test('decode decodes encoded characters ("<>|?*)', () => {
4866
const src = " !%22#$%&'()%2A+,-./:;%3C=%3E%3F@[\\]^`{%7C}~";
4967
const dst = decode(src);
5068
const exp = " !\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~";
5169
assertEquals(dst, exp);
5270
});
5371
Deno.test("decode does nothing on ๆ—ฅๆœฌ่ชž", () => {
54-
const src = "ใ„ใ‚ใฏใซใธใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
72+
const src = "ใ„ใ‚ใฏใซใปใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
5573
const dst = decode(src);
5674
const exp = src;
5775
assertEquals(dst, exp);
@@ -64,10 +82,10 @@ Deno.test("decode does nothing on emoji (๐Ÿฅƒ)", () => {
6482
});
6583
Deno.test("decode decodes encoded ๆ—ฅๆœฌ่ชž", () => {
6684
const src = encodeURIComponent(
67-
"ใ„ใ‚ใฏใซใธใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™",
85+
"ใ„ใ‚ใฏใซใปใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™",
6886
);
6987
const dst = decode(src);
70-
const exp = "ใ„ใ‚ใฏใซใธใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
88+
const exp = "ใ„ใ‚ใฏใซใปใธใจใกใ‚Šใฌใ‚‹ใ‚’ใ‚ใ‹ใ‚ˆใŸใ‚Œใใคใญใชใ‚‰ใ‚€ใ†ใ‚ใฎใŠใใ‚„ใพใ‘ใตใ“ใˆใฆใ‚ใ•ใใ‚†ใ‚ใฟใ—ใ‚‘ใฒใ‚‚ใ›ใ™";
7189
assertEquals(dst, exp);
7290
});
7391
Deno.test("decode decodes encoded emoji (๐Ÿฅƒ)", () => {

0 commit comments

Comments
ย (0)