diff --git a/src/CsToml.Extensions/CsTomlFileSerializer.cs b/src/CsToml.Extensions/CsTomlFileSerializer.cs index a809a75..9341399 100644 --- a/src/CsToml.Extensions/CsTomlFileSerializer.cs +++ b/src/CsToml.Extensions/CsTomlFileSerializer.cs @@ -2,6 +2,7 @@ using CsToml.Extensions.Utility; using Cysharp.Collections; using System.Buffers; +using System.Runtime.CompilerServices; namespace CsToml.Extensions; @@ -10,9 +11,11 @@ public partial class CsTomlFileSerializer private static readonly string TomlExtension = ".toml"; public static T Deserialize(string tomlFilePath, CsTomlSerializerOptions? options = null) + => Deserialize(tomlFilePath, options, TomlFileExtensionPolicy.Strict); + + public static T Deserialize(string tomlFilePath, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy) { - if (Path.GetExtension(tomlFilePath) != TomlExtension) - throw new FormatException($"TOML files should use the extension .toml"); + ValidateExtension(tomlFilePath, extensionPolicy); using var handle = File.OpenHandle(tomlFilePath!, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan); var length = RandomAccess.GetLength(handle); @@ -54,10 +57,15 @@ public static T Deserialize(string tomlFilePath, CsTomlSerializerOptions? opt } } - public static async ValueTask DeserializeAsync(string tomlFilePath, CsTomlSerializerOptions? options = null, bool configureAwait = false, CancellationToken cancellationToken = default) + public static ValueTask DeserializeAsync(string tomlFilePath, CsTomlSerializerOptions? options = null, bool configureAwait = false, CancellationToken cancellationToken = default) + => DeserializeAsyncCore(tomlFilePath, options, TomlFileExtensionPolicy.Strict, configureAwait, cancellationToken); + + public static ValueTask DeserializeAsync(string tomlFilePath, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy, bool configureAwait = false, CancellationToken cancellationToken = default) + => DeserializeAsyncCore(tomlFilePath, options, extensionPolicy, configureAwait, cancellationToken); + + private static async ValueTask DeserializeAsyncCore(string tomlFilePath, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy, bool configureAwait, CancellationToken cancellationToken) { - if (Path.GetExtension(tomlFilePath) != TomlExtension) - throw new FormatException($"TOML files should use the extension .toml"); + ValidateExtension(tomlFilePath, extensionPolicy); cancellationToken.ThrowIfCancellationRequested(); using var handle = File.OpenHandle(tomlFilePath!, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan | FileOptions.Asynchronous); @@ -99,9 +107,11 @@ public static async ValueTask DeserializeAsync(string tomlFilePath, CsToml } public static void Serialize(string tomlFilePath, T value, CsTomlSerializerOptions? options = null) + => Serialize(tomlFilePath, value, options, TomlFileExtensionPolicy.Strict); + + public static void Serialize(string tomlFilePath, T value, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy) { - if (Path.GetExtension(tomlFilePath) != TomlExtension) - throw new FormatException($"TOML file should use the extension .toml"); + ValidateExtension(tomlFilePath, extensionPolicy); var directory = new FileInfo(tomlFilePath).Directory; if (!directory!.Exists) @@ -116,10 +126,15 @@ public static void Serialize(string tomlFilePath, T value, CsTomlSerializerOp bufferWriter.WriteTo(fileWriter.ByteWriter); } - public static async ValueTask SerializeAsync(string tomlFilePath, T value, CsTomlSerializerOptions? options = null, bool configureAwait = false, CancellationToken cancellationToken = default) + public static ValueTask SerializeAsync(string tomlFilePath, T value, CsTomlSerializerOptions? options = null, bool configureAwait = false, CancellationToken cancellationToken = default) + => SerializeAsyncCore(tomlFilePath, value, options, TomlFileExtensionPolicy.Strict, configureAwait, cancellationToken); + + public static ValueTask SerializeAsync(string tomlFilePath, T value, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy, bool configureAwait = false, CancellationToken cancellationToken = default) + => SerializeAsyncCore(tomlFilePath, value, options, extensionPolicy, configureAwait, cancellationToken); + + private static async ValueTask SerializeAsyncCore(string tomlFilePath, T value, CsTomlSerializerOptions? options, TomlFileExtensionPolicy extensionPolicy, bool configureAwait, CancellationToken cancellationToken) { - if (Path.GetExtension(tomlFilePath) != TomlExtension) - throw new FormatException($"TOML file should use the extension .toml"); + ValidateExtension(tomlFilePath, extensionPolicy); var directory = new FileInfo(tomlFilePath).Directory; if (!directory!.Exists) @@ -136,6 +151,34 @@ public static async ValueTask SerializeAsync(string tomlFilePath, T value, Cs await bufferWriter.WriteToAsync(fileWriter.ByteWriter, configureAwait, cancellationToken); } -} + private static void ValidateExtension(string tomlFilePath, TomlFileExtensionPolicy extensionPolicy) + { + switch (extensionPolicy) + { + case TomlFileExtensionPolicy.Strict: + if (!string.Equals(Path.GetExtension(tomlFilePath), TomlExtension, StringComparison.Ordinal)) + { + ThrowExtensionFormatException(); + } + break; + case TomlFileExtensionPolicy.Relaxed: + // No validation needed for relaxed policy + break; + default: + ThrowExtensionOutOfRangeException(extensionPolicy); + break; + } + } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowExtensionFormatException() + { + throw new FormatException("TOML files should use the extension .toml"); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowExtensionOutOfRangeException(TomlFileExtensionPolicy extensionPolicy) + { + throw new ArgumentOutOfRangeException(nameof(extensionPolicy)); + } +} diff --git a/src/CsToml.Extensions/TomlFileExtensionPolicy.cs b/src/CsToml.Extensions/TomlFileExtensionPolicy.cs new file mode 100644 index 0000000..80d66b7 --- /dev/null +++ b/src/CsToml.Extensions/TomlFileExtensionPolicy.cs @@ -0,0 +1,8 @@ + +namespace CsToml.Extensions; + +public enum TomlFileExtensionPolicy +{ + Strict = 0, + Relaxed = 1 +} \ No newline at end of file diff --git a/tests/CsToml.Tests/CsTomlFileSerializerExtensionPolicyTest.cs b/tests/CsToml.Tests/CsTomlFileSerializerExtensionPolicyTest.cs new file mode 100644 index 0000000..2122147 --- /dev/null +++ b/tests/CsToml.Tests/CsTomlFileSerializerExtensionPolicyTest.cs @@ -0,0 +1,349 @@ +using CsToml.Extensions; + +namespace CsToml.Tests; + +public class CsTomlFileSerializerExtensionPolicyTest : IDisposable +{ + private readonly string _tempDir; + + public CsTomlFileSerializerExtensionPolicyTest() + { + _tempDir = Path.Combine(Path.GetTempPath(), $"CsToml_Test_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tempDir); + } + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } + + private string CreateTomlFile(string fileName, string content = "key = \"value\"") + { + var filePath = Path.Combine(_tempDir, fileName); + File.WriteAllBytes(filePath, System.Text.Encoding.UTF8.GetBytes(content)); + return filePath; + } + + private string GetNonExistentPath(string fileName) + { + return Path.Combine(_tempDir, fileName); + } + + [Fact] + public void Deserialize_Strict_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + + var doc = CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict); + + doc.ShouldNotBeNull(); + } + + [Fact] + public void Deserialize_Strict_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = CreateTomlFile("test.conf"); + + Should.Throw(() => + CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict)); + } + + [Fact] + public void Deserialize_Relaxed_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + + var doc = CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed); + + doc.ShouldNotBeNull(); + } + + [Fact] + public void Deserialize_Relaxed_WithNonTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.conf"); + + var doc = CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed); + + doc.ShouldNotBeNull(); + } + + [Fact] + public void Deserialize_DefaultOverload_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + + var doc = CsTomlFileSerializer.Deserialize(filePath); + + doc.ShouldNotBeNull(); + } + + [Fact] + public void Deserialize_DefaultOverload_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = CreateTomlFile("test.conf"); + + Should.Throw(() => + CsTomlFileSerializer.Deserialize(filePath)); + } + + [Fact] + public async ValueTask DeserializeAsync_Strict_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + + var doc = await CsTomlFileSerializer.DeserializeAsync(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict, cancellationToken: TestContext.Current.CancellationToken); + + doc.ShouldNotBeNull(); + } + + [Fact] + public async ValueTask DeserializeAsync_Strict_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = CreateTomlFile("test.conf"); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.DeserializeAsync(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async ValueTask DeserializeAsync_Relaxed_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + var doc = await CsTomlFileSerializer.DeserializeAsync(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed, cancellationToken: TestContext.Current.CancellationToken); + doc.ShouldNotBeNull(); + } + + [Fact] + public async ValueTask DeserializeAsync_Relaxed_WithNonTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.conf"); + var doc = await CsTomlFileSerializer.DeserializeAsync(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed, cancellationToken: TestContext.Current.CancellationToken); + doc.ShouldNotBeNull(); + } + + [Fact] + public async ValueTask DeserializeAsync_DefaultOverload_WithTomlExtension_Succeeds() + { + var filePath = CreateTomlFile("test.toml"); + var doc = await CsTomlFileSerializer.DeserializeAsync(filePath, cancellationToken: TestContext.Current.CancellationToken); + doc.ShouldNotBeNull(); + } + + [Fact] + public async ValueTask DeserializeAsync_DefaultOverload_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = CreateTomlFile("test.conf"); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.DeserializeAsync(filePath, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public void Serialize_Strict_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.NotThrow(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public void Serialize_Strict_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.Throw(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict)); + } + + [Fact] + public void Serialize_Relaxed_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.NotThrow(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public void Serialize_Relaxed_WithNonTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.NotThrow(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public void Serialize_DefaultOverload_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.NotThrow(() => + CsTomlFileSerializer.Serialize(filePath, doc)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public void Serialize_DefaultOverload_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.Throw(() => + CsTomlFileSerializer.Serialize(filePath, doc)); + } + + [Fact] + public async ValueTask SerializeAsync_Strict_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.NotThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict, cancellationToken: TestContext.Current.CancellationToken)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public async ValueTask SerializeAsync_Strict_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Strict, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async ValueTask SerializeAsync_Relaxed_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.NotThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed, cancellationToken: TestContext.Current.CancellationToken)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public async ValueTask SerializeAsync_Relaxed_WithNonTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.NotThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed, cancellationToken: TestContext.Current.CancellationToken)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public async ValueTask SerializeAsync_DefaultOverload_WithTomlExtension_Succeeds() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.NotThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, cancellationToken: TestContext.Current.CancellationToken)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public async ValueTask SerializeAsync_DefaultOverload_WithNonTomlExtension_ThrowsFormatException() + { + var filePath = GetNonExistentPath("output.conf"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("config.cfg")] + [InlineData("settings.ini")] + [InlineData("data.txt")] + [InlineData("noextension")] + public void Deserialize_Relaxed_WithVariousExtensions_Succeeds(string fileName) + { + var filePath = CreateTomlFile(fileName); + + var doc = CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed); + + doc.ShouldNotBeNull(); + } + + [Theory] + [InlineData("config.cfg")] + [InlineData("settings.ini")] + [InlineData("data.txt")] + [InlineData("noextension")] + public void Serialize_Relaxed_WithVariousExtensions_Succeeds(string fileName) + { + var filePath = GetNonExistentPath(fileName); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.NotThrow(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, TomlFileExtensionPolicy.Relaxed)); + + File.Exists(filePath).ShouldBeTrue(); + } + + [Fact] + public void Deserialize_UndefinedPolicy_ThrowsArgumentOutOfRangeException() + { + var filePath = CreateTomlFile("test.toml"); + + Should.Throw(() => + CsTomlFileSerializer.Deserialize(filePath, CsTomlSerializerOptions.Default, (TomlFileExtensionPolicy)99)); + } + + [Fact] + public async ValueTask DeserializeAsync_UndefinedPolicy_ThrowsArgumentOutOfRangeException() + { + var filePath = CreateTomlFile("test.toml"); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.DeserializeAsync(filePath, CsTomlSerializerOptions.Default, (TomlFileExtensionPolicy)99, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public void Serialize_UndefinedPolicy_ThrowsArgumentOutOfRangeException() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + Should.Throw(() => + CsTomlFileSerializer.Serialize(filePath, doc, CsTomlSerializerOptions.Default, (TomlFileExtensionPolicy)99)); + } + + [Fact] + public async ValueTask SerializeAsync_UndefinedPolicy_ThrowsArgumentOutOfRangeException() + { + var filePath = GetNonExistentPath("output.toml"); + var doc = CsTomlSerializer.Deserialize("key = \"value\""u8); + + await Should.ThrowAsync(async () => + await CsTomlFileSerializer.SerializeAsync(filePath, doc, CsTomlSerializerOptions.Default, (TomlFileExtensionPolicy)99, cancellationToken: TestContext.Current.CancellationToken)); + } + +} \ No newline at end of file