-
Notifications
You must be signed in to change notification settings - Fork 675
fix(dotenv): handle values with both quote types and escape sequences #7139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
| export function stringify(object: Record<string, string>): string { | ||
| const lines: string[] = []; | ||
| for (const [key, value] of Object.entries(object)) { | ||
| let quote; | ||
| let quote: string | undefined; | ||
|
|
||
| let escapedValue = value ?? ""; | ||
| if (key.startsWith("#")) { | ||
|
|
@@ -28,11 +28,21 @@ export function stringify(object: Record<string, string>): string { | |
| `key starts with a '#' indicates a comment and is ignored: '${key}'`, | ||
| ); | ||
| continue; | ||
| } else if (escapedValue.includes("\n") || escapedValue.includes("'")) { | ||
| // escape inner new lines | ||
| escapedValue = escapedValue.replaceAll("\n", "\\n"); | ||
| } | ||
|
|
||
| const hasNewline = escapedValue.includes("\n"); | ||
| const hasSingleQuote = escapedValue.includes("'"); | ||
| const hasDoubleQuote = escapedValue.includes('"'); | ||
|
|
||
| // Use double quotes when the value contains newlines (so they can be | ||
| // expanded back) or single quotes (which are safe inside double quotes). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is what causes your For a value containing If you want the double-quoted form (matching the test), the condition needs to also trigger when the value contains |
||
| if (hasNewline || hasSingleQuote) { | ||
| quote = `"`; | ||
| } else if (escapedValue.match(/\W/)) { | ||
| // Escape backslashes first so that existing backslashes are not | ||
| // confused with escape sequences when parsed. | ||
| escapedValue = escapedValue.replaceAll("\\", "\\\\"); | ||
| if (hasNewline) escapedValue = escapedValue.replaceAll("\n", "\\n"); | ||
| } else if (hasDoubleQuote || escapedValue.match(/\W/)) { | ||
| quote = "'"; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,4 +83,24 @@ Deno.test("stringify()", async (t) => { | |
| stringify({ PARSE: "par'se" }), | ||
| `PARSE="par'se"`, | ||
| )); | ||
| await t.step("handles double-quote characters", () => | ||
| assertEquals( | ||
| stringify({ JSON: '{"key":"value"}' }), | ||
| `JSON='{"key":"value"}'`, | ||
| )); | ||
| await t.step("handles both quote characters", () => | ||
| assertEquals( | ||
| stringify({ MIXED: `a'b"c` }), | ||
| `MIXED="a'b\\"c"`, | ||
| )); | ||
| await t.step("handles backslash with double quotes", () => | ||
| assertEquals( | ||
| stringify({ BS: String.raw`test\"value` }), | ||
| `BS="test\\\\\\"value"`, | ||
| )); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failing test — see the comment on |
||
| await t.step("handles newline with single quotes", () => | ||
| assertEquals( | ||
| stringify({ NL: "hello\nit's me" }), | ||
| `NL="hello\\nit's me"`, | ||
| )); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regression here. Switching the inner pattern from non-greedy
(.|\r\n|\n)*?to greedy(?:[^"\\]|\\.)*breaksPRIVATE_KEY_DOUBLE_QUOTEDintestdata/.env.test.In JS regex,
[^"\\]is a character class — it matches\nby default. So the greedy*happily consumes the trailing newline inside the quotes, leaving nothing for the outer\r?\n?to strip. Parsed value ends with an extra\n.Two possible fixes:
(?:[^"\\\r\n]|\\.|\r\n|\n)*(and keep it non-greedy, or restructure so the outer\r?\n?still wins).(?:[^"\\]|\\.)*?(non-greedy) so the outer\r?\n?can still match the trailing newline.Also a smaller edge case:
\\.won't cross an actual\n(the.doesn't match newlines without thesflag), so a literal backslash immediately before a real newline inside a double-quoted value will fail to parse. Unlikely in practice but worth a test.