diff --git a/PowerKit.Tests/EncodingExtensionsTests.cs b/PowerKit.Tests/EncodingExtensionsTests.cs index c33bce2..ff2df08 100644 --- a/PowerKit.Tests/EncodingExtensionsTests.cs +++ b/PowerKit.Tests/EncodingExtensionsTests.cs @@ -19,4 +19,48 @@ public void Utf8WithoutBom_Test() // Assert Encoding.UTF8.GetString(bytes).Should().Be(text); } + + [Fact] + public void WithoutPreamble_Test() + { + // Arrange + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + encoding.GetPreamble().Should().NotBeEmpty(); + + // Act + var result = encoding.WithoutPreamble(); + + // Assert + result.GetPreamble().Should().BeEmpty(); + result.GetString(result.GetBytes("hello")).Should().Be("hello"); + } + + [Fact] + public void WithoutPreamble_WithoutPreamble_Test() + { + // Arrange + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + encoding.GetPreamble().Should().BeEmpty(); + + // Act + var result = encoding.WithoutPreamble(); + + // Assert + result.Should().BeSameAs(encoding); + } + + [Fact] + public void WithoutPreamble_FallbackIsolation_Test() + { + // Arrange — wrap the shared UTF8 singleton (read-only) + var originalFallback = Encoding.UTF8.EncoderFallback; + + // Act — should not throw even though Encoding.UTF8 is a read-only singleton + var encoding = Encoding.UTF8.WithoutPreamble(); + + // Assert — the original singleton is not mutated and encode/decode still works + Encoding.UTF8.EncoderFallback.Should().BeSameAs(originalFallback); + encoding.GetPreamble().Should().BeEmpty(); + encoding.GetString(encoding.GetBytes("hello")).Should().Be("hello"); + } } diff --git a/PowerKit/Extensions/EncodingExtensions.cs b/PowerKit/Extensions/EncodingExtensions.cs index 293e5ae..561a116 100644 --- a/PowerKit/Extensions/EncodingExtensions.cs +++ b/PowerKit/Extensions/EncodingExtensions.cs @@ -2,18 +2,70 @@ namespace PowerKit.Extensions; +file sealed class NoPreambleEncoding : Encoding +{ + // Cloned for isolation — prevents mutations to shared singletons like Encoding.UTF8. + private readonly Encoding _inner; + + public NoPreambleEncoding(Encoding inner) => + // Clone for isolation — prevents mutations to shared singletons like Encoding.UTF8, + // and ensures the clone carries the source's fallbacks into all encode/decode operations. + _inner = (Encoding)inner.Clone(); + + public override string BodyName => _inner.BodyName; + public override string EncodingName => _inner.EncodingName; + public override string HeaderName => _inner.HeaderName; + public override string WebName => _inner.WebName; + public override int CodePage => _inner.CodePage; + public override bool IsBrowserDisplay => _inner.IsBrowserDisplay; + public override bool IsBrowserSave => _inner.IsBrowserSave; + public override bool IsMailNewsDisplay => _inner.IsMailNewsDisplay; + public override bool IsMailNewsSave => _inner.IsMailNewsSave; + public override bool IsSingleByte => _inner.IsSingleByte; + + public override byte[] GetPreamble() => []; + + public override int GetByteCount(char[] chars, int index, int count) => + _inner.GetByteCount(chars, index, count); + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => + _inner.GetBytes(chars, charIndex, charCount, bytes, byteIndex); + + public override int GetCharCount(byte[] bytes, int index, int count) => + _inner.GetCharCount(bytes, index, count); + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => + _inner.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + + public override int GetMaxByteCount(int charCount) => _inner.GetMaxByteCount(charCount); + + public override int GetMaxCharCount(int byteCount) => _inner.GetMaxCharCount(byteCount); + + public override Encoder GetEncoder() => _inner.GetEncoder(); + + public override Decoder GetDecoder() => _inner.GetDecoder(); +} + file static class EncodingEx { - public static Encoding Utf8WithoutBom { get; } = new UTF8Encoding(false); + public static Encoding Utf8WithoutBom { get; } = Encoding.UTF8.WithoutPreamble(); } internal static class EncodingExtensions { - extension(Encoding) + extension(Encoding encoding) { /// /// Gets an instance of the UTF-8 encoding that does not emit a byte order mark (BOM). /// public static Encoding Utf8WithoutBom => EncodingEx.Utf8WithoutBom; + + /// + /// Creates a derived encoding that produces an empty preamble, regardless of the original encoding's preamble. + /// + public Encoding WithoutPreamble() => + encoding.GetPreamble().Length > 0 + ? new NoPreambleEncoding(encoding) + : encoding; } }