From 60dd30ab2d1a688046ed801fa2fc34ed41786042 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:42:01 +0000 Subject: [PATCH 1/3] dotenv: stop byte-trimming 0xA0 from UTF-8 .env values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parser's WHITESPACE_CHARS set included raw 0xA0 and was applied byte-at-a-time to UTF-8 input. 0xA0 is a continuation byte in UTF-8 (NBSP is C2 A0), so trimming it severed multi-byte sequences at value boundaries: A=xठ (U+0920 = E0 A4 A0) lost its final byte and became invalid UTF-8, and NBSP-padded values were left with a bare C2 lead byte. Node.js does not trim NBSP from unquoted values, so drop 0xA0 from the set entirely. --- src/dotenv/env_loader.rs | 4 +++- test/cli/run/env.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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..e9770c62070a 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -1,5 +1,6 @@ import { beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; +import { parseEnv } from "node:util"; import { bunEnv, bunExe, @@ -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`', From 7f5a0e7920eb5f538516bb075d3b6d9f3fff17d2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:44:14 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- test/cli/run/env.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index e9770c62070a..ac7362ab30f2 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -1,6 +1,5 @@ import { beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { parseEnv } from "node:util"; import { bunEnv, bunExe, @@ -13,6 +12,7 @@ import { isWindows, tempDirWithFiles, } from "harness"; +import { parseEnv } from "node:util"; import path from "path"; function bunRunWithoutTrim(file: string, env?: Record) { From a631cb7b9985b1f4246e015daf1642a4054514c0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 12 Jul 2026 05:05:45 +0000 Subject: [PATCH 3/3] ci: retrigger