镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

With the default Encoding.UTF8, output starting with a BOM followed by a real U+FEFF loses both, so the same bytes decode differently per Encoding instance #145

Closed

Description

@matt-edmondson

What's wrong

AsyncProcessStreamReader builds its readers with detectEncodingFromByteOrderMarks: false (RunCommand/AsyncProcessStreamReader.cs:29-33) and then removes one leading U+FEFF itself in StripLeadingByteOrderMark (AsyncProcessStreamReader.cs:216-235). The remarks there assume that turning detection off "also turned off the stripping that came with it".

That's only true for encodings with no preamble. StreamReader skips the encoding's own preamble whenever encoding.Preamble.Length > 0, whatever detectEncodingFromByteOrderMarks is set to. OutputHandler defaults to Encoding.UTF8 (OutputHandler.cs:47), which has a preamble. So for the default handler:

  1. StreamReader drops the BOM.
  2. StripLeadingByteOrderMark then drops the next U+FEFF, which is real content.

The same double strip happens with Encoding.Unicode, Encoding.BigEndianUnicode and Encoding.UTF32.

Reproduction

A command writes the bytes EF BB BF EF BB BF 41 (a BOM, then U+FEFF, then A):

await RunCommand.ExecuteAsync("printf", [@"\357\273\277\357\273\277A"],
    new OutputHandler(s => sb.Append(s), null, encoding));
encoding Output delivered
Encoding.UTF8 (the default) "A": both U+FEFF are gone
new UTF8Encoding(false) "A": only the BOM is gone, which is what the doc comment promises

The underlying StreamReader behaviour is easy to confirm on its own. With detectEncodingFromByteOrderMarks: false, new StreamReader(new MemoryStream(bytes), Encoding.UTF8, false).ReadToEnd() returns FEFF 41, while the same call with new UTF8Encoding(false) returns FEFF FEFF 41.

Why it matters

The impact is low, but the data loss is silent and it hits the default configuration. Take a BOM-emitting tool that echoes a file which itself starts with a BOM: its captured output loses a character. Output should also not depend on which of two equivalent UTF-8 Encoding instances the caller passed.

Suggested fix

Strip in exactly one place. Either:

  • only call the manual strip when outputHandler.Encoding.GetPreamble().Length == 0, or
  • build the readers from a preamble-free copy of the encoding (for example new UTF8Encoding(false, throwOnInvalid) with the caller's settings preserved, or a wrapper whose Preamble is empty) and keep the manual strip.

Acceptance criteria

Related, but not a duplicate: #76 (closed) is the fix that introduced this stripping.

Activity

  1. matt-edmondson commented on Nov 8, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions