Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
385c39e
Update SanitizeFilenamesTests.cs
stesee Apr 23, 2025
444db22
Update README.md
stesee Apr 23, 2025
643edd9
Update dotnet.yml
stesee Apr 28, 2025
696f5c8
Merge pull request #22 from Codeuctivity/stesee-patch-1
stesee Apr 28, 2025
7061baf
Create FUNDING.yml
stesee Apr 28, 2025
06da956
Merge pull request #23 from Codeuctivity/stesee-patch-1-1
stesee Apr 28, 2025
537bfae
Update README.md
stesee Apr 28, 2025
8daee34
Merge pull request #24 from Codeuctivity/stesee-patch-2
stesee Apr 28, 2025
2f41972
Update FUNDING.yml
stesee May 3, 2025
1752466
Merge pull request #25 from Codeuctivity/stesee-patch-3
stesee May 3, 2025
c7c4c91
Update recommendations in extensions.json and enhance README for clar…
stesee May 27, 2025
c2b2564
Fix typos and improve clarity in README and nugetReadme files
stesee May 27, 2025
3871034
Merge pull request #26 from Codeuctivity/HouseKeeping
stesee May 27, 2025
d44440b
Update README.md
stesee May 27, 2025
c149863
Merge pull request #27 from Codeuctivity/stesee-patch-3
stesee May 27, 2025
0a312a0
Add initial devcontainer configuration for C# development
stesee May 30, 2025
23a74dd
Update devcontainer configuration to remove additional version specif…
stesee May 30, 2025
7de307c
Refactor Windows-specific test to remove platform attribute
stesee May 30, 2025
1619f20
exFat test experiment
stesee May 30, 2025
f9f485e
Enhance exFAT support in tests and devcontainer configuration
stesee May 30, 2025
00beed6
Add installation step for exfatprogs in Ubuntu workflow
stesee May 30, 2025
a4426e0
Refactor exFAT formatting command to use bash for improved compatibility
stesee May 30, 2025
8ea42ab
Refactor exFAT mounting process to capture output and errors for bett…
stesee May 30, 2025
c3ca207
Update Ubuntu workflow to install exfat-fuse alongside exfatprogs for…
stesee May 30, 2025
c446b43
Add exfat-utils to Ubuntu installation step for enhanced exFAT support
stesee May 30, 2025
aa10068
Remove exfat-utils from Ubuntu installation step for exfatprogs
stesee May 30, 2025
1b791af
Fix comment typo in CustomFsFileWriteAsserter class
stesee May 30, 2025
e7ac59b
Refactor file write assertion tests to use IDisposable pattern and ad…
stesee Jun 1, 2025
e7f1662
Enhance WindowsExFatSpecificTests to check for administrator privileg…
stesee Jun 1, 2025
efe16d7
Add error handling for exFAT VHDX creation failure in FileWriteAsserter
stesee Jun 1, 2025
72cd69e
Improve exFAT partition initialization logic in FileWriteAsserter
stesee Jun 1, 2025
4df1b3b
Refactor WindowsExFatSpecificTests to initialize FileWriteAsserter co…
stesee Jun 1, 2025
45bd79d
Fix exFat setup
stesee Jun 2, 2025
d81acca
Enhance exFAT testing setup and functionality by adding AutoPlayDisab…
stesee Jun 8, 2025
6ae41b6
Update project files and dependencies: fix newline at end of file in …
stesee Jun 8, 2025
37e1c24
Refactor exFAT testing setup: remove unnecessary installation step fo…
stesee Jun 8, 2025
096ced3
Merge pull request #28 from Codeuctivity/DevContainer
stesee Jun 9, 2025
ce9f21e
Bump actions/checkout from 4 to 5
dependabot[bot] Aug 18, 2025
6acc2ea
Merge pull request #29 from Codeuctivity/dependabot/github_actions/ac…
stesee Aug 18, 2025
1a11886
Bump actions/setup-dotnet from 4 to 5
dependabot[bot] Sep 8, 2025
e49a159
Bump actions/stale from 9 to 10
dependabot[bot] Sep 8, 2025
665cc87
Merge pull request #30 from Codeuctivity/dependabot/github_actions/ac…
stesee Sep 8, 2025
34d1f74
Merge pull request #31 from Codeuctivity/dependabot/github_actions/ac…
stesee Sep 8, 2025
5ab6261
Update SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs
stesee Oct 16, 2025
53bbc8a
Update .NET targets, workflows, and Unicode tests
stesee Oct 16, 2025
240faf9
Add net10.0 target framework to test project
stesee Oct 16, 2025
01d18dc
Merge branch 'net10' into Unicode17Tests
stesee Oct 16, 2025
75b3977
Update test project target frameworks and CI triggers
stesee Oct 16, 2025
126068b
Merge pull request #33 from Codeuctivity/Unicode17Tests
stesee Oct 16, 2025
7f5854f
Merge pull request #34 from Codeuctivity/net10
stesee Oct 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
15 changes: 15 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# These are supported funding model platforms

github: [Codeuctivity]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: stesee
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
20 changes: 12 additions & 8 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
name: .NET build and test
name: .NET build and test
env:
CURRENT_VERSION: 2.0.${{ github.run_number }}
LAST_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}

on:
push:
pull_request:
paths-ignore:
- '*.md'

jobs:
build:
Expand All @@ -14,13 +15,14 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Print OS name
shell: bash
run: |
Expand Down Expand Up @@ -48,13 +50,14 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand All @@ -78,13 +81,14 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
pull-requests: write

steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
Expand Down
5 changes: 2 additions & 3 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"recommendations": [
"ms-dotnettools.csharp",
"formulahendry.dotnet-test-explorer",
"github.vscode-pull-request-github"
"github.vscode-pull-request-github",
"ms-dotnettools.csdevkit"
]
}
50 changes: 33 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# SanitizeFilename

Sanitizes file and directory names in a manner that is compatible with Windows, Linux and OsX.
Sanitizes file and directory names to ensure compatibility with Windows (NTFS & exFat), Linux (ext4), and macOS (APFS).

[![.NET build and test](https://github.com/Codeuctivity/SanitizeFilename/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Codeuctivity/SanitizeFilename/actions/workflows/dotnet.yml) [![NuGet](https://img.shields.io/nuget/v/Codeuctivity.SanitizeFilename.svg)](https://www.nuget.org/packages/Codeuctivity.SanitizeFilename/) [![Donate](https://img.shields.io/static/v1?label=Paypal&message=Donate&color=informational)](https://www.paypal.com/donate?hosted_button_id=7M7UFMMRTS7UE)

Implements rules documented by [Microsoft](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions) + file name length truncation to 255 bytes - common on [many modern](https://en.wikipedia.org/wiki/Comparison_of_file_systems) file systems. Runs on any .net8 target platform.
Implements rules documented by [Microsoft](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions) + file name length truncation to 255 bytes, which is common on [many modern](https://en.wikipedia.org/wiki/Comparison_of_file_systems) file systems. Runs on any .NET platform.

## Example

Expand All @@ -25,23 +25,39 @@ Console.WriteLine($"SafeFileNameOptionalReplacementChar: {safeFileNameOptionalRe

## Rules

Restrictions of Windows, Linux and OsX are alle combined to an replacement pattern, that will sanitize any filename to be compatible with any of the OS and common filesystem restrictions.
Restrictions of Windows, Linux and macOS are all combined to an replacement pattern, that will sanitize any filename to be compatible with any of the OS and common filesystem restrictions.

| Pattern | OS that don't support pattern | OS that support pattern | Example |
| -------------------------------- | ----------------------------- | ----------------------- | ------------------ |
| Reserved keywords | Windows | Linux, OsX | CON, PRN, AUX, ... |
| Reserved chars | Linux, Windows, OsX | | '/', '\0' |
| Reserved chars windows | Windows | Linux, OsX | '\\\', '""', ... |
| Invalid trailing chars | Windows | Linux, OsX | ' ', ',' |
| Max length Linux | Linux, | Windows, OsX | 255 bytes |
| Max length | Linux, Windows, OsX | | 255 chars |
| Unpaired Unicode surrogates | OsX, Linux | Windows | U+D800 - U+DFFF |
| NotAssigned to Unicode | OsX | Linux, Windows | U+67803, ... |
| Pattern | OS that don't support pattern | OS that support pattern | Example |
| ----------------------------- | ----------------------------- | ----------------------- | ------------------ |
| Reserved keywords | Windows | Linux, macOS | CON, PRN, AUX, ... |
| Reserved chars | Linux, Windows, macOS | | '/', '\0' |
| Reserved chars windows | Windows | Linux, macOS | '\\\', '""', ... |
| Invalid trailing chars | Windows | Linux, macOS | ' ', ',' |
| Max length Linux | Linux, | [Windows, macOS](https://github.com/Codeuctivity/SanitizeFilename/blob/387103492098cd9cef0f8596a96dc6c2dfe2eba3/SanitizeFilenameTests/FilenameTests/LinuxSpecificTests.cs#L20) | 255 bytes |
| Max length | Linux, Windows, macOS | | 255 chars |
| Unpaired Unicode surrogates | macOS, Linux | Windows | U+D800 - U+DFFF |
| NotAssigned to Unicode | macOS | Linux, Windows | U+67803, ... |
| "New" Unicode (today 17+) | macOS | Linux, Windows | 🫩 (U+1FAE9), ... |

## .net framework support
## .NET framework support

- Support for legacy .NET versions will be maintained as long as it is [funded](https://github.com/sponsors/Codeuctivity).
- Support for .NET Framework 4.6.2 and higher was added in Version 1.x.
- Support for .NET Framework 4.6.2 and higher was added in Version [2.0.145](https://www.nuget.org/packages/Codeuctivity.SanitizeFilename/2.0.145).
- 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
- 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:

### Windows

```powershell
$vhdpath = [System.IO.Path]::Combine($env:TEMP, 'ExFatTestContainer.vhd')
Remove-Item $vhdpath -ErrorAction SilentlyContinue
$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 automatically create and mount a exFat drive while tests are running.
1 change: 1 addition & 0 deletions SanitizeFilename.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\dependabot.yml = .github\dependabot.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
README.md = README.md
testenvironments.json = testenvironments.json
EndProjectSection
EndProject
Global
Expand Down
2 changes: 1 addition & 1 deletion SanitizeFilename/SanitizeFilename.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
8 changes: 3 additions & 5 deletions SanitizeFilename/docs/nugetReadme.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# SanitizeFilename

Sanitizes file- and directory names in a manner that is compatible with Windows, Linux and OsX.
Sanitizes file and directory names to ensure compatibility with Windows (NTFS), Linux (ext4), and macOS (APFS).

[![.NET build and test](https://github.com/Codeuctivity/SanitizeFilename/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Codeuctivity/SanitizeFilename/actions/workflows/dotnet.yml) [![NuGet](https://img.shields.io/nuget/v/Codeuctivity.SanitizeFilename.svg)](https://www.nuget.org/packages/Codeuctivity.SanitizeFilename/) [![Donate](https://img.shields.io/static/v1?label=Paypal&message=Donate&color=informational)](https://www.paypal.com/donate?hosted_button_id=7M7UFMMRTS7UE)

Implements rules documented by [Microsoft](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions) + file name length truncation to 255 bytes - common on [many modern](https://en.wikipedia.org/wiki/Comparison_of_file_systems) file systems. Runs on any .net8 target platform.
Implements rules documented by [Microsoft](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions) + file name length truncation to 255 bytes - common on [many modern](https://en.wikipedia.org/wiki/Comparison_of_file_systems) file systems. Runs on any target platform.

## Example

Expand All @@ -21,4 +19,4 @@ Console.WriteLine($"Sanitized: {safeFileName}");
string safeFileNameOptionalReplacementChar = unsafeString.SanitizeFilename(' ');
Console.WriteLine($"SafeFileNameOptionalReplacementChar: {safeFileNameOptionalReplacementChar}");
//SafeFileNameOptionalReplacementChar: file Name
```
```
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void ShouldBehaviorOsDependentOnCreatingDirectoryWithMoreThan255Bytes()
{
if (IsRunningOnNet4x())
{
Assert.Pass("Test is not thought to be run with .net framwework / unicode 8");
Assert.Pass("Test is not thought to be run with .net framework / unicode 8");
}

var expected = !RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void ShouldThrow(string invalidDirectoryName, char replacement)

if (IsRunningOnNet4x())
{
Assert.Pass("Test is not thought to be run with .net framwework / unicode 8");
Assert.Pass("Test is not thought to be run with .net framework / unicode 8");
}

Assert.That(ex.Message, Is.EqualTo("Replacement '*' is invalid for Windows (Parameter 'replacement')"));
Expand Down Expand Up @@ -171,7 +171,7 @@ public void ShouldTruncateLongFileNamesPreserveUnicodeTextElements(string testSu
{
if (IsRunningOnNet4x())
{
Assert.Pass("Test is not thought to be run with .net framwework / unicode 8");
Assert.Pass("Test is not thought to be run with .net framework / unicode 8");
}

var invalidDirectoryName = new string('a', countOfFillingAChars) + testSuffix;
Expand Down
74 changes: 74 additions & 0 deletions SanitizeFilenameTests/ExFatTooling/AutoPlayDisabledScope.cs
Original file line number Diff line number Diff line change
@@ -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; }

/// <summary>
/// Temporarily disables AutoPlay if it is enabled. Returns true if it was changed.
/// </summary>
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;
}

/// <summary>
/// Restores the original AutoPlay setting if it was changed.
/// </summary>
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);
}
}
Loading