From afd0cc9ec34803fceae898a7d005230816fbff7b Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 09:54:00 +0000 Subject: [PATCH] StoreFileName: device stems no longer survive as the stem; IpaTrialWorker paths use it "nul.x" was rewritten to "nul.x-", whose stem is still NUL on Windows; rewritten device names now get a leading underscore, and the test asserts the stem is not reserved. IpaTrialWorker candidate directories go through StoreFileName.From; its remaining SafeName only slugs uuid-suffixed record ids. Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Core/StoreFileName.cs | 6 ++++++ tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs | 2 ++ tools/IpaTrialWorker/Program.cs | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/AquaSynth.Core/StoreFileName.cs b/src/AquaSynth.Core/StoreFileName.cs index de3d7cf..c52e883 100644 --- a/src/AquaSynth.Core/StoreFileName.cs +++ b/src/AquaSynth.Core/StoreFileName.cs @@ -38,6 +38,12 @@ public static class StoreFileName return safe; } + // "nul.x-" still has the device stem "nul"; the underscore moves the whole name off it. + if (IsDeviceStem(safe)) + { + safe = "_" + safe; + } + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(id)))[..HashLength].ToLowerInvariant(); return $"{safe}-{hash}"; } diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index 1efb857..0fd3e31 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -48,6 +48,8 @@ public sealed class StoreFileNameTests var name = StoreFileName.From(id); Assert.NotEqual(id.ToLowerInvariant(), name.ToLowerInvariant()); Assert.Matches("-[0-9a-f]{16}$", name); + var stem = name.Split('.')[0].TrimEnd(' '); + Assert.DoesNotMatch("^(?i:con|prn|aux|nul|com[0-9]|lpt[0-9])$", stem); } [Fact] diff --git a/tools/IpaTrialWorker/Program.cs b/tools/IpaTrialWorker/Program.cs index b463dec..23d7750 100644 --- a/tools/IpaTrialWorker/Program.cs +++ b/tools/IpaTrialWorker/Program.cs @@ -228,7 +228,7 @@ static async Task SongScoreAsync(Dictionary options) { var startedAt = DateTimeOffset.UtcNow; var startedStamp = Stopwatch.GetTimestamp(); - var safeCandidateId = SafeName(candidate.CandidateId); + var safeCandidateId = StoreFileName.From(candidate.CandidateId); var candidateDir = Path.Combine(candidateRoot, safeCandidateId); var audioDir = Path.Combine(candidateDir, "audio"); Directory.CreateDirectory(audioDir); @@ -2658,10 +2658,10 @@ static bool IsOfficialMusicArtifactPath(string path) } static string CandidatePatchPath(SongSummaryRow row) => - Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", SafeName(row.CandidateId), "candidate.aqua"); + Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", StoreFileName.From(row.CandidateId), "candidate.aqua"); static string CandidateAnalysisPath(SongSummaryRow row) => - Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", SafeName(row.CandidateId), "candidate-analysis.md"); + Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", StoreFileName.From(row.CandidateId), "candidate-analysis.md"); static string EvaluatorReportPath(SongSummaryRow row) => Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "evaluator-report.md");