diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 710618b..e430717 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: | 10.0.x @@ -27,13 +27,13 @@ jobs: - name: Test on Ubuntu (no build) run: dotnet test --no-build --verbosity normal --configuration Release - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-artifacts path: SanitizeFilename/bin/Release/ retention-days: 1 - name: Upload test artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-artifacts path: SanitizeFilenameTests/bin/Release/ @@ -51,14 +51,14 @@ jobs: steps: - uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: | 10.0.x - name: Restore dependencies run: dotnet restore - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: test-artifacts path: SanitizeFilenameTests/bin/Release/ @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest needs: [build, test] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: release-artifacts path: SanitizeFilename/bin/Release/ @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest needs: [build, test] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: release-artifacts path: SanitizeFilename/bin/Release/ diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6736c09..9ca7111 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v10 + - uses: actions/stale@v11 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Stale issue message' diff --git a/SanitizeFilename/SanitizeFilename.cs b/SanitizeFilename/SanitizeFilename.cs index 07a8d26..56dd20f 100644 --- a/SanitizeFilename/SanitizeFilename.cs +++ b/SanitizeFilename/SanitizeFilename.cs @@ -245,7 +245,8 @@ private static string InternalSanitizeReservedFileNames(string filename, string { foreach (var reservedFileName in ReservedWindowsFileNames) { - filename = filename.Replace(reservedFileName, replacement, true, CultureInfo.InvariantCulture); + if (string.Equals(filename, reservedFileName, StringComparison.OrdinalIgnoreCase)) + return replacement; } return filename; @@ -255,7 +256,7 @@ private static string InternalSanitizeReservedFileNamePrefix(string filename, st { foreach (var reservedFileNamePrefix in ReservedWindowsFileNamesWithExtension) if (filename.StartsWith(reservedFileNamePrefix, true, CultureInfo.InvariantCulture)) - filename = string.Concat(replacement, filename.AsSpan(0, reservedFileNamePrefix.Length)); + filename = string.Concat(replacement, filename.AsSpan(reservedFileNamePrefix.Length - 1)); return filename; } diff --git a/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs b/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs index f9d8ed3..d6fa609 100644 --- a/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs +++ b/SanitizeFilenameTests/FilenameTests/SanitizeFilenamesTests.cs @@ -22,10 +22,15 @@ public void TearDown() private static readonly string[] InvalidWindowsFileNames = ["invalidfilename", "invalid\"filename", "invalid/filename", "invalid\\filename", "invalid|filename", "invalid?filename", "invalid*filename",]; private static readonly string[] ReservedWindowsFileNames = ["con", "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM\u00B9", "COM\u00B2", "COM\u00B3", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT\u00B9", "LPT\u00B2", "LPT\u00B3", "endingWithSpace ", "endingWithPeriod."]; - private static readonly string[] ReservedWindowsFileNamePrefixUsed = ["con.txt", "CON.txt", "PRN.txt", "AUX.txt", "NUL.txt", "COM0.txt", "COM1.txt", "COM2.txt", "COM3.txt", "COM4.txt", "COM5.txt", "COM6.txt", "COM7.txt", "COM8.txt", "COM9.txt", "COM\u00B9.txt", "COM\u00B2.txt", "COM\u00B3.txt", "LPT0.txt", "LPT1.txt", "LPT2.txt", "LPT3.txt", "LPT4.txt", "LPT5.txt", "LPT6.txt", "LPT7.txt", "LPT8.txt", "LPT9.txt", "LPT\u00B9.txt", "LPT\u00B2.txt", "LPT\u00B3.txt"]; + private static readonly string[] ReservedWindowsFileNamePrefixUsed = ["con.txt", "CON.txt", "PRN.txt", "AUX.txt", "NUL.txt", "COM0.txt", "COM1.txt", "COM2.txt", "COM3.txt", "COM4.txt", "COM5.txt", "COM6.txt", "COM7.txt", "COM8.txt", "COM9.txt", "COM\u00B9.txt", "COM\u00B2.txt", "COM\u00B3.txt", "LPT0.txt", "LPT1.txt", "LPT2.txt", "LPT3.txt", "LPT4.txt", "LPT5.txt", "LPT6.txt", "LPT7.txt", "LPT8.txt", "LPT9.txt", "LPT\u00B9.txt", "LPT\u00B2.txt", "LPT\u00B3.txt", "NUL.tar.gz", "nul.tar.gz", "CON.tar.gz"]; private static readonly string[] ValidFileNames = ["validFileName"]; + // Regression coverage for https://github.com/Codeuctivity/SanitizeFilename/issues/46 : + // reserved device names must only be sanitized when they are the whole base file name, + // not merely a substring anywhere within an otherwise legal name. + private static readonly string[] ValidFileNamesContainingReservedNamesAsSubstring = ["ICON", "ICON.txt", "FALCON", "RECON.doc", "beacon", "silicon Valley", "CONSOLE.txt", "CONSTRUCTION", "NULLABLE.cs", "AUXILIARY.doc", "COMPANY.docx", "DISCOMFORT", "ENCOMPASS", "LPTOP.txt", "COM10", "LPT10"]; + [Test] [TestCaseSource(nameof(ValidFileNames))] public void ShouldNotTouchASaneFilename(string validFilename) @@ -35,6 +40,24 @@ public void ShouldNotTouchASaneFilename(string validFilename) Assert.That(sanitizedFilename, Is.EqualTo(validFilename)); } + [Test] + [TestCaseSource(nameof(ValidFileNamesContainingReservedNamesAsSubstring))] + public void ShouldNotTouchFileNamesContainingReservedNamesAsSubstring(string validFilename) + { + var sanitizedFilename = validFilename.SanitizeFilename(); + + Assert.That(sanitizedFilename, Is.EqualTo(validFilename)); + } + + [Test] + [TestCase("CONfile*Name", "CONfile_Name")] + public void ShouldOnlyReplaceTheInvalidCharNotTheReservedNameSubstring(string invalidFilename, string expectedOutcome) + { + var sanitizedFilename = invalidFilename.SanitizeFilename(); + + Assert.That(sanitizedFilename, Is.EqualTo(expectedOutcome)); + } + [Test] [TestCase("CO*", 'N', "N")] public void ShouldSanitizeEdgeCase(string invalidFilename, char replacement, string expectedOutcome)