diff --git a/src/extension/common/variables/environment.ts b/src/extension/common/variables/environment.ts index 0fd45e1b..ce67bb8b 100644 --- a/src/extension/common/variables/environment.ts +++ b/src/extension/common/variables/environment.ts @@ -94,16 +94,80 @@ export function appendPaths( export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVariables): EnvironmentVariables { const globalVars = baseVars ? baseVars : {}; const vars: EnvironmentVariables = {}; - lines - .toString() - .split('\n') - .forEach((line, _idx) => { - const [name, value] = parseEnvLine(line); - if (name === '') { - return; + const content = lines.toString(); + + // State machine to handle multiline quoted values + let currentLine = ''; + let inQuotes = false; + let quoteChar = ''; + let afterEquals = false; + + for (let i = 0; i < content.length; i++) { + const char = content[i]; + + // Track if we've seen an '=' sign (indicating we're in the value part) + if (char === '=' && !inQuotes) { + afterEquals = true; + currentLine += char; + continue; + } + + // Handle quote characters - need to check for proper escaping + if ((char === '"' || char === "'") && afterEquals) { + // Count consecutive backslashes before this quote + let numBackslashes = 0; + let j = i - 1; + while (j >= 0 && content[j] === '\\') { + numBackslashes++; + j--; + } + + // Quote is escaped if there's an odd number of backslashes before it + const isEscaped = numBackslashes % 2 === 1; + + if (!isEscaped) { + if (!inQuotes) { + // Starting a quoted section + inQuotes = true; + quoteChar = char; + } else if (char === quoteChar) { + // Ending a quoted section + inQuotes = false; + quoteChar = ''; + } + } + currentLine += char; + continue; + } + + // Handle newlines + if (char === '\n') { + if (inQuotes) { + // We're inside quotes, preserve the newline + currentLine += char; + } else { + // We're not in quotes, this is the end of a line + const [name, value] = parseEnvLine(currentLine); + if (name !== '') { + vars[name] = substituteEnvVars(value, vars, globalVars); + } + // Reset for next line + currentLine = ''; + afterEquals = false; } + } else { + currentLine += char; + } + } + + // Handle the last line if there's no trailing newline + if (currentLine.trim() !== '') { + const [name, value] = parseEnvLine(currentLine); + if (name !== '') { vars[name] = substituteEnvVars(value, vars, globalVars); - }); + } + } + return vars; } @@ -112,7 +176,8 @@ function parseEnvLine(line: string): [string, string] { // https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32 // We don't use dotenv here because it loses ordering, which is // significant for substitution. - const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); + // Modified to handle multiline values by using 's' flag so $ matches before newlines in multiline strings + const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/s); if (!match) { return ['', '']; } diff --git a/src/test/unittest/common/environment.unit.test.ts b/src/test/unittest/common/environment.unit.test.ts new file mode 100644 index 00000000..bf1e8cf7 --- /dev/null +++ b/src/test/unittest/common/environment.unit.test.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { parseEnvFile } from '../../../extension/common/variables/environment'; + +suite('Environment File Parsing Tests', () => { + test('Should parse simple environment variables', () => { + const content = 'VAR1=value1\nVAR2=value2'; + const result = parseEnvFile(content); + + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(result).to.deep.equal({ + VAR1: 'value1', + VAR2: 'value2', + }); + }); + + test('Should parse single-quoted multiline values', () => { + const content = "EXAMPLE_VAR='very long value\nwith new line , we need to get all the lines'"; + const result = parseEnvFile(content); + + expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines'); + }); + + test('Should parse double-quoted multiline values', () => { + const content = 'EXAMPLE_VAR="very long value\nwith new line , we need to get all the lines"'; + const result = parseEnvFile(content); + + expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines'); + }); + + test('Should parse escaped newlines in single-quoted values', () => { + const content = "VAR='line1\\nline2'"; + const result = parseEnvFile(content); + + expect(result.VAR).to.equal('line1\nline2'); + }); + + test('Should parse escaped newlines in double-quoted values', () => { + const content = 'VAR="line1\\nline2"'; + const result = parseEnvFile(content); + + expect(result.VAR).to.equal('line1\nline2'); + }); + + test('Should handle multiple variables with multiline values', () => { + const content = "VAR1='multiline\nvalue1'\nVAR2='multiline\nvalue2'"; + const result = parseEnvFile(content); + + expect(result.VAR1).to.equal('multiline\nvalue1'); + expect(result.VAR2).to.equal('multiline\nvalue2'); + }); + + test('Should handle unquoted values', () => { + const content = 'VAR=value_without_quotes'; + const result = parseEnvFile(content); + + expect(result.VAR).to.equal('value_without_quotes'); + }); + + test('Should handle empty values', () => { + const content = 'VAR='; + const result = parseEnvFile(content); + + expect(result.VAR).to.equal(''); + }); + + test('Should ignore lines without equals sign', () => { + const content = 'VAR1=value1\nInvalid line\nVAR2=value2'; + const result = parseEnvFile(content); + + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(result).to.deep.equal({ + VAR1: 'value1', + VAR2: 'value2', + }); + }); + + test('Should handle multiline value with multiple newlines', () => { + const content = "VAR='line1\nline2\nline3\nline4'"; + const result = parseEnvFile(content); + + expect(result.VAR).to.equal('line1\nline2\nline3\nline4'); + }); + + test('Should parse environment file as Buffer', () => { + const content = Buffer.from("VAR='multiline\nvalue'"); + const result = parseEnvFile(content); + + expect(result.VAR).to.equal('multiline\nvalue'); + }); + + test('Should handle whitespace around variable names and equals', () => { + const content = " VAR1 = value1 \n VAR2='multiline\nvalue'"; + const result = parseEnvFile(content); + + expect(result.VAR1).to.equal('value1'); + expect(result.VAR2).to.equal('multiline\nvalue'); + }); +});