From 3567f8b79fe57d3e8af80f15728c3373bc54cc4c Mon Sep 17 00:00:00 2001 From: SashaRX Date: Thu, 6 Aug 2026 13:53:35 +0200 Subject: [PATCH] Validate sidecar xatlas settings --- Editor/Tools/LightmapTransferTool.cs | 35 +++++++++++++---- Tests/Editor/ToolSettingsValidationTests.cs | 39 +++++++++++++++++++ .../ToolSettingsValidationTests.cs.meta | 2 + 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 Tests/Editor/ToolSettingsValidationTests.cs create mode 100644 Tests/Editor/ToolSettingsValidationTests.cs.meta diff --git a/Editor/Tools/LightmapTransferTool.cs b/Editor/Tools/LightmapTransferTool.cs index 4e680229..c66d2120 100644 --- a/Editor/Tools/LightmapTransferTool.cs +++ b/Editor/Tools/LightmapTransferTool.cs @@ -1997,7 +1997,7 @@ async Task ExecRepackImpl(List entries, bool useAsync) async Task ExecRepackCoreImpl(List entries, bool useAsync) { - uint resolvedResolution = (uint)ctx.AtlasResolution; + uint resolvedResolution = (uint)SanitizeAtlasResolution(ctx.AtlasResolution); if (ctx.RepackResolutionMode == ResolutionMode.AutoFromTexelDensity) { double area = MeshAreaHelper.ComputeTotal3DAreaMeters( @@ -2013,7 +2013,9 @@ async Task ExecRepackCoreImpl(List entries, bool useAsync) // where the resolved value can differ by an octave from the user // setting. BenchmarkRecorder.Current?.SetResolvedAtlasResolution((int)resolvedResolution); - UvtLog.Info($"[Repack] {entries.Count} meshes, res={resolvedResolution}, pad={ctx.ShellPaddingPx}, bdr={ctx.BorderPaddingPx}"); + int safeShellPadding = SanitizePadding(ctx.ShellPaddingPx); + int safeBorderPadding = SanitizePadding(ctx.BorderPaddingPx); + UvtLog.Info($"[Repack] {entries.Count} meshes, res={resolvedResolution}, pad={safeShellPadding}, bdr={safeBorderPadding}"); var validEntries = new List(); var meshCopies = new List(); foreach (var e in entries) @@ -2030,8 +2032,8 @@ async Task ExecRepackCoreImpl(List entries, bool useAsync) var opts = RepackOptions.Default; opts.resolution = resolvedResolution; - opts.padding = (uint)ctx.ShellPaddingPx; - opts.borderPadding = (uint)ctx.BorderPaddingPx; + opts.padding = (uint)safeShellPadding; + opts.borderPadding = (uint)safeBorderPadding; opts.bruteForce = ctx.XatlasBruteForce; opts.rotateCharts = ctx.XatlasRotateCharts; opts.rotateChartsToAxis = ctx.XatlasRotateChartsToAxis; @@ -4462,9 +4464,9 @@ void TryLoadSettingsFromSidecar() var data = AssetDatabase.LoadAssetAtPath(selectedSidecarPath); if (data?.toolSettings == null) return; var s = data.toolSettings; - ctx.AtlasResolution = s.atlasResolution; - ctx.ShellPaddingPx = s.shellPaddingPx; - ctx.BorderPaddingPx = s.borderPaddingPx; + ctx.AtlasResolution = SanitizeAtlasResolution(s.atlasResolution); + ctx.ShellPaddingPx = SanitizePadding(s.shellPaddingPx); + ctx.BorderPaddingPx = SanitizePadding(s.borderPaddingPx); ctx.RepackPerMesh = s.repackPerMesh; symSplitThresholdMode = Enum.IsDefined(typeof(SymmetrySplitShells.ThresholdMode), s.symmetrySplitThresholdMode) ? (SymmetrySplitShells.ThresholdMode)s.symmetrySplitThresholdMode @@ -4472,7 +4474,24 @@ void TryLoadSettingsFromSidecar() SymmetrySplitShells.CurrentThresholdMode = symSplitThresholdMode; ctx.SourceLodIndex = Mathf.Clamp(s.sourceLodIndex, 0, Mathf.Max(0, ctx.LodCount - 1)); ctx.PipeSettings.saveNewMeshAssets = s.saveNewMeshAssets; - if (!string.IsNullOrEmpty(s.savePath)) ctx.PipeSettings.savePath = s.savePath; + if (IsSafeAssetFolderPath(s.savePath)) ctx.PipeSettings.savePath = s.savePath; + } + + static int SanitizeAtlasResolution(int resolution) => Mathf.Clamp(resolution, 64, 4096); + + static int SanitizePadding(int padding) => Mathf.Clamp(padding, 0, 16); + + static bool IsSafeAssetFolderPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) return false; + string normalized = path.Replace('\\', '/').TrimEnd('/'); + if (normalized != "Assets" && !normalized.StartsWith("Assets/", StringComparison.Ordinal)) + return false; + + var segments = normalized.Split('/'); + foreach (string segment in segments) + if (segment.Length == 0 || segment == "." || segment == "..") return false; + return true; } void SaveSettingsToSidecar() diff --git a/Tests/Editor/ToolSettingsValidationTests.cs b/Tests/Editor/ToolSettingsValidationTests.cs new file mode 100644 index 00000000..980cb448 --- /dev/null +++ b/Tests/Editor/ToolSettingsValidationTests.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using NUnit.Framework; + +namespace SashaRX.UnityMeshLab.Tests +{ + public class ToolSettingsValidationTests + { + static object Invoke(string methodName, object value) + { + var method = typeof(LightmapTransferTool).GetMethod( + methodName, + BindingFlags.NonPublic | BindingFlags.Static); + Assert.IsNotNull(method, $"Missing validation method {methodName}"); + return method.Invoke(null, new[] { value }); + } + + [TestCase(-1, 64)] + [TestCase(0, 64)] + [TestCase(1024, 1024)] + [TestCase(int.MaxValue, 4096)] + public void AtlasResolution_IsClampedToSupportedRange(int input, int expected) + => Assert.AreEqual(expected, Invoke("SanitizeAtlasResolution", input)); + + [TestCase(-1, 0)] + [TestCase(2, 2)] + [TestCase(int.MaxValue, 16)] + public void Padding_IsClampedToUiRange(int input, int expected) + => Assert.AreEqual(expected, Invoke("SanitizePadding", input)); + + [TestCase("Assets/Generated", true)] + [TestCase("Assets", true)] + [TestCase("Assets/../Library", false)] + [TestCase("/tmp/output", false)] + [TestCase("Packages/output", false)] + [TestCase("", false)] + public void SavePath_MustRemainInsideAssets(string input, bool expected) + => Assert.AreEqual(expected, Invoke("IsSafeAssetFolderPath", input)); + } +} diff --git a/Tests/Editor/ToolSettingsValidationTests.cs.meta b/Tests/Editor/ToolSettingsValidationTests.cs.meta new file mode 100644 index 00000000..8b81c4e0 --- /dev/null +++ b/Tests/Editor/ToolSettingsValidationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c39a43c862ed4fef8a8e38993935b321