diff --git a/lib/internal/util/stringify_env.js b/lib/internal/util/stringify_env.js new file mode 100644 index 00000000000000..1cfdf7f317f333 --- /dev/null +++ b/lib/internal/util/stringify_env.js @@ -0,0 +1,50 @@ +'use strict'; + +const { + ArrayPrototypePush, + ObjectEntries, + RegExpPrototypeTest, + String, + StringPrototypeReplace, +} = primordials; + +const { + validateObject, +} = require('internal/validators'); + +const kEnvNeedQuoteRegExp = /[\s"'\n#=]/; +const kEnvEscapeRegExp = /(["\\])/g; +const kEnvNewlinesRegExp = /\n/g; +const kEnvReturnRegExp = /\r/g; + +function stringifyEnv(env) { + validateObject(env, 'env'); + + const lines = []; + const entries = ObjectEntries(env); + + for (let i = 0; i < entries.length; i++) { + const { 0: key, 1: value } = entries[i]; + const strValue = String(value); + + // If the value contains characters that need quoting in .env files + // (space, newline, quote, etc.), quoting is safer. + // For simplicity and safety, we quote if it has spaces, quotes, newlines, + // or starts with # (comment). + if (strValue === '' || RegExpPrototypeTest(kEnvNeedQuoteRegExp, strValue)) { + // Escape existing double quotes and newlines + const escaped = StringPrototypeReplace(strValue, kEnvEscapeRegExp, '\\$1') + .replace(kEnvNewlinesRegExp, '\\n') + .replace(kEnvReturnRegExp, '\\r'); + ArrayPrototypePush(lines, `${key}="${escaped}"`); + } else { + ArrayPrototypePush(lines, `${key}=${strValue}`); + } + } + + return lines.join('\n'); +} + +module.exports = { + stringifyEnv, +}; diff --git a/lib/util.js b/lib/util.js index 0baa747acd23d9..603635363077e7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -226,7 +226,7 @@ function inherits(ctor, superCtor) { if (superCtor.prototype === undefined) { throw new ERR_INVALID_ARG_TYPE('superCtor.prototype', - 'Object', superCtor.prototype); + 'Object', superCtor.prototype); } ObjectDefineProperty(ctor, 'super_', { __proto__: null, @@ -288,7 +288,7 @@ function callbackify(original) { // implications (stack, `uncaughtException`, `async_hooks`) ReflectApply(original, this, args) .then((ret) => process.nextTick(cb, null, ret), - (rej) => process.nextTick(callbackifyOnRejected, rej, cb)); + (rej) => process.nextTick(callbackifyOnRejected, rej, cb)); } const descriptors = ObjectGetOwnPropertyDescriptors(original); @@ -302,8 +302,8 @@ function callbackify(original) { } const propertiesValues = ObjectValues(descriptors); for (let i = 0; i < propertiesValues.length; i++) { - // We want to use null-prototype objects to not rely on globally mutable - // %Object.prototype%. + // We want to use null-prototype objects to not rely on globally mutable + // %Object.prototype%. ObjectSetPrototypeOf(propertiesValues[i], null); } ObjectDefineProperties(callbackified, descriptors); @@ -470,8 +470,8 @@ module.exports = { _errnoException, _exceptionWithHostPort, _extend: internalDeprecate(_extend, - 'The `util._extend` API is deprecated. Please use Object.assign() instead.', - 'DEP0060'), + 'The `util._extend` API is deprecated. Please use Object.assign() instead.', + 'DEP0060'), callbackify, convertProcessSignalToExitCode, debug: debuglog, @@ -487,8 +487,8 @@ module.exports = { inherits, inspect, isArray: internalDeprecate(ArrayIsArray, - 'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.', - 'DEP0044'), + 'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.', + 'DEP0044'), isDeepStrictEqual(a, b, skipPrototype) { if (internalDeepEqual === undefined) { internalDeepEqual = require('internal/util/comparisons').isDeepStrictEqual; @@ -542,3 +542,9 @@ defineLazyProperties( 'internal/util/trace_sigint', ['setTraceSigInt'], ); + +defineLazyProperties( + module.exports, + 'internal/util/stringify_env', + ['stringifyEnv'], +); diff --git a/test/parallel/test-util-env.js b/test/parallel/test-util-env.js new file mode 100644 index 00000000000000..dc628bd86c40c8 --- /dev/null +++ b/test/parallel/test-util-env.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); + +// Test util.stringifyEnv +{ + const simple = { A: '1', B: '2' }; + assert.strictEqual(util.stringifyEnv(simple), 'A=1\nB=2'); +} + +{ + const quotes = { A: '1 "2" 3' }; + assert.strictEqual(util.stringifyEnv(quotes), 'A="1 \\"2\\" 3"'); +} + +{ + const newlines = { A: '1\n2' }; + assert.strictEqual(util.stringifyEnv(newlines), 'A="1\\n2"'); +} + +{ + const empty = {}; + assert.strictEqual(util.stringifyEnv(empty), ''); +} + +{ + const complex = { + A: 'val_a', + B: 'val_b', + C: 'val with spaces', + D: 'val_with_"quotes"', + E: 'val_with_\n_newlines' + }; + const expected = 'A=val_a\n' + + 'B=val_b\n' + + 'C="val with spaces"\n' + + 'D="val_with_\\"quotes\\""\n' + + 'E="val_with_\\n_newlines"'; + assert.strictEqual(util.stringifyEnv(complex), expected); +} + +// Test validation +{ + assert.throws(() => util.stringifyEnv(null), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws(() => util.stringifyEnv('string'), { + code: 'ERR_INVALID_ARG_TYPE', + }); +}