Repository: nodejs/node
Pull request: module: use buffer.toString base64

`btoa` only supports latin-1 charset and produces invalid source mapping urls.

This also forbids usage of `btoa` in `lib/` without particular reasons.

Refs: https://github.com/nodejs/node/issues/56296

Changed files:
filename: lib/eslint.config_partial.mjs
patch: @@ -13,6 +13,15 @@ const noRestrictedSyntax = [
     selector: "CallExpression[callee.object.name='assert']:not([callee.property.name='ok']):not([callee.property.name='fail']):not([callee.property.name='ifError'])",
     message: 'Only use simple assertions',
   },
+  {
+    // Forbids usages of `btoa` that are not caught by no-restricted-globals, like:
+    // ```
+    // const { btoa } = internalBinding('buffer');
+    // btoa('...');
+    // ```
+    selector: "CallExpression[callee.property.name='btoa'], CallExpression[callee.name='btoa']",
+    message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
+  },
   {
     selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
     message: "Use an error exported by 'internal/errors' instead.",

filename: lib/internal/modules/typescript.js
patch: @@ -17,6 +17,7 @@ const {
 } = require('internal/errors').codes;
 const { getOptionValue } = require('internal/options');
 const assert = require('internal/assert');
+const { Buffer } = require('buffer');
 
 /**
  * The TypeScript parsing mode, either 'strip-only' or 'transform'.
@@ -134,9 +135,10 @@ function stripTypeScriptModuleTypes(source, filename) {
  * @returns {string} The code with the source map attached.
  */
 function addSourceMap(code, sourceMap) {
-  // TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
-  // base64 transformation, we should change this line.
-  const base64SourceMap = internalBinding('buffer').btoa(sourceMap);
+  // The base64 encoding should be https://datatracker.ietf.org/doc/html/rfc4648#section-4,
+  // not base64url https://datatracker.ietf.org/doc/html/rfc4648#section-5. See data url
+  // spec https://tools.ietf.org/html/rfc2397#section-2.
+  const base64SourceMap = Buffer.from(sourceMap).toString('base64');
   return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
 }
 

filename: test/parallel/test-module-strip-types.js
patch: @@ -12,6 +12,12 @@ common.expectWarning(
   'stripTypeScriptTypes is an experimental feature and might change at any time',
 );
 
+const sourceToBeTransformed = `
+  namespace MathUtil {
+    export const add = (a: number, b: number) => a + b;
+  }`;
+const sourceToBeTransformedMapping = 'UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA';
+
 test('stripTypeScriptTypes', () => {
   const source = 'const x: number = 1;';
   const result = stripTypeScriptTypes(source);
@@ -48,45 +54,52 @@ test('stripTypeScriptTypes sourceUrl throws when mode is strip', () => {
 });
 
 test('stripTypeScriptTypes source map when mode is transform', () => {
-  const source = `
-  namespace MathUtil {
-    export const add = (a: number, b: number) => a + b;
-  }`;
-  const result = stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true });
+  const result = stripTypeScriptTypes(sourceToBeTransformed, { mode: 'transform', sourceMap: true });
   const script = new vm.Script(result);
   const sourceMap =
     {
       version: 3,
-      sources: [
-        '<anon>',
-      ],
-      sourcesContent: [
-        '\n  namespace MathUtil {\n    export const add = (a: number, b: number) => a + b;\n  }',
-      ],
+      sources: [''],
       names: [],
-      mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
+      mappings: sourceToBeTransformedMapping,
     };
-  assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
+  const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
+  assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`);
 });
 
 test('stripTypeScriptTypes source map when mode is transform and sourceUrl', () => {
-  const source = `
-  namespace MathUtil {
-    export const add = (a: number, b: number) => a + b;
-  }`;
-  const result = stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true, sourceUrl: 'test.ts' });
+  const result = stripTypeScriptTypes(sourceToBeTransformed, {
+    mode: 'transform',
+    sourceMap: true,
+    sourceUrl: 'test.ts'
+  });
+  const script = new vm.Script(result);
+  const sourceMap =
+    {
+      version: 3,
+      sources: ['test.ts'],
+      names: [],
+      mappings: sourceToBeTransformedMapping,
+    };
+  const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
+  assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`);
+});
+
+test('stripTypeScriptTypes source map when mode is transform and sourceUrl with non-latin-1 chars', () => {
+  const sourceUrl = 'dir%20with $unusual"chars?\'åß∂ƒ©∆¬…`.cts';
+  const result = stripTypeScriptTypes(sourceToBeTransformed, {
+    mode: 'transform',
+    sourceMap: true,
+    sourceUrl,
+  });
   const script = new vm.Script(result);
   const sourceMap =
     {
       version: 3,
-      sources: [
-        'test.ts',
-      ],
-      sourcesContent: [
-        '\n  namespace MathUtil {\n    export const add = (a: number, b: number) => a + b;\n  }',
-      ],
+      sources: [sourceUrl],
       names: [],
-      mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
+      mappings: sourceToBeTransformedMapping,
     };
-  assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
+  const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
+  assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`);
 });