diff --git a/lib/fs.js b/lib/fs.js index 4a03fada49ea8a..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,7 +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') { + if (isUtf8Encoding(options.encoding)) { if (!isInt32(path)) { path = getValidatedPath(path); } @@ -2390,7 +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')) { + 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!'); 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!');