Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { uuid } from 'utils/common';
import { utils } from '@usebruno/common';

export const variablesToRaw = (variables) => {
return variables
.filter((v) => v.name && v.name.trim() !== '')
.map((v) => {
const value = v.value || '';
if (value.includes('\n') || value.includes('"') || value.includes('\'')) {
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `${v.name}="${escapedValue}"`;
}
return `${v.name}=${value}`;
})
.join('\n');
return utils.jsonToDotenv(variables);
};

export const rawToVariables = (rawContent) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/bruno-common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export {
extractPromptVariables,
extractPromptVariablesFromString
} from './prompt-variables';

export {
jsonToDotenv,
DotenvVariable
} from './jsonToDotenv';
139 changes: 139 additions & 0 deletions packages/bruno-common/src/utils/jsonToDotenv.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { jsonToDotenv } from './jsonToDotenv';
import * as dotenv from 'dotenv';

// Helper to parse .env content using dotenv package
const dotenvToJson = (content: string): Record<string, string> => {
return dotenv.parse(Buffer.from(content));
};

describe('jsonToDotenv', () => {
describe('basic serialization', () => {
test('it should serialize simple variables', () => {
const variables = [
{ name: 'FOO', value: 'bar' },
{ name: 'BAZ', value: 'qux' }
];
const output = jsonToDotenv(variables);
expect(output).toBe('FOO=bar\nBAZ=qux');
});

test('it should filter out variables with empty names', () => {
const variables = [
{ name: 'VALID', value: 'value' },
{ name: '', value: 'ignored' },
{ name: ' ', value: 'also ignored' }
];
const output = jsonToDotenv(variables);
expect(output).toBe('VALID=value');
});

test('it should handle empty values', () => {
const variables = [
{ name: 'EMPTY', value: '' },
{ name: 'UNDEFINED', value: undefined }
];
const output = jsonToDotenv(variables);
expect(output).toBe('EMPTY=\nUNDEFINED=');
});

test('it should return empty string for empty array', () => {
expect(jsonToDotenv([])).toBe('');
});

test('it should return empty string for non-array input', () => {
expect(jsonToDotenv(null as any)).toBe('');
expect(jsonToDotenv(undefined as any)).toBe('');
expect(jsonToDotenv({} as any)).toBe('');
});
});

describe('special character handling', () => {
test('it should quote values containing hash (#)', () => {
const variables = [
{ name: 'PASSWORD', value: 'ABC#DEF' },
{ name: 'SECRET', value: 'key#123' }
];
const output = jsonToDotenv(variables);
expect(output).toBe('PASSWORD="ABC#DEF"\nSECRET="key#123"');
});

test('it should quote values containing newlines and escape them', () => {
const variables = [{ name: 'MULTILINE', value: 'line1\nline2' }];
const output = jsonToDotenv(variables);
expect(output).toBe('MULTILINE="line1\\nline2"');
});

test('it should quote and escape values containing double quotes', () => {
const variables = [{ name: 'QUOTED', value: 'say "hello"' }];
const output = jsonToDotenv(variables);
expect(output).toBe('QUOTED="say \\"hello\\""');
});

test('it should quote values containing single quotes', () => {
const variables = [{ name: 'APOSTROPHE', value: 'it\'s fine' }];
const output = jsonToDotenv(variables);
expect(output).toBe('APOSTROPHE="it\'s fine"');
});

test('it should quote and escape values containing backslashes', () => {
const variables = [{ name: 'PATH', value: 'C:\\Users\\name' }];
const output = jsonToDotenv(variables);
expect(output).toBe('PATH="C:\\\\Users\\\\name"');
});
});

describe('round-trip with dotenvToJson', () => {
test('it should preserve simple values through round-trip', () => {
const variables = [
{ name: 'FOO', value: 'bar' },
{ name: 'BAZ', value: 'qux123' }
];
const serialized = jsonToDotenv(variables);
const parsed = dotenvToJson(serialized);
expect(parsed.FOO).toBe('bar');
expect(parsed.BAZ).toBe('qux123');
});

test('it should preserve values with hash (#) through round-trip', () => {
const variables = [
{ name: 'PASSWORD', value: 'ABC#DEF' },
{ name: 'API_KEY', value: 'key#123#456' },
{ name: 'HASH_START', value: '#startsWithHash' },
{ name: 'HASH_SPACE', value: 'value # comment-like' }
];
const serialized = jsonToDotenv(variables);
const parsed = dotenvToJson(serialized);
expect(parsed.PASSWORD).toBe('ABC#DEF');
expect(parsed.API_KEY).toBe('key#123#456');
expect(parsed.HASH_START).toBe('#startsWithHash');
expect(parsed.HASH_SPACE).toBe('value # comment-like');
});

test('it should preserve values with single quotes through round-trip', () => {
const variables = [{ name: 'APOSTROPHE', value: 'it\'s working' }];
const serialized = jsonToDotenv(variables);
const parsed = dotenvToJson(serialized);
expect(parsed.APOSTROPHE).toBe('it\'s working');
});

test('it should preserve empty values through round-trip', () => {
const variables = [{ name: 'EMPTY', value: '' }];
const serialized = jsonToDotenv(variables);
const parsed = dotenvToJson(serialized);
expect(parsed.EMPTY).toBe('');
});

test('it should handle complex real-world passwords', () => {
const variables = [
{ name: 'DB_PASSWORD', value: 'P@ss#w0rd!123' },
{ name: 'API_SECRET', value: 'abc#def$ghi%jkl' },
{ name: 'JWT_SECRET', value: 'secret-key#with-special_chars' }
];
const serialized = jsonToDotenv(variables);
const parsed = dotenvToJson(serialized);
expect(parsed.DB_PASSWORD).toBe('P@ss#w0rd!123');
expect(parsed.API_SECRET).toBe('abc#def$ghi%jkl');
expect(parsed.JWT_SECRET).toBe('secret-key#with-special_chars');
});
});
});
37 changes: 37 additions & 0 deletions packages/bruno-common/src/utils/jsonToDotenv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export interface DotenvVariable {
name: string;
value?: string;
}

/**
* Serializes an array of environment variables to .env file format.
*
* This is the inverse of dotenvToJson - it converts a variables array
* back to .env file content that can be parsed by the dotenv package.
*
* Values containing special characters are wrapped in double quotes:
* - newlines (\n): would break the line-based format
* - double quotes ("): need escaping
* - single quotes ('): need escaping
* - backslashes (\): need escaping
* - hash (#): would be interpreted as comment start by dotenv parser
*/
export const jsonToDotenv = (variables: DotenvVariable[]): string => {
if (!Array.isArray(variables)) {
return '';
}

return variables
.filter((v) => v.name && v.name.trim() !== '')
.map((v) => {
const value = v.value || '';
// If value contains special characters, wrap in quotes
if (value.includes('\n') || value.includes('"') || value.includes('\'') || value.includes('\\') || value.includes('#')) {
// Escape backslashes first, then double quotes, then newlines
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `${v.name}="${escapedValue}"`;
}
return `${v.name}=${value}`;
})
.join('\n');
};
18 changes: 2 additions & 16 deletions packages/bruno-electron/src/ipc/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
DEFAULT_COLLECTION_FORMAT
} = require('@usebruno/filestore');
const { dotenvToJson } = require('@usebruno/lang');
const { utils } = require('@usebruno/common');
const brunoConverters = require('@usebruno/converters');
const { postmanToBruno } = brunoConverters;
const { cookiesStore } = require('../store/cookies');
Expand Down Expand Up @@ -671,22 +672,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}

const dotEnvPath = path.join(collectionPathname, filename);

// Convert variables array to .env format
const content = variables
.filter((v) => v.name && v.name.trim() !== '')
.map((v) => {
const value = v.value || '';
// If value contains newlines or special characters, wrap in quotes
if (value.includes('\n') || value.includes('"') || value.includes('\'') || value.includes('\\')) {
// Escape backslashes first, then double quotes
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `${v.name}="${escapedValue}"`;
}
return `${v.name}=${value}`;
})
.join('\n');

const content = utils.jsonToDotenv(variables);
await writeFile(dotEnvPath, content);

return { success: true };
Expand Down
18 changes: 2 additions & 16 deletions packages/bruno-electron/src/ipc/global-environments.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { ipcMain } = require('electron');
const { utils: { jsonToDotenv } } = require('@usebruno/common');
const { globalEnvironmentsStore } = require('../store/global-environments');
const { generateUniqueName, sanitizeName, writeFile, isValidDotEnvFilename } = require('../utils/filesystem');

Expand Down Expand Up @@ -114,22 +115,7 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager)
}

const dotEnvPath = path.join(workspacePath, filename);

// Convert variables array to .env format
const content = variables
.filter((v) => v.name && v.name.trim() !== '')
.map((v) => {
const value = v.value || '';
// If value contains newlines or special characters, wrap in quotes
if (value.includes('\n') || value.includes('"') || value.includes('\'') || value.includes('\\')) {
// Escape backslashes first, then double quotes
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `${v.name}="${escapedValue}"`;
}
return `${v.name}=${value}`;
})
.join('\n');

const content = jsonToDotenv(variables);
await writeFile(dotEnvPath, content);

return { success: true };
Expand Down
Loading