/* * Copyright (c) 2026 James Maes * SPDX-License-Identifier: MIT */ using MarkdownViewr.Windows.Core.Documents; using MarkdownViewr.Windows.Core.Themes; namespace MarkdownViewr.Windows.Core.Tests; /// The written-out Windows name rules, and the two places that depend on them. public sealed class WindowsFileNamesTests { private static readonly string[] s_devices = [ "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³", ]; /// A device is a device in any letter case, with any extension, and with spaces before the extension or at the end. [Fact] public void IsReservedDeviceName_EveryDeviceSpelling_IsReserved() { Assert.All(Spellings(), spelling => { Assert.True(WindowsFileNames.IsReservedDeviceName(spelling), spelling); Assert.True(WindowsFileNames.IsReservedDeviceName(spelling + ".png"), spelling); Assert.True(WindowsFileNames.IsReservedDeviceName(spelling + ".tar.md"), spelling); Assert.True(WindowsFileNames.IsReservedDeviceName(spelling + " .png"), spelling); Assert.True(WindowsFileNames.IsReservedDeviceName(spelling + " "), spelling); }); Assert.Equal(96, Spellings().Count()); } /// Names that only resemble a device stay ordinary file names; only spaces are ignored before the extension. [Theory] [InlineData("")] [InlineData("console.md")] [InlineData("COM10.png")] [InlineData("COM")] [InlineData("LPT.txt")] [InlineData("xCON.png")] [InlineData("CON-notes.md")] [InlineData("CON-.md")] [InlineData("null.md")] [InlineData(" NUL.png")] [InlineData("NUL\u00A0.png")] [InlineData("NUL\t.png")] [InlineData("readme.NUL")] [InlineData("COM⁴.png")] [InlineData("CLOCK$")] [InlineData("CLOCK$.txt")] public void IsReservedDeviceName_OrdinaryName_IsNotReserved(string name) => Assert.False(WindowsFileNames.IsReservedDeviceName(name)); /// A missing name is a contract error, not an ordinary file. [Fact] public void IsReservedDeviceName_Null_RejectsInput() => Assert.Throws(() => WindowsFileNames.IsReservedDeviceName(null!)); /// The printable characters Windows forbids in a name; control characters are refused separately by each caller. [Fact] public void InvalidCharacters_Constant_ListsThePrintableForbiddenCharacters() => Assert.Equal("\"*/:<>?\\|", new string(WindowsFileNames.InvalidCharacters.Order().ToArray())); /// No image or linked-document request can name a device in any spelling, as the file or as a folder on the way to it. [Fact] public void GetRelativePath_DeviceSegment_IsDenied() { var policy = new DocumentResourcePolicy(); Assert.All(Spellings(), spelling => { string segment = Uri.EscapeDataString(spelling); Assert.Null(policy.GetImageRelativePath(new Uri(policy.DocumentUri, segment + ".png"))); Assert.Null(policy.GetImageRelativePath(new Uri(policy.DocumentUri, segment + "%20.png"))); Assert.Null(policy.GetImageRelativePath(new Uri(policy.DocumentUri, segment + "/picture.png"))); Assert.Null(policy.GetDocumentRelativePath(new Uri(policy.DocumentUri, segment + ".md"))); Assert.Null(policy.GetDocumentRelativePath(new Uri(policy.DocumentUri, segment + ".tar.md"))); Assert.Null(policy.GetDocumentRelativePath(new Uri(policy.DocumentUri, "docs/" + segment + "/child.md"))); }); Assert.Equal("console.png", policy.GetImageRelativePath(new Uri(policy.DocumentUri, "console.png"))); Assert.Equal("docs/com10/child.md", policy.GetDocumentRelativePath(new Uri(policy.DocumentUri, "docs/com10/child.md"))); } /// A theme named after a device, in any case, never becomes the suggested export file name. [Fact] public void SuggestedFileName_EveryDeviceName_IsPrefixed() { Assert.All(s_devices, device => { Assert.Equal("theme-" + device.ToLowerInvariant() + ".json", ThemeJson.SuggestedFileName(ThemeCatalog.Find(null).Rename(device))); Assert.Equal("theme-" + device.ToLowerInvariant() + ".json", ThemeJson.SuggestedFileName(ThemeCatalog.Find(null).Rename(device.ToLowerInvariant()))); Assert.Equal("theme-" + device.ToLowerInvariant() + ".backup.json", ThemeJson.SuggestedFileName(ThemeCatalog.Find(null).Rename(device + ".backup"))); }); } /// Leading and trailing dots and hyphens are dropped from a suggested name, and a name made only of them falls back to a plain one. [Theory] [InlineData("-Edge-", "edge.json")] [InlineData(".Dotted.", "dotted.json")] [InlineData("--", "theme.json")] [InlineData(" spaced ", "spaced.json")] [InlineData("-con-", "theme-con.json")] public void SuggestedFileName_LeadingAndTrailingSeparators_AreTrimmed(string name, string expected) => Assert.Equal(expected, ThemeJson.SuggestedFileName(ThemeCatalog.Find(null).Rename(name))); /// Each device name in upper, lower and mixed case. private static IEnumerable Spellings() => s_devices.SelectMany(device => new[] { device, device.ToLowerInvariant(), char.ToUpperInvariant(device[0]) + device[1..].ToLowerInvariant() }); }