fix(filesystem): preserve UTF-8 multibyte characters across chunk boundaries in head/tail - #4186
Open
ishidakei wants to merge 1 commit into
Open
Conversation
…ndaries in head/tail
read_text_file with head or tail corrupts multi-byte UTF-8 characters
that straddle the 1024-byte chunk read boundary. Each chunk was decoded
independently via Buffer.toString('utf-8'), so partial byte sequences
at chunk edges became U+FFFD.
- headFile: use node:string_decoder StringDecoder to hold trailing
partial bytes across reads
- tailFile: accumulate raw Buffer chunks and decode the concatenated
buffer once; count newlines at byte level (0x0A is unambiguous in
UTF-8). The discarded partial first line absorbs any U+FFFD at the
lowest read position.
Adds encoding.test.ts with real-I/O tests placing 3-byte and 4-byte
UTF-8 characters at exact chunk boundaries for both head and tail.
This comment was marked as abuse.
This comment was marked as abuse.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
read_text_filewithheadortailcorrupts UTF-8 multibyte characters that straddle the 1024-byte chunk read boundary. The corrupted bytes becomeU+FFFD(replacement character). Reading the same file in full mode (nohead/tail) is unaffected.Server Details
lib.ts(headFile,tailFile)Motivation and Context
Both
headFileandtailFileread the file as raw byte chunks viafileHandle.read(chunk, 0, size, position)and then call.toString('utf-8')on each chunk independently before concatenating the resulting strings. UTF-8 multi-byte sequences (e.g. CJK characters at 3 bytes each, most non-Latin scripts at 2–4 bytes) that span a chunk boundary are split mid-sequence, so each chunk decodes its partial bytes asU+FFFDand the original character is lost.Reproduction (default 1024-byte chunk):
あ(UTF-8:E3 81 82), then a newline.head: 1returns ASCII followed by���instead ofあ.The same mechanism breaks
tailsymmetrically: any multi-byte character spanning a 1024-byte boundary measured from EOF is replaced withU+FFFD.This impacts every script outside Basic Latin in practice — Japanese/Chinese/Korean, Cyrillic, Greek, Arabic, accented Latin (German umlauts, French/Spanish accents, etc.), and emoji.
Changes
headFile: usenode:string_decoderStringDecoderso trailing partial bytes from one read are held internally and combined with the leading bytes of the next read.decoder.end()is called on EOF to flush any genuinely truncated sequence.tailFile: accumulate raw bytes into aBuffer[]in iteration order and decode the concatenated buffer once at the end. Newline counting is performed at the byte level (safe because0x0Anever appears inside a UTF-8 multi-byte sequence by design). The discarded partial first line absorbs anyU+FFFDintroduced at the lowest read position.Behavior on ASCII input is unchanged. Memory characteristics of
tailFileare unchanged (chunks accumulated correspond to the same range of bytes the original read).How Has This Been Tested?
src/filesystem/__tests__/encoding.test.tsusing real temp file I/O (nofsmocking) that place 3-byte (あ) and 4-byte (😀) UTF-8 characters at exact 1024-byte chunk boundaries for both head and tail directions. The new tests fail onmainand pass with this change.lib.test.tsand other suites continue to pass — full local run shows 156 passed across 8 test files.read_text_filewithheadortailreturnedU+FFFDat multiples of 1024 bytes from the relevant end.Breaking Changes
None. The fix only affects previously incorrect output for non-ASCII input.
Types of changes
Checklist
read_text_filealready documents UTF-8 support; this fix brings behavior in line with documentation)