diff --git a/src/AquaSynth.Core/IpaGestureExperiment.cs b/src/AquaSynth.Core/IpaGestureExperiment.cs index aead558..ec5e562 100644 --- a/src/AquaSynth.Core/IpaGestureExperiment.cs +++ b/src/AquaSynth.Core/IpaGestureExperiment.cs @@ -108,7 +108,7 @@ public static class IpaGestureExperiment throw new ArgumentException("At least one experiment variant is required.", nameof(variants)); } - var roundDirectory = Path.Combine(rootDirectory, SafeFileName(roundId)); + var roundDirectory = Path.Combine(rootDirectory, StoreFileName.From(roundId)); var candidateDirectory = Path.Combine(roundDirectory, "candidates"); var timelineDirectory = Path.Combine(roundDirectory, "timelines"); Directory.CreateDirectory(candidateDirectory); @@ -122,7 +122,7 @@ public static class IpaGestureExperiment { foreach (var variant in variantList) { - var candidateId = SafeFileName($"{target.Id}-{variant.Id}"); + var candidateId = StoreFileName.From($"{target.Id}-{variant.Id}"); var script = BuildCandidateScript(target, variant); var scriptPath = Path.Combine(candidateDirectory, $"{candidateId}.aqua"); File.WriteAllText(scriptPath, script, Encoding.UTF8); @@ -705,13 +705,6 @@ public static class IpaGestureExperiment private static string SafeDslName(string value) => new(value.Select(character => char.IsLetterOrDigit(character) ? character : '_').ToArray()); - private static string SafeFileName(string value) - { - var invalid = Path.GetInvalidFileNameChars().ToHashSet(); - var safe = new string(value.Select(character => invalid.Contains(character) || char.IsWhiteSpace(character) ? '-' : character).ToArray()); - return safe.Length == 0 ? "candidate" : safe; - } - private static string Format(float value) => value.ToString("0.######", CultureInfo.InvariantCulture); diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index d8012ae..2f82405 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -113,5 +113,20 @@ public sealed class StoreFileNameTests } } + [Fact] + public void IpaRoundCandidatesWithCollidingSanitisedIdsKeepDistinctFilesAndStayInRoot() + { + var root = Path.Combine(Path.GetTempPath(), $"aquasynth-ipa-sfn-{Guid.NewGuid():N}"); + var result = IpaGestureExperiment.WriteRound( + root, + "round-sfn", + [new IpaGestureExperimentTarget("a/b", "p", "voiceless_bilabial_plosive", .1f), new IpaGestureExperimentTarget("a-b", "m", "voiced_bilabial_nasal", .1f)], + [new IpaGestureExperimentVariant("base")]); + + Assert.Equal(2, result.Candidates.Select(c => c.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + Assert.All(result.Candidates, c => Assert.StartsWith(Path.GetFullPath(root), Path.GetFullPath(c.ScriptPath))); + Assert.Throws(() => IpaGestureExperiment.WriteRound(root, "..", [new IpaGestureExperimentTarget("a", "p", "voiceless_bilabial_plosive")], [new IpaGestureExperimentVariant("base")])); + } + private static string UriPath(string uri) => new Uri(uri).LocalPath; }