mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Windows maps CON, PRN, AUX, NUL, COM1-9 and LPT1-9 to devices -- on many builds even with an extension appended, so a type named Con made both whole-project export and the save dialog fail with IOException '\\.\Con'. CleanUpName only checked for reserved names after re-appending the file extension, where they never match, and the save-dialog default-name helpers did not check them at all. The escape appends the underscore to the base name (con_.txt, not con.txt_) because device-name parsing ignores everything after the first dot, and is applied per path segment so reserved directory names produced by namespaces are covered too. The ILSpy.Tests.Windows fixture verifies on a real Windows filesystem that the escaped names are creatable. Assisted-by: Claude:claude-fable-5:Claude Code
129 lines
5.0 KiB
C#
129 lines
5.0 KiB
C#
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|
// substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System.IO;
|
|
|
|
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
|
|
|
|
using NUnit.Framework;
|
|
|
|
namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler;
|
|
|
|
/// <summary>
|
|
/// Windows reserves the device names below as file/directory names — both bare ("CON") and,
|
|
/// on many Windows versions, with an extension appended ("CON.cs" resolves to \\.\CON).
|
|
/// Every sanitizer in <see cref="WholeProjectDecompiler"/> must escape the base name (the part
|
|
/// before the first dot) with a trailing underscore so the decompiled output is creatable on
|
|
/// Windows (issue #3775).
|
|
/// </summary>
|
|
[TestFixture]
|
|
public sealed class ReservedFileSystemNameTests
|
|
{
|
|
static readonly string[] ReservedNames = {
|
|
"AUX", "CON", "NUL", "PRN",
|
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
|
};
|
|
|
|
static readonly char Sep = Path.DirectorySeparatorChar;
|
|
|
|
[Test]
|
|
public void CleanUpFileNameEscapesReservedNames([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.CleanUpFileName(name, ".cs"), Is.EqualTo($"{name}_.cs"));
|
|
}
|
|
|
|
[Test]
|
|
public void CleanUpFileNameEscapesReservedNamesCaseInsensitively([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
string lower = name.ToLowerInvariant();
|
|
Assert.That(WholeProjectDecompiler.CleanUpFileName(lower, ".cs"), Is.EqualTo($"{lower}_.cs"));
|
|
}
|
|
|
|
[Test]
|
|
public void CleanUpFileNameEscapesTheBaseNameOfMultiDotNames()
|
|
{
|
|
// The underscore must land on the base name: Windows device-name parsing ignores
|
|
// everything after the first dot, so "con.fig.cs_" would still resolve to \\.\CON.
|
|
Assert.That(WholeProjectDecompiler.CleanUpFileName("con.fig", ".cs"), Is.EqualTo("con_.fig.cs"));
|
|
}
|
|
|
|
[Test]
|
|
public void SanitizeFileNameEscapesReservedNames([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName(name + ".txt"), Is.EqualTo($"{name}_.txt"));
|
|
}
|
|
|
|
[Test]
|
|
public void SanitizeFileNameEscapesReservedDirectorySegments([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName(name + "/data.bin"), Is.EqualTo($"{name}_{Sep}data.bin"));
|
|
}
|
|
|
|
[Test]
|
|
public void SanitizeFileNameEscapesReservedFinalSegments([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName($"dir/{name}.png"), Is.EqualTo($"dir{Sep}{name}_.png"));
|
|
}
|
|
|
|
[Test]
|
|
public void CleanUpDirectoryNameEscapesReservedNames([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.CleanUpDirectoryName(name), Is.EqualTo(name + "_"));
|
|
}
|
|
|
|
[Test]
|
|
public void CleanUpPathEscapesReservedLeadingSegments([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.CleanUpPath(name + ".Foo"), Is.EqualTo($"{name}_{Sep}Foo"));
|
|
}
|
|
|
|
[Test]
|
|
public void CleanUpPathEscapesReservedTrailingSegments([ValueSource(nameof(ReservedNames))] string name)
|
|
{
|
|
Assert.That(WholeProjectDecompiler.CleanUpPath("Foo." + name), Is.EqualTo($"Foo{Sep}{name}_"));
|
|
}
|
|
|
|
[Test]
|
|
[TestCase("Console")]
|
|
[TestCase("CONtoso")]
|
|
[TestCase("con1")]
|
|
[TestCase("com10")]
|
|
[TestCase("lpt10")]
|
|
public void NamesMerelyStartingWithReservedNamesAreNotEscaped(string name)
|
|
{
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(WholeProjectDecompiler.CleanUpFileName(name, ".cs"), Is.EqualTo(name + ".cs"));
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName(name + ".txt"), Is.EqualTo(name + ".txt"));
|
|
Assert.That(WholeProjectDecompiler.CleanUpDirectoryName(name), Is.EqualTo(name));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void OrdinaryPathsPassThroughUnchanged()
|
|
{
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName("config.txt"), Is.EqualTo("config.txt"));
|
|
Assert.That(WholeProjectDecompiler.SanitizeFileName("dir/file.png"), Is.EqualTo($"dir{Sep}file.png"));
|
|
Assert.That(WholeProjectDecompiler.CleanUpPath("Foo.Bar"), Is.EqualTo($"Foo{Sep}Bar"));
|
|
}
|
|
}
|
|
}
|