From 5a77dc8e83b28e8eca48818ffca4755dd75c5f59 Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 7 Nov 2026 10:38:28 +0000 Subject: [PATCH] AquaLower names lowered .dsp files through StoreFileName; pin Windows forms; note limits Co-Authored-By: Claude Sonnet 5.5 --- src/AquaSynth.Core/StoreFileName.cs | 5 ++++ .../AquaSynth.Dsl.Tests/StoreFileNameTests.cs | 27 +++++++++++++++++++ tools/AquaLower/Program.cs | 5 ++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/AquaSynth.Core/StoreFileName.cs b/src/AquaSynth.Core/StoreFileName.cs index 0de02d1..9dcac70 100644 --- a/src/AquaSynth.Core/StoreFileName.cs +++ b/src/AquaSynth.Core/StoreFileName.cs @@ -13,6 +13,11 @@ namespace AquaSynth.Dsl; /// rewritten names always do, so the two sets cannot meet; distinct ids stay distinct up to a 64-bit /// hash. The result is never a path: no separators, never "." or "..". /// +/// +/// Not handled: symlinks and junctions already inside a store are followed, and Windows 8.3 short names +/// were not verified. An id with an uppercase letter is rewritten, so a batch directory named for such +/// an id gains a hash suffix. +/// public static class StoreFileName { private const int MaxStem = 100; diff --git a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs index b083f6f..8da1f52 100644 --- a/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs +++ b/tests/AquaSynth.Dsl.Tests/StoreFileNameTests.cs @@ -160,6 +160,33 @@ public sealed class StoreFileNameTests Assert.Equal(Path.GetFullPath(root), Path.GetDirectoryName(full)); } + [Theory] + [InlineData("\\\\host\\share\\x")] + [InlineData("C:x")] + [InlineData("a:b")] + [InlineData("x.")] + [InlineData("x ")] + [InlineData("x. ")] + public void WindowsAbsoluteAndTrailingDotOrSpaceIdsStayContainedAndPlain(string id) + { + var root = Path.Combine(Path.GetTempPath(), "artifact-root-probe", "artifacts"); + var name = StoreFileName.From(id); + Assert.Matches("^[A-Za-z0-9._-]+$", name); + Assert.False(name.EndsWith('.') || name.EndsWith(' ')); + Assert.Equal(Path.GetFullPath(root), Path.GetDirectoryName(Path.GetFullPath(StoreFileName.Combine(root, id)))); + } + + [Theory] + [InlineData("../x")] + [InlineData("a/b")] + [InlineData("name:with:colons")] + public void LoweredDspFileNameStaysInTheOutputDirectory(string name) + { + var outDir = Path.Combine(Path.GetTempPath(), "aqualower-probe", "out"); + var file = StoreFileName.From(name) + ".dsp"; + Assert.Equal(Path.GetFullPath(outDir), Path.GetDirectoryName(Path.GetFullPath(Path.Combine(outDir, file)))); + } + [Fact] public void BareDotDotBatchIdIsRefused() => Assert.Throws(() => StoreFileName.Combine(Path.GetTempPath(), "..")); diff --git a/tools/AquaLower/Program.cs b/tools/AquaLower/Program.cs index e2d4948..8ee3fd7 100644 --- a/tools/AquaLower/Program.cs +++ b/tools/AquaLower/Program.cs @@ -47,8 +47,9 @@ foreach (var spec in args.Skip(2)) warnings.AddRange(export.Warnings); } else source = text; - File.WriteAllText(Path.Combine(outDir, name + ".dsp"), source); - entries.Add(new { name, origin = rel.Replace('\\', '/'), kind = path.EndsWith(".aqua", StringComparison.Ordinal) ? "aqua" : "faust", dsp = name + ".dsp", warnings }); + var dspFile = StoreFileName.From(name) + ".dsp"; + File.WriteAllText(Path.Combine(outDir, dspFile), source); + entries.Add(new { name, origin = rel.Replace('\\', '/'), kind = path.EndsWith(".aqua", StringComparison.Ordinal) ? "aqua" : "faust", dsp = dspFile, warnings }); Console.WriteLine($"{name}: {source.Length} chars from {rel}, {warnings.Count} warnings"); } File.WriteAllText(Path.Combine(outDir, "catalog.json"), JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true }));