Skip to content

Commit 95a6236

Browse files
Merge pull request #133 from PassivePicasso/path-resolution-safety
Path Components, EnvironmentVariable, EnvironmentSpecialFolder, Tests, Safety
2 parents 5bdc762 + 797f45a commit 95a6236

29 files changed

Lines changed: 1374 additions & 20 deletions

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
which carries no version resource outside Windows
1515
* The **Installed Unity Games** window reports these games' Unity version instead of
1616
falling back to scanning file headers
17+
* Two `PathComponent`s resolve per-machine locations so a shared PathReference no longer
18+
has to be hand-edited per collaborator —
19+
[SpecialFolder](Editor/Core/Paths/Components/SpecialFolder.cs) and
20+
[EnvironmentVariable](Editor/Core/Paths/Components/EnvironmentVariable.cs)
21+
* `SpecialFolder` selects an `Environment.SpecialFolder`, so `ApplicationData` resolves
22+
to `%APPDATA%`, `~/.config` or `~/Library/Application Support` from one asset
23+
* `EnvironmentVariable` looks a variable up by name with an optional fallback, so an
24+
unset variable is reported rather than becoming a literal path segment
25+
* Neither parses `%VAR%` or `$VAR`, so the same asset resolves on Windows, Linux and
26+
macOS
1727

1828
### Fixes
1929

@@ -35,6 +45,20 @@
3545
executor sub-assets, preserving each executor's enabled state
3646
* Equal-priority executors are ordered by type name, so the saved order no longer
3747
depends on Unity's sub-asset ordering
48+
* Path resolution reports authoring mistakes instead of failing opaquely — new
49+
[PathAssembler](Editor/Core/Paths/PathAssembler.cs) and
50+
[PathResolutionScope](Editor/Core/Paths/PathResolutionScope.cs) sit behind
51+
`PathReference.GetPath`
52+
* A cycle through `OutputReference` or a `Resolver` token names the chain that caused
53+
it, rather than recursing until `StackOverflowException` terminates the Editor
54+
* Null, invalid, drive-relative and misplaced rooted segments are refused and name the
55+
`PathComponent` that produced them, so a segment can no longer silently discard the
56+
components before it
57+
* Two `PathReference` assets sharing a name report both assets instead of surfacing as
58+
a dictionary key collision
59+
* [PathReference](Editor/Core/Paths/PathReference.cs) `ElementTemplate` scaffolds
60+
`GetPathInternal`, so generated `PathComponent`s compile — it previously declared an
61+
override of the non-virtual `GetPath`
3862

3963
### Tests
4064

@@ -47,6 +71,20 @@
4771
* Added [PlayerDataResolverTests](Tests/Editor/PlayerDataResolverTests.cs) covering player
4872
layout selection and the bundle branch against a synthesized UnityFS bundle, so the
4973
compressed layout is exercised without a multi-megabyte fixture
74+
* Added coverage for the path component system across
75+
[PathComponentTests](Tests/Editor/PathComponentTests.cs),
76+
[PathComponentFileSystemTests](Tests/Editor/PathComponentFileSystemTests.cs),
77+
[PathReferenceCombineTests](Tests/Editor/PathReferenceCombineTests.cs),
78+
[PathReferenceCycleTests](Tests/Editor/PathReferenceCycleTests.cs) and
79+
[PathReferenceAssetTests](Tests/Editor/PathReferenceAssetTests.cs)
80+
* Pins the authoring patterns that ship in `Templates/`, including `Constant("..")` and
81+
a rooted first component, so future validation cannot outlaw them
82+
* Records that `ManifestName` and `ManifestVersion` return null rather than entering
83+
their reported-error paths, and that `FindFile` and `FindDirectory` lose the
84+
underlying cause outside pipeline execution
85+
* [PathComponentEnvironmentTests](Tests/Editor/PathComponentEnvironmentTests.cs) cover both
86+
new components, including the unset-variable diagnostic and that shell syntax in a
87+
variable name is looked up verbatim rather than unwrapped
5088

5189
## 9.4.3
5290

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using ThunderKit.Core.Pipelines;
3+
using UnityEngine;
4+
5+
namespace ThunderKit.Core.Paths.Components
6+
{
7+
public class EnvironmentVariable : PathComponent
8+
{
9+
[Tooltip("Name only, without % or $. Names differ per platform; prefer SpecialFolder for a portable location")]
10+
public string VariableName;
11+
12+
[Tooltip("Used when the variable is not set. Leave empty to require it")]
13+
public string Fallback;
14+
15+
protected override string GetPathInternal(PathReference output, Pipeline pipeline)
16+
{
17+
if (string.IsNullOrEmpty(VariableName))
18+
throw new InvalidOperationException(
19+
$"{PathDiagnostics.Link(output, this)} has no variable name assigned.");
20+
21+
var value = Environment.GetEnvironmentVariable(VariableName);
22+
if (!string.IsNullOrEmpty(value))
23+
return value;
24+
25+
if (!string.IsNullOrEmpty(Fallback))
26+
return Fallback;
27+
28+
throw new InvalidOperationException(
29+
$"{PathDiagnostics.Link(output, this)} requires environment variable \"{VariableName}\", " +
30+
"which is not set for the Editor process. The Editor reads the environment it was " +
31+
"launched with, so a newly added variable needs an Editor restart.");
32+
}
33+
}
34+
}

Editor/Core/Paths/Components/EnvironmentVariable.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Core/Paths/Components/OutputReference.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
22
using ThunderKit.Core.Pipelines;
3-
using UnityEditor;
4-
using UnityEngine.Networking;
53

64
namespace ThunderKit.Core.Paths.Components
75
{
@@ -16,14 +14,12 @@ protected override string GetPathInternal(PathReference output, Pipeline pipelin
1614
}
1715
catch (NullReferenceException nre)
1816
{
19-
var pathReferencePath = UnityWebRequest.EscapeURL(AssetDatabase.GetAssetPath(output));
20-
var pathReferenceLink = $"[{output.name}.{name}.reference](assetlink://{pathReferencePath})";
17+
var pathReferenceLink = PathDiagnostics.Link(output, this, ".reference");
2118
throw new InvalidOperationException($"Error {pathReferenceLink} is unassigned or null", nre);
2219
}
2320
catch (Exception e)
2421
{
25-
var pathReferencePath = UnityWebRequest.EscapeURL(AssetDatabase.GetAssetPath(output));
26-
var pathReferenceLink = $"[{output.name}.{name}.reference({reference.name})](assetlink://{pathReferencePath})";
22+
var pathReferenceLink = PathDiagnostics.Link(output, this, $".reference({reference.name})");
2723
throw new InvalidOperationException($"Error Invoking PathReference: {pathReferenceLink}", e);
2824
}
2925
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using ThunderKit.Core.Pipelines;
3+
using UnityEngine;
4+
5+
namespace ThunderKit.Core.Paths.Components
6+
{
7+
public class SpecialFolder : PathComponent
8+
{
9+
[Tooltip("Resolved per platform. ApplicationData: %APPDATA%, ~/.config, ~/Library/Application Support")]
10+
public Environment.SpecialFolder Folder = Environment.SpecialFolder.ApplicationData;
11+
12+
protected override string GetPathInternal(PathReference output, Pipeline pipeline)
13+
{
14+
string folderPath;
15+
try
16+
{
17+
folderPath = Environment.GetFolderPath(Folder);
18+
}
19+
catch (ArgumentException argumentException)
20+
{
21+
throw new InvalidOperationException(
22+
$"{PathDiagnostics.Link(output, this)} is set to {(int)Folder}, which is not a known special folder.",
23+
argumentException);
24+
}
25+
26+
if (string.IsNullOrEmpty(folderPath))
27+
throw new InvalidOperationException(
28+
$"{PathDiagnostics.Link(output, this)} requested {Folder}, which has no location on this platform.");
29+
30+
return folderPath;
31+
}
32+
}
33+
}

Editor/Core/Paths/Components/SpecialFolder.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Core/Paths/PathAssembler.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using ThunderKit.Core.Pipelines;
5+
6+
namespace ThunderKit.Core.Paths
7+
{
8+
// Every PathComponent result converges here, so this is the only place that can
9+
// enforce that a component cannot silently corrupt the assembled path.
10+
internal static class PathAssembler
11+
{
12+
static readonly char[] InvalidPathChars = Path.GetInvalidPathChars();
13+
14+
// Drive-relative segments ("C:") only carry that meaning where paths are DOS
15+
// shaped; ':' is an ordinary filename character elsewhere.
16+
static readonly bool DosPaths = Path.DirectorySeparatorChar == '\\';
17+
18+
public static string Assemble(PathReference output, Pipeline pipeline, ComposableElement[] data)
19+
{
20+
if (data == null)
21+
return string.Empty;
22+
23+
var components = data.OfType<PathComponent>().ToArray();
24+
var segments = new string[components.Length];
25+
for (int index = 0; index < components.Length; index++)
26+
{
27+
var component = components[index];
28+
segments[index] = Validated(output, component, component.GetPath(output, pipeline), index);
29+
}
30+
31+
if (segments.Length == 0)
32+
return string.Empty;
33+
34+
return Path.Combine(segments);
35+
}
36+
37+
// Ordered so that Path.IsPathRooted and Path.Combine, which reject invalid
38+
// characters on the .NET Framework profile, are never reached with them.
39+
static string Validated(PathReference output, PathComponent component, string segment, int index)
40+
{
41+
if (segment == null)
42+
throw new InvalidOperationException(
43+
$"{PathDiagnostics.Link(output, component)} returned null. " +
44+
"A PathComponent must return a path segment or an empty string.");
45+
46+
if (segment.IndexOfAny(InvalidPathChars) >= 0)
47+
throw new InvalidOperationException(
48+
$"{PathDiagnostics.Link(output, component)} returned \"{segment}\", " +
49+
"which contains characters that are not valid in a path.");
50+
51+
if (DosPaths && IsDriveRelative(segment))
52+
throw new InvalidOperationException(
53+
$"{PathDiagnostics.Link(output, component)} returned the drive-relative path \"{segment}\", " +
54+
"which resolves against the current directory of that drive rather than its root. " +
55+
"Add a trailing separator to mean the drive root.");
56+
57+
if (index > 0 && Path.IsPathRooted(segment))
58+
throw new InvalidOperationException(
59+
$"{PathDiagnostics.Link(output, component)} returned the rooted path \"{segment}\" at position {index}. " +
60+
"Combining a rooted segment discards every component before it; " +
61+
"move it to the first position or make it relative.");
62+
63+
return segment;
64+
}
65+
66+
static bool IsDriveRelative(string segment)
67+
{
68+
if (segment.Length < 2 || segment[1] != ':' || !char.IsLetter(segment[0]))
69+
return false;
70+
71+
if (segment.Length == 2)
72+
return true;
73+
74+
return segment[2] != '\\' && segment[2] != '/';
75+
}
76+
}
77+
}

Editor/Core/Paths/PathAssembler.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using UnityEditor;
2+
using UnityEngine.Networking;
3+
using Object = UnityEngine.Object;
4+
5+
namespace ThunderKit.Core.Paths
6+
{
7+
// Pipeline logs render assetlink:// URIs as clickable links to the offending
8+
// asset, so every path resolution failure is reported through this shape.
9+
internal static class PathDiagnostics
10+
{
11+
public static string Link(PathReference output, PathComponent component, string suffix = null)
12+
{
13+
return Link(output, $"{Describe(output)}.{Describe(component)}{suffix}");
14+
}
15+
16+
public static string Link(PathReference output, string label)
17+
{
18+
var assetPath = UnityWebRequest.EscapeURL(AssetDatabase.GetAssetPath(output));
19+
return $"[{label}](assetlink://{assetPath})";
20+
}
21+
22+
static string Describe(Object target)
23+
{
24+
return target ? target.name : "<unassigned>";
25+
}
26+
}
27+
}

Editor/Core/Paths/PathDiagnostics.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)