StoreFileName: device stems no longer survive as the stem; IpaTrialWorker paths use it

"nul.x" was rewritten to "nul.x-<hash>", 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 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-07 09:54:00 +00:00
commit afd0cc9ec3
3 changed files with 11 additions and 3 deletions

View file

@ -38,6 +38,12 @@ public static class StoreFileName
return safe;
}
// "nul.x-<hash>" 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}";
}

View file

@ -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]

View file

@ -228,7 +228,7 @@ static async Task SongScoreAsync(Dictionary<string, string> 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");