diff --git a/src/lib/components/variables/importVariablesModal.svelte b/src/lib/components/variables/importVariablesModal.svelte index 9eed108b23..2aa3d27490 100644 --- a/src/lib/components/variables/importVariablesModal.svelte +++ b/src/lib/components/variables/importVariablesModal.svelte @@ -5,7 +5,7 @@ import type { Models } from '@appwrite.io/console'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { Icon, Layout, Selector, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte'; - import { parse } from '$lib/helpers/envfile'; + import { parse, readEnvFile } from '$lib/helpers/envfile'; import { removeFile } from '$lib/helpers/files'; import { validateVariables } from '$lib/helpers/variables'; @@ -31,7 +31,7 @@ throw new Error('No file selected'); } - const uploaded = parse(await files[0].text()); + const uploaded = parse(await readEnvFile(files[0])); if (!Object.keys(uploaded).length) { throw new Error('No variables found'); diff --git a/src/lib/helpers/envfile.test.ts b/src/lib/helpers/envfile.test.ts new file mode 100644 index 0000000000..18194cdab8 --- /dev/null +++ b/src/lib/helpers/envfile.test.ts @@ -0,0 +1,54 @@ +import { parse, readEnvFile } from '$lib/helpers/envfile'; +import { expect, test } from 'vitest'; + +function encodeUtf16(text: string, littleEndian: boolean, bom: boolean): Uint8Array { + const codeUnits = bom + ? [0xfeff, ...text.split('').map((c) => c.charCodeAt(0))] + : text.split('').map((c) => c.charCodeAt(0)); + const bytes = new Uint8Array(codeUnits.length * 2); + const view = new DataView(bytes.buffer); + codeUnits.forEach((unit, i) => view.setUint16(i * 2, unit, littleEndian)); + return bytes; +} + +const ENV = 'VITE_GEMINI_API_KEY=secret-value\nOTHER_KEY=other'; +const EXPECTED = { VITE_GEMINI_API_KEY: 'secret-value', OTHER_KEY: 'other' }; + +test('reads UTF-8', async () => { + const file = new Blob([new TextEncoder().encode(ENV)]); + expect(parse(await readEnvFile(file))).toEqual(EXPECTED); +}); + +test('reads UTF-8 with BOM', async () => { + const bytes = new Uint8Array([0xef, 0xbb, 0xbf, ...new TextEncoder().encode(ENV)]); + expect(parse(await readEnvFile(new Blob([bytes])))).toEqual(EXPECTED); +}); + +test('reads UTF-16LE with BOM (PowerShell default)', async () => { + const file = new Blob([encodeUtf16(ENV, true, true)]); + expect(parse(await readEnvFile(file))).toEqual(EXPECTED); +}); + +test('reads UTF-16BE with BOM', async () => { + const file = new Blob([encodeUtf16(ENV, false, true)]); + expect(parse(await readEnvFile(file))).toEqual(EXPECTED); +}); + +test('reads BOM-less UTF-16LE by NUL heuristic', async () => { + const file = new Blob([encodeUtf16(ENV, true, false)]); + const parsed = parse(await readEnvFile(file)); + expect(parsed).toEqual(EXPECTED); + // The regression this guards: keys must not carry interleaved NUL bytes. + expect(Object.keys(parsed).some((key) => key.includes('\u0000'))).toBe(false); +}); + +test('reads BOM-less UTF-16BE by NUL heuristic', async () => { + const file = new Blob([encodeUtf16(ENV, false, false)]); + expect(parse(await readEnvFile(file))).toEqual(EXPECTED); +}); + +test('keeps UTF-8 text containing a stray NUL as UTF-8', async () => { + const text = 'A=1\nB=has\u0000nul'; + const file = new Blob([new TextEncoder().encode(text)]); + expect(await readEnvFile(file)).toBe(text); +}); diff --git a/src/lib/helpers/envfile.ts b/src/lib/helpers/envfile.ts index 637e852737..78abfb1b74 100644 Binary files a/src/lib/helpers/envfile.ts and b/src/lib/helpers/envfile.ts differ diff --git a/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte b/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte index e2d10734c0..a1d491c6b2 100644 --- a/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte @@ -14,7 +14,7 @@ Typography, Upload } from '@appwrite.io/pink-svelte'; - import { parse } from '$lib/helpers/envfile'; + import { parse, readEnvFile } from '$lib/helpers/envfile'; import { removeFile } from '$lib/helpers/files'; import { validateVariables } from '$lib/helpers/variables'; import type { VariablesOperationItem } from './variablesOperation'; @@ -57,7 +57,7 @@ throw new Error('No file selected'); } - const uploaded = parse(await files[0].text()); + const uploaded = parse(await readEnvFile(files[0])); if (!Object.keys(uploaded).length) { throw new Error('No variables found');