Skip to content

[Code Quality] ExportServiceCommand — ReservedPortRegex misses COM0/LPT0 (and Unicode-superscript COM¹/LPT¹/etc.) per current Microsoft naming docs #900

@Christophe-Rogiers

Description

@Christophe-Rogiers

Severity: Info (export-time validation, edge case — but the reserved-names list is supposed to be exhaustive and currently isn't)

File / line: src/Servy.CLI/Commands/ExportServiceCommand.cs lines 24–36

Code:

private static readonly HashSet<string> ReservedDeviceNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "CON", "PRN", "AUX", "NUL"          // <-- complete (just 4 entries)
};

private static readonly Regex ReservedPortRegex = new Regex(
    @"^(COM|LPT)[1-9]$",                 // <-- only [1-9], excludes 0 and superscripts
    RegexOptions.Compiled | RegexOptions.IgnoreCase,
    AppConfig.InputRegexTimeout);

What's wrong: The list of Windows reserved device names used by the export safety check is incomplete relative to the current Microsoft naming-a-file documentation. Microsoft's authoritative list is:

CON, PRN, AUX, NUL, 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³

The regex ^(COM|LPT)[1-9]$ rejects COM1COM9 and LPT1LPT9 but accepts COM0/LPT0 (CharRange [1-9] excludes 0). It also accepts the Unicode superscript variants COM¹, COM², COM³, LPT¹, LPT², LPT³ — these were added to the official reserved list in newer Microsoft docs.

A user running servy-cli export --name MyService --config xml --path "C:\backup\COM0.xml" would pass Path.GetFileNameWithoutExtension(...) == "COM0", fall through both ReservedDeviceNames.Contains(...) and the regex match, and the export would proceed. On most Windows builds the actual File.WriteAllText would either succeed (writing a regular file with a confusing name) or — on certain builds and certain APIs — silently redirect the write to a device handle. Either way the validation isn't doing what its comment promises.

Suggested fix: Either widen the regex to include 0 and the superscripts, or fold the whole thing into a single canonical helper. A minimal regex fix:

private static readonly Regex ReservedPortRegex = new Regex(
    @"^(COM|LPT)([0-9]|¹|²|³)$",
    RegexOptions.Compiled | RegexOptions.IgnoreCase,
    AppConfig.InputRegexTimeout);

Or, since the full list is small and finite, drop the regex entirely and put everything in the HashSet — easier to audit against the docs:

private static readonly HashSet<string> ReservedDeviceNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "CON", "PRN", "AUX", "NUL",
    "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³",
};

// ...later:
if (ReservedDeviceNames.Contains(fileName))
    throw new ArgumentException($"Security Alert: '{fileName}' is a reserved Windows device name and cannot be used.");

The HashSet form also drops the RegexMatchTimeoutException fallback at line 180–184, which is overkill for a name-equality check.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions