using ZB.MOM.WW.Configuration; namespace ZB.MOM.WW.MxGateway.Server.Configuration; /// /// Shared filesystem-path validation primitives used by more than one options validator /// ( and ). /// Both the auth credential store and the Galaxy snapshot are written by the running gateway /// process, so both must reject paths the host cannot use — the rules live here once so the two /// validators cannot drift. /// internal static class GatewayConfigPathRules { /// /// Fails validation when is not an absolute (rooted) path on the /// host running the validator. Security-sensitive paths (the auth DB, the self-signed /// private key, the Galaxy snapshot) must be absolute: a non-rooted value silently resolves /// against the launch working directory, so the store moves with the CWD and can leak into the /// source tree. Rooting is checked with — the current /// OS — so a Windows drive/UNC literal on a Unix host fails fast at startup rather than being /// blessed and then written as a junk-named relative file (the SEC-01/SEC-33 mechanism). Reject /// rather than auto-root; silent relocation of a credential store is worse than a boot error. /// Blank is handled by the caller's required-field check and is not treated as non-rooted here. /// /// The configured path value. /// The failure message to record when the value is not rooted. /// The validation builder accumulating failures. public static void AddIfNotRooted(string? value, string message, ValidationBuilder builder) { if (string.IsNullOrWhiteSpace(value)) { return; } if (!Path.IsPathRooted(value)) { builder.Add(message); } } /// /// Fails validation when is non-blank but not a syntactically valid /// filesystem path (as judged by ). Blank values are the /// caller's required-field concern and pass here. /// /// The configured path value. /// The failure message to record when the value is not a valid path. /// The validation builder accumulating failures. public static void AddIfInvalidPath(string? value, string message, ValidationBuilder builder) { if (string.IsNullOrWhiteSpace(value)) { return; } if (!TryGetFullPath(value, out _)) { builder.Add(message); } } /// /// Fails validation when resolves to a location inside /// — the directory the application runs from. /// /// /// /// Rooted is not the same as safe, and this is the rule that closes the gap. /// stops a store drifting with the working directory, but an /// absolute path inside the app directory passes it cleanly — and that is what failed /// in production on 2026-08-09. The upgrade procedure renames the app directory to /// Server.bak.* and unpacks a new one; a store living there is renamed away with it, the /// process then creates a fresh empty one at the same path, and nothing reports an error. All /// API keys were lost and no gRPC client could authenticate for two days. The deploy itself was /// executed correctly — the binaries were the point of the rename, and the store was collateral. /// /// /// The same shape catches the dev-side symptom: a store under the content root lands in the /// source tree, which is how mxgateway-secrets.db once tripped the repository's /// tree-hygiene test. /// /// /// Comparison is case-insensitive only on Windows. On a case-insensitive macOS volume this can /// miss a violation that differs only in case, which is a missed warning in dev; assuming /// case-insensitivity on Linux would instead reject a legitimate path, and a false startup /// abort is the worse failure. /// /// /// The configured path value. /// The application content root to test against. /// The failure message to record when the value is under the content root. /// The validation builder accumulating failures. public static void AddIfUnderContentRoot( string? value, string? contentRoot, string message, ValidationBuilder builder) { if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(contentRoot)) { return; } // A malformed path is AddIfInvalidPath's message to report; staying silent here keeps one // bad value from producing two failures that say different things about the same mistake. if (!TryGetFullPath(value, out string fullValue) || !TryGetFullPath(contentRoot, out string fullRoot)) { return; } fullRoot = fullRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); StringComparison comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; // The separator is load-bearing: a bare prefix test would also match a sibling directory // whose name merely starts with the root's ("/srv/app" against "/srv/app-data"). if (string.Equals(fullValue, fullRoot, comparison) || fullValue.StartsWith(fullRoot + Path.DirectorySeparatorChar, comparison)) { builder.Add(message); } } private static bool TryGetFullPath(string value, out string fullPath) { try { fullPath = Path.GetFullPath(value); return true; } catch (ArgumentException) { fullPath = string.Empty; return false; } catch (NotSupportedException) { fullPath = string.Empty; return false; } catch (PathTooLongException) { fullPath = string.Empty; return false; } catch (IOException) { fullPath = string.Empty; return false; } } }