Skip to content

Commit d88d698

Browse files
author
wdsmini
committed
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
1 parent 7f98286 commit d88d698

File tree

2 files changed

+110
-4
lines changed

2 files changed

+110
-4
lines changed

lib/fs.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,40 @@ function lazyLoadUtf8Stream() {
171171
Utf8Stream ??= require('internal/streams/fast-utf8-stream');
172172
}
173173

174+
// Check if encoding is a UTF-8 variant that should use the fast path.
175+
// This covers all valid UTF-8 encoding names: utf8, utf-8, UTF8, UTF-8,
176+
// and case-insensitive variations like Utf8, Utf-8, etc.
177+
function isUtf8Encoding(enc) {
178+
if (enc === 'utf8' || enc === 'utf-8') return true;
179+
if (enc.length === 4) {
180+
// Check for UTF8 (case-insensitive)
181+
const c1 = StringPrototypeCharCodeAt(enc, 0);
182+
const c2 = StringPrototypeCharCodeAt(enc, 1);
183+
const c3 = StringPrototypeCharCodeAt(enc, 2);
184+
const c4 = StringPrototypeCharCodeAt(enc, 3);
185+
// U/u, T/t, F/f, 8
186+
return (c1 === 85 || c1 === 117) && // U/u
187+
(c2 === 84 || c2 === 116) && // T/t
188+
(c3 === 70 || c3 === 102) && // F/f
189+
c4 === 56; // 8
190+
}
191+
if (enc.length === 5) {
192+
// Check for UTF-8 (case-insensitive)
193+
const c1 = StringPrototypeCharCodeAt(enc, 0);
194+
const c2 = StringPrototypeCharCodeAt(enc, 1);
195+
const c3 = StringPrototypeCharCodeAt(enc, 2);
196+
const c4 = StringPrototypeCharCodeAt(enc, 3);
197+
const c5 = StringPrototypeCharCodeAt(enc, 4);
198+
// U/u, T/t, F/f, -, 8
199+
return (c1 === 85 || c1 === 117) && // U/u
200+
(c2 === 84 || c2 === 116) && // T/t
201+
(c3 === 70 || c3 === 102) && // F/f
202+
c4 === 45 && // -
203+
c5 === 56; // 8
204+
}
205+
return false;
206+
}
207+
174208
// Ensure that callbacks run in the global context. Only use this function
175209
// for callbacks that are passed to the binding layer, callbacks that are
176210
// invoked from JS already run in the proper scope.
@@ -429,8 +463,7 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) {
429463
function readFileSync(path, options) {
430464
options = getOptions(options, { flag: 'r' });
431465

432-
if (options.encoding === 'utf8' || options.encoding === 'utf-8' ||
433-
options.encoding === 'UTF8' || options.encoding === 'UTF-8') {
466+
if (isUtf8Encoding(options.encoding)) {
434467
if (!isInt32(path)) {
435468
path = getValidatedPath(path);
436469
}
@@ -2391,8 +2424,7 @@ function writeFileSync(path, data, options) {
23912424
const flag = options.flag || 'w';
23922425

23932426
// C++ fast path for string data and UTF8 encoding
2394-
if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8' ||
2395-
options.encoding === 'UTF8' || options.encoding === 'UTF-8')) {
2427+
if (typeof data === 'string' && isUtf8Encoding(options.encoding)) {
23962428
if (!isInt32(path)) {
23972429
path = getValidatedPath(path);
23982430
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
// Test that fs.readFileSync and fs.writeFileSync accept all valid
4+
// UTF-8 encoding names (case-insensitive) for the fast path.
5+
// Refs: https://github.com/nodejs/node/issues/49888
6+
7+
const common = require('../common');
8+
const fs = require('fs');
9+
const path = require('path');
10+
const assert = require('assert');
11+
const tmpdir = require('../common/tmpdir');
12+
13+
tmpdir.refresh();
14+
15+
const testFile = path.join(tmpdir.path, 'test-utf8-encoding.txt');
16+
const testContent = 'Hello, World! 你好,世界!';
17+
18+
// All valid UTF-8 encoding variants that should use the fast path
19+
const utf8Variants = [
20+
'utf8',
21+
'utf-8',
22+
'UTF8',
23+
'UTF-8',
24+
'Utf8',
25+
'Utf-8',
26+
'uTf8',
27+
'uTf-8',
28+
'utF8',
29+
'utF-8',
30+
'UTf8',
31+
'UTf-8',
32+
'uTF8',
33+
'uTF-8',
34+
];
35+
36+
// Test writeFileSync with all UTF-8 variants
37+
for (const encoding of utf8Variants) {
38+
const testPath = path.join(tmpdir.path, `test-write-${encoding}.txt`);
39+
40+
// Should not throw and should write the file correctly
41+
fs.writeFileSync(testPath, testContent, { encoding });
42+
43+
// Verify the file was written correctly
44+
const content = fs.readFileSync(testPath, 'utf8');
45+
assert.strictEqual(content, testContent,
46+
`writeFileSync should work with encoding "${encoding}"`);
47+
}
48+
49+
// Test readFileSync with all UTF-8 variants
50+
for (const encoding of utf8Variants) {
51+
const testPath = path.join(tmpdir.path, `test-read-${encoding}.txt`);
52+
53+
// Create a test file
54+
fs.writeFileSync(testPath, testContent, 'utf8');
55+
56+
// Should read the file correctly with any UTF-8 variant
57+
const content = fs.readFileSync(testPath, { encoding });
58+
assert.strictEqual(content, testContent,
59+
`readFileSync should work with encoding "${encoding}"`);
60+
}
61+
62+
// Test that non-UTF-8 encodings still work correctly
63+
const otherEncodings = ['ascii', 'base64', 'hex', 'latin1'];
64+
for (const encoding of otherEncodings) {
65+
const testPath = path.join(tmpdir.path, `test-${encoding}.txt`);
66+
67+
// These should not use the UTF-8 fast path but should still work
68+
fs.writeFileSync(testPath, testContent, { encoding });
69+
const content = fs.readFileSync(testPath, { encoding });
70+
assert.strictEqual(content, testContent,
71+
`Should work with encoding "${encoding}"`);
72+
}
73+
74+
console.log('All tests passed!');

0 commit comments

Comments
 (0)