diff --git a/src/dotenv/env_loader.rs b/src/dotenv/env_loader.rs index 50a321b0d8f7..931b5699dc65 100644 --- a/src/dotenv/env_loader.rs +++ b/src/dotenv/env_loader.rs @@ -994,7 +994,9 @@ struct Parser<'a> { value_buffer: &'a mut Vec, } -const WHITESPACE_CHARS: &[u8] = b"\t\x0B\x0C \xA0\n\r"; +// Input is UTF-8, so this set must be ASCII-only: 0xA0 is a continuation byte +// there (NBSP is C2 A0), and trimming it byte-wise corrupts multi-byte sequences. +const WHITESPACE_CHARS: &[u8] = b"\t\x0B\x0C \n\r"; impl<'a> Parser<'a> { fn skip_line(&mut self) { diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 23038a50c7ba..ac7362ab30f2 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -12,6 +12,7 @@ import { isWindows, tempDirWithFiles, } from "harness"; +import { parseEnv } from "node:util"; import path from "path"; function bunRunWithoutTrim(file: string, env?: Record) { @@ -372,6 +373,24 @@ test(".env space edgecase (issue #411)", () => { expect(stdout).toBe("[A B]"); }); +test(".env does not byte-trim 0xA0 out of UTF-8 values", () => { + // U+0920 DEVANAGARI LETTER TTHA encodes as E0 A4 A0 (trailing 0xA0) + // U+00A0 NO-BREAK SPACE encodes as C2 A0; Node.js preserves it verbatim. + expect(parseEnv("A=x\u0920\nB=\u00A0x\u00A0\nC=\u00A0\nD= x \n")).toEqual({ + A: "x\u0920", + B: "\u00A0x\u00A0", + C: "\u00A0", + D: "x", + }); + + const dir = tempDirWithFiles("dotenv-utf8-nbsp", { + ".env": "A=x\u0920\nB=\u00A0x\u00A0\n", + "index.ts": "console.log(JSON.stringify({ A: process.env.A, B: process.env.B }));", + }); + const { stdout } = bunRun(`${dir}/index.ts`); + expect(JSON.parse(stdout)).toEqual({ A: "x\u0920", B: "\u00A0x\u00A0" }); +}); + test(".env special characters 1 (issue #2823)", () => { const dir = tempDirWithFiles("dotenv-issue-2823", { ".env": 'A="a$t"\nC=`c\\$v`',