diff --git a/plugins/action-copy-curl/src/index.ts b/plugins/action-copy-curl/src/index.ts index 4f02afa7..1efbaf3c 100644 --- a/plugins/action-copy-curl/src/index.ts +++ b/plugins/action-copy-curl/src/index.ts @@ -83,7 +83,9 @@ export async function convertToCurl(request: Partial) { if (p.file) { let v = `${p.name}=@${p.file}`; v += p.contentType ? `;type=${p.contentType}` : ""; - xs.push(flag, v); + // A bare `;` separates commands and a path can hold spaces, so this + // argument needs quoting like every other one. + xs.push(flag, quote(v)); } else { xs.push(flag, quote(`${p.name}=${p.value}`)); } @@ -157,7 +159,10 @@ export async function convertToCurl(request: Partial) { } function quote(arg: string): string { - const escaped = arg.replace(/'/g, "\\'"); + // A single-quoted POSIX string takes no escapes, so `\'` does not close it: + // the string ends one character early and the rest of the command is left + // dangling. Step out of the quotes, emit an escaped quote, step back in. + const escaped = arg.replace(/'/g, `'\\''`); return `'${escaped}'`; } diff --git a/plugins/action-copy-curl/tests/index.test.ts b/plugins/action-copy-curl/tests/index.test.ts index 824cf718..3f2e167a 100644 --- a/plugins/action-copy-curl/tests/index.test.ts +++ b/plugins/action-copy-curl/tests/index.test.ts @@ -120,7 +120,7 @@ describe("exporter-curl", () => { `curl -X PUT 'https://yaak.app'`, `--form 'a=aaa'`, `--form 'b=bbb'`, - "--form f=@/foo/bar.png;type=image/png", + "--form 'f=@/foo/bar.png;type=image/png'", ].join(" \\\n "), ); }); @@ -140,11 +140,48 @@ describe("exporter-curl", () => { [ `curl -X POST 'https://yaak.app'`, `--header 'Content-Type: application/json'`, - `--data '{"foo":"bar\\'s"}'`, + `--data '{"foo":"bar'\\''s"}'`, ].join(" \\\n "), ); }); + test("Quotes an apostrophe so the command still parses", async () => { + // POSIX single quotes take no escapes: `\'` ends the string one + // character early and everything after it is left dangling, so the copied + // command is a syntax error rather than a request. + const command = await convertToCurl({ + url: "https://yaak.app/it's", + method: "POST", + bodyType: "application/json", + body: { text: `{"note":"don't stop"}` }, + headers: [{ name: "X-Note", value: "it's fine" }], + }); + + expect(command).toEqual( + [ + `curl -X POST 'https://yaak.app/it'\\''s'`, + `--header 'X-Note: it'\\''s fine'`, + `--data '{"note":"don'\\''t stop"}'`, + ].join(" \\\n "), + ); + }); + + test("Quotes a file form field so its type suffix survives", async () => { + // A bare `;` separates commands, so an unquoted `f=@x.png;type=image/png` + // reaches curl as `f=@x.png` and the rest runs as its own command. + expect( + await convertToCurl({ + url: "https://yaak.app", + method: "POST", + bodyType: "multipart/form-data", + body: { form: [{ name: "f", file: "/my files/a.png", contentType: "image/png" }] }, + }), + ).toEqual( + [`curl -X POST 'https://yaak.app'`, `--form 'f=@/my files/a.png;type=image/png'`].join( + " \\\n ", + ), + ); + }); test("Exports multi-line JSON body", async () => { expect( await convertToCurl({ diff --git a/plugins/action-copy-grpcurl/src/index.ts b/plugins/action-copy-grpcurl/src/index.ts index 1dd6378e..32232980 100644 --- a/plugins/action-copy-grpcurl/src/index.ts +++ b/plugins/action-copy-grpcurl/src/index.ts @@ -129,7 +129,10 @@ export async function convert(request: Partial, allProtoFiles: stri } function quote(arg: string): string { - const escaped = arg.replace(/'/g, "\\'"); + // A single-quoted POSIX string takes no escapes, so `\'` does not close it: + // the string ends one character early and the rest of the command is left + // dangling. Step out of the quotes, emit an escaped quote, step back in. + const escaped = arg.replace(/'/g, `'\\''`); return `'${escaped}'`; } diff --git a/plugins/action-copy-grpcurl/tests/index.test.ts b/plugins/action-copy-grpcurl/tests/index.test.ts index e4144a18..9cec88cc 100644 --- a/plugins/action-copy-grpcurl/tests/index.test.ts +++ b/plugins/action-copy-grpcurl/tests/index.test.ts @@ -175,4 +175,21 @@ describe("exporter-curl", () => { ].join(" \\\n "), ); }); + + test("Quotes an apostrophe so the command still parses", async () => { + // POSIX single quotes take no escapes: `\'` ends the string one + // character early and leaves the rest of the command dangling. + const command = await convert( + { + url: "https://yaak.app", + service: "Service", + method: "Method", + message: `{"note":"don't stop"}`, + metadata: [{ name: "x-note", value: "it's fine" }], + }, + [], + ); + expect(command).toContain(`'{"note":"don'\\''t stop"}'`); + expect(command).toContain(`'x-note: it'\\''s fine'`); + }); });