From 7f982864cb87508516a2910ac9856dc371de2aec Mon Sep 17 00:00:00 2001 From: wdsmini Date: Wed, 18 Mar 2026 09:54:53 +0800 Subject: [PATCH 1/2] fs: accept all valid utf8 values in fast paths Accept UTF8 and UTF-8 (uppercase) in fs.readFileSync and fs.writeFileSync fast paths, in addition to utf8 and utf-8. Fixes: https://github.com/nodejs/node/issues/49888 --- lib/fs.js | 6 ++- .../parallel/test-fs-utf8-fast-path-casing.js | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-fs-utf8-fast-path-casing.js diff --git a/lib/fs.js b/lib/fs.js index 4a03fada49ea8a..d16d64108f68ec 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -429,7 +429,8 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) { function readFileSync(path, options) { options = getOptions(options, { flag: 'r' }); - if (options.encoding === 'utf8' || options.encoding === 'utf-8') { + if (options.encoding === 'utf8' || options.encoding === 'utf-8' || + options.encoding === 'UTF8' || options.encoding === 'UTF-8') { if (!isInt32(path)) { path = getValidatedPath(path); } @@ -2390,7 +2391,8 @@ function writeFileSync(path, data, options) { const flag = options.flag || 'w'; // C++ fast path for string data and UTF8 encoding - if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8')) { + if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8' || + options.encoding === 'UTF8' || options.encoding === 'UTF-8')) { if (!isInt32(path)) { path = getValidatedPath(path); } diff --git a/test/parallel/test-fs-utf8-fast-path-casing.js b/test/parallel/test-fs-utf8-fast-path-casing.js new file mode 100644 index 00000000000000..c9dae85167857f --- /dev/null +++ b/test/parallel/test-fs-utf8-fast-path-casing.js @@ -0,0 +1,49 @@ +'use strict'; + +// This test ensures that fs.readFileSync and fs.writeFileSync +// accept all valid UTF8 encoding variants (utf8, utf-8, UTF8, UTF-8). +// Refs: https://github.com/nodejs/node/issues/49888 + +const common = require('../common'); +const tmpdir = require('../../test/common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +tmpdir.refresh(); + +const testContent = 'Hello, World! 你好,世界!'; +const encodings = ['utf8', 'utf-8', 'UTF8', 'UTF-8']; + +// Test writeFileSync and readFileSync with different UTF8 variants +for (const encoding of encodings) { + const testFile = tmpdir.resolve(`test_utf8_fast_path_${encoding}.txt`); + + // Test writeFileSync + fs.writeFileSync(testFile, testContent, { encoding }); + + // Test readFileSync + const result = fs.readFileSync(testFile, { encoding }); + assert.strictEqual(result, testContent, + `readFileSync should return correct content for encoding "${encoding}"`); +} + +// Test with file descriptor +for (const encoding of encodings) { + const testFile = tmpdir.resolve(`test_utf8_fast_path_fd_${encoding}.txt`); + + // Write with fd + const fdWrite = fs.openSync(testFile, 'w'); + fs.writeFileSync(fdWrite, testContent, { encoding }); + fs.closeSync(fdWrite); + + // Read with fd + const fdRead = fs.openSync(testFile, 'r'); + const result = fs.readFileSync(fdRead, { encoding }); + fs.closeSync(fdRead); + + assert.strictEqual(result, testContent, + `readFileSync with fd should return correct content for encoding "${encoding}"`); +} + +console.log('All UTF8 fast path casing tests passed!'); From d88d698bccc860b1212c036cbb566f4a8f559281 Mon Sep 17 00:00:00 2001 From: wdsmini Date: Sun, 22 Mar 2026 17:11:27 +0800 Subject: [PATCH 2/2] fs: accept all valid UTF-8 encoding names in fast paths The fs.readFileSync and fs.writeFileSync fast paths for UTF-8 encoding only accepted exact matches: 'utf8', 'utf-8', 'UTF8', 'UTF-8'. However, Node.js considers all case-insensitive variations as valid UTF-8 (e.g., 'Utf8', 'Utf-8', 'uTf8', etc.). This commit adds an isUtf8Encoding() helper function that accepts all valid UTF-8 encoding names, matching the behavior of normalizeEncoding() in lib/internal/util.js. Fixes #49888 --- lib/fs.js | 40 +++++++++- .../test-fs-utf8-encoding-case-insensitive.js | 74 +++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-fs-utf8-encoding-case-insensitive.js diff --git a/lib/fs.js b/lib/fs.js index d16d64108f68ec..dcc8d764a53c79 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -171,6 +171,40 @@ function lazyLoadUtf8Stream() { Utf8Stream ??= require('internal/streams/fast-utf8-stream'); } +// Check if encoding is a UTF-8 variant that should use the fast path. +// This covers all valid UTF-8 encoding names: utf8, utf-8, UTF8, UTF-8, +// and case-insensitive variations like Utf8, Utf-8, etc. +function isUtf8Encoding(enc) { + if (enc === 'utf8' || enc === 'utf-8') return true; + if (enc.length === 4) { + // Check for UTF8 (case-insensitive) + const c1 = StringPrototypeCharCodeAt(enc, 0); + const c2 = StringPrototypeCharCodeAt(enc, 1); + const c3 = StringPrototypeCharCodeAt(enc, 2); + const c4 = StringPrototypeCharCodeAt(enc, 3); + // U/u, T/t, F/f, 8 + return (c1 === 85 || c1 === 117) && // U/u + (c2 === 84 || c2 === 116) && // T/t + (c3 === 70 || c3 === 102) && // F/f + c4 === 56; // 8 + } + if (enc.length === 5) { + // Check for UTF-8 (case-insensitive) + const c1 = StringPrototypeCharCodeAt(enc, 0); + const c2 = StringPrototypeCharCodeAt(enc, 1); + const c3 = StringPrototypeCharCodeAt(enc, 2); + const c4 = StringPrototypeCharCodeAt(enc, 3); + const c5 = StringPrototypeCharCodeAt(enc, 4); + // U/u, T/t, F/f, -, 8 + return (c1 === 85 || c1 === 117) && // U/u + (c2 === 84 || c2 === 116) && // T/t + (c3 === 70 || c3 === 102) && // F/f + c4 === 45 && // - + c5 === 56; // 8 + } + return false; +} + // Ensure that callbacks run in the global context. Only use this function // for callbacks that are passed to the binding layer, callbacks that are // invoked from JS already run in the proper scope. @@ -429,8 +463,7 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) { function readFileSync(path, options) { options = getOptions(options, { flag: 'r' }); - if (options.encoding === 'utf8' || options.encoding === 'utf-8' || - options.encoding === 'UTF8' || options.encoding === 'UTF-8') { + if (isUtf8Encoding(options.encoding)) { if (!isInt32(path)) { path = getValidatedPath(path); } @@ -2391,8 +2424,7 @@ function writeFileSync(path, data, options) { const flag = options.flag || 'w'; // C++ fast path for string data and UTF8 encoding - if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8' || - options.encoding === 'UTF8' || options.encoding === 'UTF-8')) { + if (typeof data === 'string' && isUtf8Encoding(options.encoding)) { if (!isInt32(path)) { path = getValidatedPath(path); } diff --git a/test/parallel/test-fs-utf8-encoding-case-insensitive.js b/test/parallel/test-fs-utf8-encoding-case-insensitive.js new file mode 100644 index 00000000000000..659759138ed6c4 --- /dev/null +++ b/test/parallel/test-fs-utf8-encoding-case-insensitive.js @@ -0,0 +1,74 @@ +'use strict'; + +// Test that fs.readFileSync and fs.writeFileSync accept all valid +// UTF-8 encoding names (case-insensitive) for the fast path. +// Refs: https://github.com/nodejs/node/issues/49888 + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const testFile = path.join(tmpdir.path, 'test-utf8-encoding.txt'); +const testContent = 'Hello, World! 你好,世界!'; + +// All valid UTF-8 encoding variants that should use the fast path +const utf8Variants = [ + 'utf8', + 'utf-8', + 'UTF8', + 'UTF-8', + 'Utf8', + 'Utf-8', + 'uTf8', + 'uTf-8', + 'utF8', + 'utF-8', + 'UTf8', + 'UTf-8', + 'uTF8', + 'uTF-8', +]; + +// Test writeFileSync with all UTF-8 variants +for (const encoding of utf8Variants) { + const testPath = path.join(tmpdir.path, `test-write-${encoding}.txt`); + + // Should not throw and should write the file correctly + fs.writeFileSync(testPath, testContent, { encoding }); + + // Verify the file was written correctly + const content = fs.readFileSync(testPath, 'utf8'); + assert.strictEqual(content, testContent, + `writeFileSync should work with encoding "${encoding}"`); +} + +// Test readFileSync with all UTF-8 variants +for (const encoding of utf8Variants) { + const testPath = path.join(tmpdir.path, `test-read-${encoding}.txt`); + + // Create a test file + fs.writeFileSync(testPath, testContent, 'utf8'); + + // Should read the file correctly with any UTF-8 variant + const content = fs.readFileSync(testPath, { encoding }); + assert.strictEqual(content, testContent, + `readFileSync should work with encoding "${encoding}"`); +} + +// Test that non-UTF-8 encodings still work correctly +const otherEncodings = ['ascii', 'base64', 'hex', 'latin1']; +for (const encoding of otherEncodings) { + const testPath = path.join(tmpdir.path, `test-${encoding}.txt`); + + // These should not use the UTF-8 fast path but should still work + fs.writeFileSync(testPath, testContent, { encoding }); + const content = fs.readFileSync(testPath, { encoding }); + assert.strictEqual(content, testContent, + `Should work with encoding "${encoding}"`); +} + +console.log('All tests passed!');