diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..0c5fd02
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,10 @@
+{
+ "name": "C# Dev Container",
+ "image": "mcr.microsoft.com/devcontainers/dotnet",
+ "features": {
+ "ghcr.io/devcontainers/features/dotnet:2.2.2": {
+ "version": "9.0"
+ }
+ },
+ "postCreateCommand": "apt-get update && apt-get install -y exfatprogs"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index cc4d18e..c90b5fc 100644
--- a/README.md
+++ b/README.md
@@ -46,3 +46,15 @@ Restrictions of Windows, Linux and OsX are alle combined to an replacement patte
- Edge case Unicode sanitization: [.NET Framework](https://learn.microsoft.com/en-us/dotnet/framework/whats-new/#character-categories) uses Unicode 8.0, while .NET 8+ uses a newer version to detect unpaired surrogates and unassigned code points.
- This is relevant when dealing with emoticons.
- For example, ["💏🏻"](https://emojipedia.org/kiss-light-skin-tone) will be sanitized when running on .NET Framework 4.8, while it is supported as a valid filename on modern filesystems
+
+## Test setup
+
+The ExFat specific tests are skipped as long as no ExFat filesystem is available. Use this snippet to enable them:
+
+```powershell
+$vhdpath = 'C:\temp\ExFatTestContainer.vhd'
+$vhdsize = 100MB
+New-VHD -Path $vhdpath -Dynamic -SizeBytes $vhdsize | Mount-VHD -Passthru |Initialize-Disk -Passthru |New-Partition -AssignDriveLetter -UseMaximumSize |Format-Volume -FileSystem 'exFAT' -Confirm:$false -NewFileSystemLabel '{exfatLabel}' -Force|Out-Null
+```
+
+Running as admin will automaticly create and mount a Exfat drive while tests are running.
\ No newline at end of file
diff --git a/SanitizeFilenameTests/ExFatTooling/AutoPlayDisabledScope.cs b/SanitizeFilenameTests/ExFatTooling/AutoPlayDisabledScope.cs
new file mode 100644
index 0000000..8132b1b
--- /dev/null
+++ b/SanitizeFilenameTests/ExFatTooling/AutoPlayDisabledScope.cs
@@ -0,0 +1,74 @@
+using Microsoft.Win32;
+using System.Runtime.InteropServices;
+
+public class AutoPlayDisabledScope : IDisposable
+{
+ private const string AutoPlayRegKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer";
+ private const string AutoPlayRegValue = "NoDriveTypeAutoRun";
+ private const int DisableAllAutoPlay = 0xFF;
+
+ private static int? _originalValue;
+ private bool disposedValue;
+
+ public bool AutoPlayerInitialState { get; }
+
+ ///
+ /// Temporarily disables AutoPlay if it is enabled. Returns true if it was changed.
+ ///
+ public AutoPlayDisabledScope()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ AutoPlayerInitialState = false;
+ return;
+ }
+
+ object? current = Registry.GetValue(AutoPlayRegKey, AutoPlayRegValue, null);
+ if (current is int value && value == DisableAllAutoPlay)
+ {
+ // Already disabled
+ AutoPlayerInitialState = false;
+ return;
+ }
+
+ _originalValue = current as int?;
+ Registry.SetValue(AutoPlayRegKey, AutoPlayRegValue, DisableAllAutoPlay, RegistryValueKind.DWord);
+ AutoPlayerInitialState = true;
+ }
+
+ ///
+ /// Restores the original AutoPlay setting if it was changed.
+ ///
+ public static void RestoreAutoPlay()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ if (_originalValue.HasValue)
+ {
+ Registry.SetValue(AutoPlayRegKey, AutoPlayRegValue, _originalValue.Value, RegistryValueKind.DWord);
+ _originalValue = null;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ RestoreAutoPlay();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file
diff --git a/SanitizeFilenameTests/ExFatTooling/ExFatFileWriteAsserterFactory.cs b/SanitizeFilenameTests/ExFatTooling/ExFatFileWriteAsserterFactory.cs
new file mode 100644
index 0000000..e00b87d
--- /dev/null
+++ b/SanitizeFilenameTests/ExFatTooling/ExFatFileWriteAsserterFactory.cs
@@ -0,0 +1,166 @@
+using System.Runtime.InteropServices;
+
+namespace SanitizeFilenameTests.ExFatTooling
+{
+ public class ExFatFileWriteAsserterFactory : FileWriteAsserter
+ {
+ public static FileWriteAsserter? TryGetOrCreateExFatPartition(out string reason)
+ {
+ reason = string.Empty;
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ if (TryGetExFatPartition(out string path))
+ return new FileWriteAsserter(path);
+
+ reason = "ExFatFileWriteAsserterFactory is only applicable on Windows.";
+ return null;
+ }
+
+ if (RuntimeInformation.OSArchitecture == Architecture.Arm || RuntimeInformation.OSArchitecture == Architecture.Arm64)
+ {
+ if (TryGetExFatPartition(out string path))
+ return new FileWriteAsserter(path);
+
+ reason = "Test is skipped on Windows ARM because VHD mounting is not supported.";
+ return null;
+ }
+
+ if (!IsRunningAsAdministrator())
+ {
+ if (TryGetExFatPartition(out string path))
+ return new FileWriteAsserter(path);
+
+ reason = "Test requires administrator privileges to create and mount exFAT VHD or an mounted ExFat drive.";
+ return null;
+ }
+
+ var imageTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ var exFatPartitionCreateInfo = CreateAndMountExFatPartition(imageTempPath);
+ FileWriteAsserter fileWriteAsserter = new(exFatPartitionCreateInfo.TempPathInExFatPartition, exFatPartitionCreateInfo.VhdxPath);
+
+ return fileWriteAsserter;
+ }
+
+ private static bool TryGetExFatPartition(out string path)
+ {
+ path = string.Empty;
+ try
+ {
+ foreach (var drive in DriveInfo.GetDrives())
+ {
+ if (drive.IsReady && string.Equals(drive.DriveFormat, "exFAT", StringComparison.OrdinalIgnoreCase))
+ {
+ string testDir = Path.Combine(drive.RootDirectory.FullName, "test" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(testDir);
+ path = testDir;
+ return true;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore exceptions and return false
+ }
+ return false;
+ }
+
+ private static bool IsRunningAsAdministrator()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return false;
+ using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
+ var principal = new System.Security.Principal.WindowsPrincipal(identity);
+ return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
+ }
+
+ private static (string TempPathInExFatPartition, string VhdxPath) CreateAndMountExFatPartition(string ImageTempPath)
+ {
+ using AutoPlayDisabledScope autoPlayDisabledScope = new AutoPlayDisabledScope();
+
+ var vhdxPath = Path.Combine(ImageTempPath, $"exfat-test-{Guid.NewGuid():N}.vhdx");
+ string vhdxFileName = Path.GetFileName(vhdxPath);
+ // exFAT volume label max length is 11 characters
+ string exfatLabel = Guid.NewGuid().ToString("N").Substring(0, 11);
+
+ string psScript = $@"
+
+$vhdpath = '{vhdxPath}'
+$vhdsize = 100MB
+New-VHD -Path $vhdpath -Dynamic -SizeBytes $vhdsize | Mount-VHD -Passthru | Initialize-Disk -Passthru | Out-Null
+Start-Sleep -Seconds 2
+$disk = Get-Disk | Where-Object {{ $_.Location -like '*{vhdxFileName}*' }}
+$partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter
+Format-Volume -Partition $partition -FileSystem 'exFAT' -Confirm:$false -NewFileSystemLabel '{exfatLabel}' -Force | Out-Null
+$partition.DriveLetter
+";
+ var process = new System.Diagnostics.Process
+ {
+ StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "powershell",
+ Arguments = $"-NoProfile -Command \"{psScript.Replace("\"", "`\"")}\"",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+ process.Start();
+ string stdOut = process.StandardOutput.ReadToEnd().Trim();
+ string stdErr = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+ if (process.ExitCode != 0 || !string.IsNullOrEmpty(stdErr))
+ {
+ throw new InvalidOperationException(
+ $"Failed to create or mount exFAT VHDX for testing. Exit code: {process.ExitCode}. Output: {stdOut} Error: {stdErr}"
+ );
+ }
+
+ if (!string.IsNullOrEmpty(stdOut))
+ {
+ var TempPathOnExFat = stdOut + @":\test" + Guid.NewGuid();
+ if (!Directory.Exists(TempPathOnExFat))
+ Directory.CreateDirectory(TempPathOnExFat);
+
+ return (TempPathOnExFat, vhdxPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Failed to create or mount exFAT VHDX for testing.");
+ }
+ }
+
+ public static void UnmountAndDeleteImage(string exfatVhdxPath)
+ {
+ if (!string.IsNullOrEmpty(exfatVhdxPath))
+ {
+ // Unmount the exFAT VHDX by its file path
+ var unmountProcess = new System.Diagnostics.Process
+ {
+ StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "powershell",
+ Arguments = $"-NoProfile -Command \"Dismount-VHD -Path '{exfatVhdxPath}'\"",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+ unmountProcess.Start();
+ string stdOut = unmountProcess.StandardOutput.ReadToEnd();
+ string stdErr = unmountProcess.StandardError.ReadToEnd();
+ unmountProcess.WaitForExit();
+ if (unmountProcess.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"Failed to dismount exFAT VHD. Exit code: {unmountProcess.ExitCode}. Output: {stdOut} Error: {stdErr}");
+ }
+
+ if (File.Exists(exfatVhdxPath))
+ {
+ File.Delete(exfatVhdxPath);
+ }
+ }
+ }
+ }
+}
diff --git a/SanitizeFilenameTests/FilenameTests/FileWriteAsserter.cs b/SanitizeFilenameTests/FilenameTests/FileWriteAsserter.cs
index b621acf..9c0e816 100644
--- a/SanitizeFilenameTests/FilenameTests/FileWriteAsserter.cs
+++ b/SanitizeFilenameTests/FilenameTests/FileWriteAsserter.cs
@@ -1,15 +1,22 @@
-namespace SanitizeFilenameTests
+using SanitizeFilenameTests.ExFatTooling;
+
+namespace SanitizeFilenameTests
{
- public class FileWriteAsserter
+ public class FileWriteAsserter : IDisposable
{
- public FileWriteAsserter()
+ private bool disposedValue;
+
+ public FileWriteAsserter(string? tempPath = null, string? disposableVhdxPath = null)
{
- TempPath = Path.Combine(Path.GetTempPath(), "test" + Guid.NewGuid());
+ DisposableVhdxPath = disposableVhdxPath;
+ TempPath = tempPath ?? Path.Combine(Path.GetTempPath(), "test" + Guid.NewGuid());
+
if (!Directory.Exists(TempPath))
Directory.CreateDirectory(TempPath);
}
- public string TempPath { get; }
+ public string? DisposableVhdxPath { get; }
+ public string TempPath { get; set; }
internal void AssertCollection(List<(string, int)> validFilenames)
{
@@ -26,9 +33,6 @@ internal void AssertCollection(List<(string, int)> validFilenames)
}
});
- //invalidFilenames.Add(("test", 1));
- //invalidFilenames.Add(("test", 2));
-
Assert.That(invalidFilenames.OrderBy(x => x.Item2), Is.Empty, GenerateAssertionMessage(invalidFilenames));
}
@@ -62,5 +66,27 @@ private static string GenerateAssertionMessage(List<(string, int)> invalidFilena
{
return "Invalid chars: " + string.Join(", ", invalidFilenames.Select(x => $"{x.Item2}"));
}
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ if (DisposableVhdxPath != null)
+ ExFatFileWriteAsserterFactory.UnmountAndDeleteImage(DisposableVhdxPath);
+ else
+ Directory.Delete(TempPath, true);
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/SanitizeFilenameTests/FilenameTests/LinuxSpecificTests.cs b/SanitizeFilenameTests/FilenameTests/LinuxSpecificTests.cs
index a3fcf0e..f73883f 100644
--- a/SanitizeFilenameTests/FilenameTests/LinuxSpecificTests.cs
+++ b/SanitizeFilenameTests/FilenameTests/LinuxSpecificTests.cs
@@ -12,8 +12,7 @@ public LinuxSpecificTests()
[OneTimeTearDown]
public void TearDown()
{
- if (Directory.Exists(FileWriteAsserter.TempPath))
- Directory.Delete(FileWriteAsserter.TempPath, true);
+ FileWriteAsserter.Dispose();
}
[Test]
diff --git a/SanitizeFilenameTests/FilenameTests/OsXSpecificTests.cs b/SanitizeFilenameTests/FilenameTests/OsXSpecificTests.cs
index 9bf52bf..659a3cc 100644
--- a/SanitizeFilenameTests/FilenameTests/OsXSpecificTests.cs
+++ b/SanitizeFilenameTests/FilenameTests/OsXSpecificTests.cs
@@ -13,8 +13,7 @@ public OsXSpecificTests()
[OneTimeTearDown]
public void TearDown()
{
- if (Directory.Exists(FileWriteAsserter.TempPath))
- Directory.Delete(FileWriteAsserter.TempPath, true);
+ FileWriteAsserter.Dispose();
}
public FileWriteAsserter FileWriteAsserter { get; }
@@ -47,7 +46,7 @@ public void MacOsDoesNotSupportToWriteNotAssignedCodepointsWithSurrogates()
[Test]
public void ShouldSanitizeValidSurrogatesWithoutFollowingCodepoint()
{
- // https://unicodelookup.com/#557056/1
+ // https://unicodelookup.com/#557056/1
var oneOfManyValuesFoundByRunningEveryPossibleUTF16ValueAgainstMacOs = 557056;
var sanitizedFilenames = new List<(string, int)>();
string unicodeString = char.ConvertFromUtf32(oneOfManyValuesFoundByRunningEveryPossibleUTF16ValueAgainstMacOs);
@@ -64,7 +63,6 @@ public void ShouldSanitizeValidSurrogatesWithoutFollowingCodepoint()
[TestCase(3315)]
// U+11F02 Kawi Sign Repha https://codepoints.net/U+11F02
[TestCase(73474)]
-
public void MacOsSupportToWriteCodePointsThatFailedOnOsXGibhutRunnersInBeginOf2024(int bogusOsXValue)
{
// https://unicodelookup.com/#423939/1
diff --git a/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs b/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs
index ef606be..f99ba9e 100644
--- a/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs
+++ b/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs
@@ -16,8 +16,7 @@ public SanitizeFilenamesTests()
[OneTimeTearDown]
public void TearDown()
{
- if (Directory.Exists(FileWriteAsserter.TempPath))
- Directory.Delete(FileWriteAsserter.TempPath, true);
+ FileWriteAsserter.Dispose();
}
private static readonly string[] InvalidWindowsFileNames = ["invalidfilename", "invalid\"filename", "invalid/filename", "invalid\\filename", "invalid|filename", "invalid?filename", "invalid*filename",];
@@ -53,7 +52,7 @@ public void ShouldThrow(string invalidFilename, char replacement)
if (IsRunningOnNet4x())
{
- Assert.That(ex.Message, Is.EqualTo("Replacement '*' is invalid for Windows\r\nParameter name: replacement"));
+ Assert.That(ex?.Message, Is.EqualTo("Replacement '*' is invalid for Windows\r\nParameter name: replacement"));
return;
}
@@ -187,7 +186,9 @@ public void ShouldTruncateLongFileNamesPreserveUnicodeTextElements(string testSu
[TestCase("💏🏻", 13, "__", "Unicode 13.1 example https://emojipedia.org/kiss-light-skin-tone but is an https://emojipedia.org/emoji-modifier-sequence combining unicode a codpage from v8 and v6 -> that is not touched by ")]
[TestCase("", null, null, " Private Use Area (PUA) character that is supported on iOS and macOS https://emojipedia.org/apple-logo")]
[TestCase("⛷️", 5, null, "Unicode 5.2 example https://emojipedia.org/skier")]
+#pragma warning disable IDE0060 // unicodeVersionNote is used for documentation purposes only
public void ShouldNotBeTouchedBySanitizer(string unicodeSpecificEmoticon, int? unicodeVersion, string? expectedNetFramework, string unicodeVersionNote)
+#pragma warning restore IDE0060 // unicodeVersionNote is used for documentation purposes only
{
var expected = unicodeSpecificEmoticon;
@@ -214,6 +215,7 @@ public void ShouldNotBeTouchedBySanitizer(string unicodeSpecificEmoticon, int? u
[TestCase("🫠", "Unicode 14 example https://emojipedia.org/melting-face")]
[TestCase("🫥", "Unicode 14 example https://emojipedia.org/dotted-line-face")]
[TestCase("🪿", "Unicode 15 example https://emojipedia.org/goose")]
+#pragma warning disable IDE0060 // unicodeVersionNote is used for documentation purposes only
public void ShouldSanitizeUnicodeVersion9Plus(string unicodeSpecificEmoticon, string unicodeVersion)
{
var sanitizedFilename = unicodeSpecificEmoticon.SanitizeFilename();
@@ -234,4 +236,5 @@ public void Unicode17SpecificMacoOsBehavior(string unicodeSpecificEmoticon, stri
Assert.That(FileWriteAsserter.TryWriteFileToTempDirectory(unicodeSpecificEmoticon), Is.Not.EqualTo(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)));
}
}
+#pragma warning restore IDE0060 // unicodeVersionNote is used for documentation purposes only
}
\ No newline at end of file
diff --git a/SanitizeFilenameTests/FilenameTests/WindowsExFatSpecificTests.cs b/SanitizeFilenameTests/FilenameTests/WindowsExFatSpecificTests.cs
new file mode 100644
index 0000000..bf1d013
--- /dev/null
+++ b/SanitizeFilenameTests/FilenameTests/WindowsExFatSpecificTests.cs
@@ -0,0 +1,55 @@
+using Codeuctivity;
+using SanitizeFilenameTests.ExFatTooling;
+
+namespace SanitizeFilenameTests
+{
+ internal class WindowsExFatSpecificTests : SanitizeFilenamesTestsBase
+ {
+ public string GetOrCreateExFatPartitionFailReason { get; set; }
+ private FileWriteAsserter? ExFatFileWriteAsserter { get; set; }
+
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ ExFatFileWriteAsserter = ExFatFileWriteAsserterFactory.TryGetOrCreateExFatPartition(out var reason);
+ GetOrCreateExFatPartitionFailReason = reason;
+ }
+
+ [OneTimeTearDown]
+ public void TearDown()
+ {
+ ExFatFileWriteAsserter?.Dispose();
+ }
+
+ // will fail if an explorer window is open
+ [Test, Platform("Win")]
+ public void ShouldBehaviorSpecificOnExFat()
+ {
+ if (ExFatFileWriteAsserter == null)
+ {
+ Assert.Ignore(GetOrCreateExFatPartitionFailReason);
+ }
+
+ //https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#table-35-invalid-filename-characters
+ var invalidChars = new[] {
+ '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u0007',
+ '\u0008', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u000E', '\u000F',
+ '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017',
+ '\u0018', '\u0019', '\u001A', '\u001B', '\u001C', '\u001D', '\u001E', '\u001F',
+ '\"', '*', '/', ':', '<', '>', '?', '\\', '|'
+ };
+
+ foreach (var invalidOnExFat in invalidChars)
+ {
+ var filenameInvalidOnExFat = "valid" + invalidOnExFat + "filename";
+ var actual = ExFatFileWriteAsserter.TryWriteFileToTempDirectory(filenameInvalidOnExFat);
+ Assert.That(actual, Is.False,
+ $"Expected writing file with name '{filenameInvalidOnExFat}' to fail on exFAT, but it succeeded.");
+ var sanitizedFilename = filenameInvalidOnExFat.SanitizeFilename();
+ var actualSanitized = ExFatFileWriteAsserter.TryWriteFileToTempDirectory(sanitizedFilename);
+ Assert.That(actualSanitized, Is.True,
+ $"Expected writing sanitized file with name '{sanitizedFilename}' to succeed on exFAT, but it failed.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SanitizeFilenameTests/FilenameTests/WindowsSpecificTests.cs b/SanitizeFilenameTests/FilenameTests/WindowsSpecificTests.cs
index ea78207..3142986 100644
--- a/SanitizeFilenameTests/FilenameTests/WindowsSpecificTests.cs
+++ b/SanitizeFilenameTests/FilenameTests/WindowsSpecificTests.cs
@@ -12,8 +12,7 @@ public WindowsSpecificTests()
[OneTimeTearDown]
public void TearDown()
{
- if (Directory.Exists(FileWriteAsserter.TempPath))
- Directory.Delete(FileWriteAsserter.TempPath, true);
+ FileWriteAsserter.Dispose();
}
public FileWriteAsserter FileWriteAsserter { get; }
@@ -25,7 +24,8 @@ public void ShouldBehaviorOsDependentOnWritingFilenameWithKnownWindowsSpecificEx
{
var filenameInvalidOnWindows = "valid" + invalidOnWindows + "filename";
var actual = FileWriteAsserter.TryWriteFileToTempDirectory(filenameInvalidOnWindows);
- Assert.That(actual, Is.False);
+ Assert.That(actual, Is.False,
+ $"Expected writing file with name '{filenameInvalidOnWindows}' to fail on Windows, but it succeeded.");
}
}
}
diff --git a/SanitizeFilenameTests/SanitizeFilenameTests.csproj b/SanitizeFilenameTests/SanitizeFilenameTests.csproj
index d81e4d4..12f2fd0 100644
--- a/SanitizeFilenameTests/SanitizeFilenameTests.csproj
+++ b/SanitizeFilenameTests/SanitizeFilenameTests.csproj
@@ -1,4 +1,4 @@
-
+
net8.0;net9.0;net48
@@ -12,10 +12,10 @@
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive