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;
}
}