File: Pipelines\Internal\FileDeploymentStateManager.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREPIPELINES002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Aspire.Shared;
using Aspire.Shared.UserSecrets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Pipelines.Internal;
 
/// <summary>
/// File-based deployment state manager for deployment scenarios.
/// </summary>
internal sealed partial class FileDeploymentStateManager(
    ILogger<FileDeploymentStateManager> logger,
    IConfiguration configuration,
    IHostEnvironment hostEnvironment,
    IOptions<PipelineOptions> pipelineOptions) : DeploymentStateManagerBase<FileDeploymentStateManager>(logger)
{
    private const string ClaimedSectionsProperty = "ClaimedSections";
    private const string CurrentStateProperty = "CurrentState";
    private const string LegacyStateProperty = "LegacyState";
    private const string LegacyFallbackDisabledProperty = "LegacyFallbackDisabled";
 
    private JsonObject _currentState = [];
    private JsonObject _legacyState = [];
    private JsonObject _legacyStateSnapshot = [];
    private readonly HashSet<string> _claimedSectionNames = new(StringComparer.Ordinal);
    private bool _legacyFallbackDisabled;
 
    // Regex pattern matching only alphanumeric characters, underscores, and hyphens
    [GeneratedRegex(@"^[a-zA-Z0-9_-]+$")]
    private static partial Regex ValidEnvironmentNameRegex();
 
    /// <inheritdoc/>
    public override string? StateFilePath => GetStatePath();
 
    /// <summary>
    /// Validates that the environment name contains only allowed characters and is safe for use in file paths.
    /// </summary>
    /// <param name="environmentName">The environment name to validate.</param>
    /// <returns><c>true</c> if the environment name is valid; otherwise, <c>false</c>.</returns>
    internal static bool IsValidEnvironmentName(string environmentName)
    {
        if (string.IsNullOrEmpty(environmentName))
        {
            return false;
        }
 
        // Validate against allowed characters: [a-zA-Z0-9_-]+
        // This pattern also guards against path traversal attacks since it doesn't allow
        // dots (.), slashes (/), or backslashes (\)
        return ValidEnvironmentNameRegex().IsMatch(environmentName);
    }
 
    /// <inheritdoc/>
    protected override string? GetStatePath()
    {
        var currentStatePath = GetStatePath(configuration, GetDeploymentStatePathSha(configuration), hostEnvironment.EnvironmentName);
        if (currentStatePath is null ||
            File.Exists(currentStatePath) ||
            File.Exists(GetMigrationStatePath(currentStatePath)))
        {
            return currentStatePath;
        }
 
        var legacyStatePath = GetStatePath(configuration, configuration["AppHost:LegacyDeploymentStatePathSha256"], hostEnvironment.EnvironmentName);
        return legacyStatePath is not null && File.Exists(legacyStatePath)
            ? legacyStatePath
            : currentStatePath;
    }
 
    private string? GetCanonicalStatePath() => GetStatePath(configuration, GetDeploymentStatePathSha(configuration), hostEnvironment.EnvironmentName);
 
    private string? GetLegacyStatePath() => GetStatePath(configuration, configuration["AppHost:LegacyDeploymentStatePathSha256"], hostEnvironment.EnvironmentName);
 
    private static string? GetDeploymentStatePathSha(IConfiguration configuration)
        => configuration["AppHost:DeploymentStatePathSha256"] ?? configuration["AppHost:PathSha256"];
 
    internal static string GetMigrationStatePath(string canonicalStatePath) =>
        $"{canonicalStatePath}.migration";
 
    internal static async Task<JsonObject> LoadEffectiveStateAsync(
        string canonicalStatePath,
        string? legacyStatePath,
        CancellationToken cancellationToken = default)
    {
        JsonObject currentState;
        MigrationState migrationState;
        using (await AcquireStateLockAsync(canonicalStatePath, cancellationToken).ConfigureAwait(false))
        {
            try
            {
                migrationState = await LoadMigrationStateFileAsync(canonicalStatePath, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) when (ex is JsonException or InvalidOperationException or InvalidDataException)
            {
                return await LoadStateFileAsync(canonicalStatePath, cancellationToken).ConfigureAwait(false);
            }
 
            currentState = migrationState.CurrentState ??
                await LoadStateFileAsync(canonicalStatePath, cancellationToken).ConfigureAwait(false);
        }
 
        var (legacyFallbackDisabled, legacyStateSnapshot, claimedSectionNames, _) = migrationState;
        if (legacyFallbackDisabled)
        {
            return currentState;
        }
 
        JsonObject legacyState = [];
        // Only lock and read legacy state when the file actually exists. Acquiring the lock would
        // eagerly create the shared legacy directory and lock file for identities that never had
        // legacy state (e.g. brand-new source AppHosts), and would serialize sibling AppHosts that
        // share the legacy identity on that lock. Legacy state is immutable in this model, so an
        // existence check before locking is safe.
        if (!string.IsNullOrEmpty(legacyStatePath) && File.Exists(legacyStatePath))
        {
            try
            {
                using (await AcquireStateLockAsync(legacyStatePath, cancellationToken).ConfigureAwait(false))
                {
                    legacyState = await LoadStateFileAsync(legacyStatePath, cancellationToken).ConfigureAwait(false);
                }
            }
            catch (Exception ex) when (IsUnavailableLegacyState(ex))
            {
                // Legacy state is optional fallback data. A malformed or unreadable shared file
                // must not hide usable canonical state.
            }
        }
 
        var effectiveState = MergeState(MergeState(legacyStateSnapshot, legacyState), currentState);
        ApplyClaimedSections(effectiveState, currentState, claimedSectionNames);
 
        return effectiveState;
    }
 
    internal static JsonObject LoadEffectiveState(string canonicalStatePath, string? legacyStatePath)
    {
        JsonObject currentState;
        MigrationState migrationState;
        using (AcquireStateLock(canonicalStatePath))
        {
            try
            {
                migrationState = LoadMigrationStateFile(canonicalStatePath);
            }
            catch (Exception ex) when (ex is JsonException or InvalidOperationException or InvalidDataException)
            {
                return LoadStateFile(canonicalStatePath);
            }
 
            currentState = migrationState.CurrentState ?? LoadStateFile(canonicalStatePath);
        }
 
        var (legacyFallbackDisabled, legacyStateSnapshot, claimedSectionNames, _) = migrationState;
        if (legacyFallbackDisabled)
        {
            return currentState;
        }
 
        JsonObject legacyState = [];
        // See LoadEffectiveStateAsync: skip locking when no legacy file exists so we do not create
        // phantom legacy directories/locks or re-couple sibling AppHosts on a shared lock.
        if (!string.IsNullOrEmpty(legacyStatePath) && File.Exists(legacyStatePath))
        {
            try
            {
                using (AcquireStateLock(legacyStatePath))
                {
                    legacyState = LoadStateFile(legacyStatePath);
                }
            }
            catch (Exception ex) when (IsUnavailableLegacyState(ex))
            {
                // Legacy state is optional fallback data. A malformed or unreadable shared file
                // must not hide usable canonical state.
            }
        }
 
        var effectiveState = MergeState(MergeState(legacyStateSnapshot, legacyState), currentState);
        ApplyClaimedSections(effectiveState, currentState, claimedSectionNames);
 
        return effectiveState;
    }
 
    internal static string? GetStatePath(IConfiguration configuration, string? appHostSha, string environmentName)
    {
        if (string.IsNullOrEmpty(appHostSha))
        {
            return null;
        }
 
        var environment = environmentName.ToLowerInvariant();
 
        // Validate the environment name to ensure it only contains safe characters
        // and guard against path traversal attacks
        if (!IsValidEnvironmentName(environment))
        {
            throw new ArgumentException($"The environment name '{environment}' contains invalid characters. Environment names must only contain alphanumeric characters, underscores, and hyphens ([a-zA-Z0-9_-]+).", nameof(environmentName));
        }
 
        var aspireHome = configuration[KnownConfigNames.AspireHome];
        if (string.IsNullOrWhiteSpace(aspireHome))
        {
            aspireHome = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                ".aspire");
        }
 
        var aspireDir = Path.Combine(
            aspireHome,
            "deployments",
            appHostSha
        );
 
        return Path.Combine(aspireDir, $"{environment}.json");
    }
 
    /// <inheritdoc/>
    protected override async Task<JsonObject> LoadStateFromStorageAsync(CancellationToken cancellationToken = default)
    {
        var currentStatePath = GetCanonicalStatePath();
        var legacyStatePath = GetLegacyStatePath();
 
        if (currentStatePath is not null)
        {
            using (await AcquireStateLockAsync(currentStatePath, cancellationToken).ConfigureAwait(false))
            {
                if (!await LoadMigrationStateAsync(currentStatePath, cancellationToken).ConfigureAwait(false))
                {
                    _currentState = await LoadStateFileAsync(currentStatePath, cancellationToken).ConfigureAwait(false);
                }
            }
        }
 
        // Skip locking when no legacy file exists so we do not create a phantom legacy directory and
        // lock for identities that never had legacy state (e.g. brand-new source AppHosts), and so
        // sibling AppHosts that share the legacy identity do not serialize on that shared lock.
        if (!_legacyFallbackDisabled &&
            legacyStatePath is not null &&
            !string.Equals(currentStatePath, legacyStatePath, StringComparison.Ordinal) &&
            File.Exists(legacyStatePath))
        {
            try
            {
                using (await AcquireStateLockAsync(legacyStatePath, cancellationToken).ConfigureAwait(false))
                {
                    _legacyState = await LoadStateFileAsync(legacyStatePath, cancellationToken).ConfigureAwait(false);
                }
            }
            catch (Exception ex) when (IsUnavailableLegacyState(ex))
            {
                _legacyState = [];
            }
        }
 
        return MergeState(GetLegacyFallbackState(), _currentState);
    }
 
    /// <inheritdoc/>
    protected override JsonNode? GetSectionState(JsonObject? state, string sectionName, bool includeLegacyState)
    {
        if (!includeLegacyState)
        {
            return TryGetNestedPropertyValue(_currentState, sectionName);
        }
 
        if (_legacyFallbackDisabled ||
            _claimedSectionNames.Any(name =>
                string.Equals(name, sectionName, StringComparison.Ordinal) ||
                sectionName.StartsWith($"{name}:", StringComparison.Ordinal)))
        {
            return TryGetNestedPropertyValue(_currentState, sectionName);
        }
 
        var mergedSection = base.GetSectionState(state, sectionName, includeLegacyState)?.DeepClone();
        foreach (var claimedSectionName in _claimedSectionNames.Where(name => name.StartsWith($"{sectionName}:", StringComparison.Ordinal)))
        {
            if (mergedSection is not JsonObject mergedObject)
            {
                continue;
            }
 
            var relativeSectionName = claimedSectionName[(sectionName.Length + 1)..];
            var currentValueExists = NestedPropertyExists(_currentState, claimedSectionName);
            SetNestedNodeValue(
                mergedObject,
                relativeSectionName,
                currentValueExists ? TryGetNestedPropertyValue(_currentState, claimedSectionName)?.DeepClone() : null,
                currentValueExists);
        }
 
        return mergedSection;
    }
 
    /// <inheritdoc/>
    protected override async Task SaveStateToStorageAsync(
        JsonObject state,
        string? sectionName,
        JsonObject? originalSectionData,
        CancellationToken cancellationToken)
    {
        try
        {
            if (pipelineOptions.Value.ClearCache)
            {
                logger.LogInformation("Skipping deployment state save due to --clear-cache flag");
                return;
            }
 
            var deploymentStatePath = GetCanonicalStatePath();
            if (deploymentStatePath is null)
            {
                logger.LogWarning("Cannot save deployment state: AppHostSha is not configured");
                return;
            }
 
            using (await AcquireStateLockAsync(deploymentStatePath, cancellationToken).ConfigureAwait(false))
            {
                if (sectionName is null)
                {
                    // Normalize through the same persisted representation used by WriteStateAsync
                    // so backward-compatible flattened input and nested input have identical
                    // authoritative sidecar state.
                    _currentState = JsonFlattener.UnflattenJsonObject(JsonFlattener.FlattenJsonObject(state));
                    _legacyState = [];
                    _legacyStateSnapshot = [];
                    _claimedSectionNames.Clear();
                    _legacyFallbackDisabled = true;
                }
                else
                {
                    // Reload while holding the cross-process lock so repeated or concurrent
                    // process starts cannot overwrite sections saved by another instance.
                    if (!await LoadMigrationStateAsync(deploymentStatePath, cancellationToken).ConfigureAwait(false))
                    {
                        _currentState = await LoadStateFileAsync(deploymentStatePath, cancellationToken).ConfigureAwait(false);
                    }
                    var sectionData = TryGetNestedPropertyValue(state, sectionName) as JsonObject;
                    var legacySectionData = TryGetNestedPropertyValue(GetLegacyFallbackState(), sectionName);
                    var latestEffectiveState = MergeState(GetLegacyFallbackState(), _currentState);
                    ApplyClaimedSections(latestEffectiveState, _currentState, _claimedSectionNames);
                    var latestSectionData = NormalizeSectionData(
                        TryGetNestedPropertyValue(latestEffectiveState, sectionName));
                    var desiredSectionData = ApplyChanges(latestSectionData, originalSectionData, sectionData) as JsonObject;
                    if (TryGetNestedPropertyValue(_legacyStateSnapshot, sectionName) is null &&
                        legacySectionData is not null)
                    {
                        SetNestedNodeValue(_legacyStateSnapshot, sectionName, legacySectionData.DeepClone(), valueExists: true);
                    }
                    SetNestedPropertyValue(_currentState, sectionName, null);
                    _claimedSectionNames.RemoveWhere(name =>
                        string.Equals(name, sectionName, StringComparison.Ordinal) ||
                        name.StartsWith($"{sectionName}:", StringComparison.Ordinal));
                    ApplyStateDelta(_currentState, sectionName, legacySectionData, desiredSectionData, _claimedSectionNames);
                }
 
                await SaveMigrationStateAsync(deploymentStatePath, cancellationToken).ConfigureAwait(false);
                await WriteStateAsync(deploymentStatePath, _currentState, cancellationToken).ConfigureAwait(false);
            }
 
            logger.LogDebug("Deployment state saved to {Path}", deploymentStatePath);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to save deployment state.");
            throw;
        }
    }
 
    private static JsonObject? NormalizeSectionData(JsonNode? sectionData) =>
        sectionData switch
        {
            JsonObject sectionObject => sectionObject,
            JsonValue sectionValue when sectionValue.GetValueKind() == JsonValueKind.String =>
                new JsonObject { [""] = sectionValue.DeepClone() },
            _ => null
        };
 
    /// <inheritdoc/>
    protected override async Task ClearStateStorageAsync(CancellationToken cancellationToken)
    {
        var currentStatePath = GetCanonicalStatePath();
        if (currentStatePath is null)
        {
            _currentState = [];
            _legacyState = [];
            _legacyStateSnapshot = [];
            _claimedSectionNames.Clear();
            _legacyFallbackDisabled = true;
            return;
        }
 
        using (await AcquireStateLockAsync(currentStatePath, cancellationToken).ConfigureAwait(false))
        {
            await SaveMigrationStateAsync(
                currentStatePath,
                currentState: [],
                legacyStateSnapshot: [],
                claimedSectionNames: [],
                legacyFallbackDisabled: true,
                cancellationToken).ConfigureAwait(false);
 
            // Publish the cleared in-memory view only after the sidecar durably makes empty current
            // state authoritative. If deleting the superseded state file then fails, readers still
            // observe the committed empty sidecar state.
            _currentState = [];
            _legacyState = [];
            _legacyStateSnapshot = [];
            _claimedSectionNames.Clear();
            _legacyFallbackDisabled = true;
 
            if (File.Exists(currentStatePath))
            {
                File.Delete(currentStatePath);
                logger.LogInformation("Deployment state cleared: {Path}", currentStatePath);
            }
        }
    }
 
    private static JsonObject MergeState(JsonObject legacyState, JsonObject currentState)
    {
        var mergedState = legacyState.DeepClone().AsObject();
        MergeInto(mergedState, currentState);
        return mergedState;
 
        static void MergeInto(JsonObject target, JsonObject source)
        {
            foreach (var (name, sourceValue) in source)
            {
                if (sourceValue is JsonObject sourceObject &&
                    target[name] is JsonObject targetObject)
                {
                    MergeInto(targetObject, sourceObject);
                }
                else
                {
                    target[name] = sourceValue?.DeepClone();
                }
            }
        }
    }
 
    private JsonObject GetLegacyFallbackState() =>
        _legacyFallbackDisabled ? [] : MergeState(_legacyStateSnapshot, _legacyState);
 
    private static async Task WriteStateAsync(string statePath, JsonObject state, CancellationToken cancellationToken)
    {
        var flattenedSecrets = JsonFlattener.FlattenJsonObject(state);
        var deploymentStateDirectory = Path.GetDirectoryName(statePath)!;
        DirectoryHelper.CreateWithOwnerOnlyPermissions(deploymentStateDirectory);
        var temporaryStatePath = Path.Combine(
            deploymentStateDirectory,
            $".{Path.GetFileName(statePath)}.{Guid.NewGuid():N}.tmp");
        try
        {
            await File.WriteAllTextAsync(
                temporaryStatePath,
                flattenedSecrets.ToJsonString(UserSecretsJsonOptions.s_instance),
                cancellationToken).ConfigureAwait(false);
            File.Move(temporaryStatePath, statePath, overwrite: true);
        }
        finally
        {
            File.Delete(temporaryStatePath);
        }
    }
 
    private static FileLock AcquireStateLock(string statePath)
    {
        EnsureStateDirectoryPermissions(statePath);
        return FileLock.Acquire($"{statePath}.lock", TimeSpan.FromMinutes(5));
    }
 
    private static async Task<FileLock> AcquireStateLockAsync(
        string statePath,
        CancellationToken cancellationToken)
    {
        EnsureStateDirectoryPermissions(statePath);
        return await FileLock.AcquireAsync($"{statePath}.lock", cancellationToken).ConfigureAwait(false);
    }
 
    private static void EnsureStateDirectoryPermissions(string statePath)
    {
        var stateDirectory = Path.GetDirectoryName(statePath)!;
        DirectoryHelper.CreateWithOwnerOnlyPermissions(stateDirectory);
    }
 
    private static void ApplyStateDelta(
        JsonObject currentState,
        string sectionName,
        JsonNode? legacyValue,
        JsonNode? savedValue,
        HashSet<string> claimedSectionNames)
    {
        ApplyDelta(
            sectionName,
            legacyValue,
            baselineExists: legacyValue is not null,
            savedValue,
            savedExists: savedValue is not null);
 
        void ApplyDelta(
            string path,
            JsonNode? baseline,
            bool baselineExists,
            JsonNode? value,
            bool savedExists)
        {
            if (baseline is JsonObject baselineObject && value is JsonObject valueObject)
            {
                foreach (var propertyName in baselineObject.Select(static pair => pair.Key)
                    .Union(valueObject.Select(static pair => pair.Key), StringComparer.Ordinal))
                {
                    var baselinePropertyExists = baselineObject.TryGetPropertyValue(propertyName, out var baselineProperty);
                    var savedPropertyExists = valueObject.TryGetPropertyValue(propertyName, out var valueProperty);
                    ApplyDelta(
                        $"{path}:{propertyName}",
                        baselineProperty,
                        baselinePropertyExists,
                        valueProperty,
                        savedPropertyExists);
                }
 
                return;
            }
 
            if (baselineExists == savedExists && JsonNode.DeepEquals(baseline, value))
            {
                return;
            }
 
            SetNestedNodeValue(currentState, path, value?.DeepClone(), savedExists);
            claimedSectionNames.Add(path);
        }
    }
 
    private static JsonNode? ApplyChanges(JsonNode? latestValue, JsonNode? originalValue, JsonNode? savedValue)
    {
        if (originalValue is JsonObject originalObject &&
            savedValue is JsonObject savedObject &&
            !IsScalarSectionData(originalObject) &&
            !IsScalarSectionData(savedObject) &&
            (latestValue is not JsonObject latestObject || !IsScalarSectionData(latestObject)))
        {
            var result = latestValue?.DeepClone() as JsonObject ?? [];
            foreach (var propertyName in originalObject.Select(static pair => pair.Key)
                .Union(savedObject.Select(static pair => pair.Key), StringComparer.Ordinal))
            {
                var originalPropertyExists = originalObject.TryGetPropertyValue(propertyName, out var originalProperty);
                var savedPropertyExists = savedObject.TryGetPropertyValue(propertyName, out var savedProperty);
                var latestPropertyExists = result.TryGetPropertyValue(propertyName, out var latestProperty);
                var updatedProperty = ApplyPropertyChanges(
                    latestProperty,
                    latestPropertyExists,
                    originalProperty,
                    originalPropertyExists,
                    savedProperty,
                    savedPropertyExists);
                if (!updatedProperty.Exists)
                {
                    result.Remove(propertyName);
                }
                else
                {
                    result[propertyName] = updatedProperty.Value;
                }
            }
 
            return result;
        }
 
        return JsonNode.DeepEquals(originalValue, savedValue)
            ? latestValue?.DeepClone()
            : savedValue?.DeepClone();
 
        static (bool Exists, JsonNode? Value) ApplyPropertyChanges(
            JsonNode? latestValue,
            bool latestExists,
            JsonNode? originalValue,
            bool originalExists,
            JsonNode? savedValue,
            bool savedExists)
        {
            if (originalExists == savedExists && JsonNode.DeepEquals(originalValue, savedValue))
            {
                return (latestExists, latestValue?.DeepClone());
            }
 
            if (savedValue is JsonObject savedObject &&
                (originalValue is JsonObject || !originalExists) &&
                !IsScalarSectionData(savedObject) &&
                (originalValue is not JsonObject originalObject || !IsScalarSectionData(originalObject)) &&
                (latestValue is not JsonObject latestObject || !IsScalarSectionData(latestObject)))
            {
                return (true, ApplyChanges(latestValue, originalValue as JsonObject ?? [], savedObject));
            }
 
            return savedExists
                ? (true, savedValue?.DeepClone())
                : (false, null);
        }
    }
 
    private static bool IsScalarSectionData(JsonObject value) => value.ContainsKey(string.Empty);
 
    private static bool IsUnavailableLegacyState(Exception exception) =>
        exception is JsonException or InvalidOperationException or InvalidDataException or IOException or UnauthorizedAccessException;
 
    private static void ApplyClaimedSections(
        JsonObject effectiveState,
        JsonObject currentState,
        IEnumerable<string> claimedSectionNames)
    {
        foreach (var sectionName in claimedSectionNames)
        {
            if (NestedPropertyExists(currentState, sectionName))
            {
                SetNestedNodeValue(
                    effectiveState,
                    sectionName,
                    TryGetNestedPropertyValue(currentState, sectionName)?.DeepClone(),
                    valueExists: true);
            }
            else
            {
                SetNestedNodeValue(effectiveState, sectionName, value: null, valueExists: false);
            }
        }
    }
 
    private static bool NestedPropertyExists(JsonObject root, string path)
    {
        var segments = path.Split(':');
        JsonObject current = root;
        for (var i = 0; i < segments.Length - 1; i++)
        {
            if (!current.TryGetPropertyValue(segments[i], out var nextNode) ||
                nextNode is not JsonObject nextObject)
            {
                return false;
            }
 
            current = nextObject;
        }
 
        return current.ContainsKey(segments[^1]);
    }
 
    private static void SetNestedNodeValue(JsonObject root, string path, JsonNode? value, bool valueExists)
    {
        var segments = path.Split(':');
        var current = root;
        for (var i = 0; i < segments.Length - 1; i++)
        {
            var segment = segments[i];
            if (!current.TryGetPropertyValue(segment, out var nextNode) || nextNode is not JsonObject nextObject)
            {
                if (!valueExists)
                {
                    return;
                }
 
                nextObject = [];
                current[segment] = nextObject;
            }
 
            current = nextObject;
        }
 
        if (!valueExists)
        {
            current.Remove(segments[^1]);
        }
        else
        {
            current[segments[^1]] = value;
        }
    }
 
    private async Task<bool> LoadMigrationStateAsync(string? canonicalStatePath, CancellationToken cancellationToken)
    {
        _claimedSectionNames.Clear();
        _legacyStateSnapshot = [];
        _legacyFallbackDisabled = false;
        if (canonicalStatePath is null)
        {
            return false;
        }
 
        try
        {
            var migrationState = await LoadMigrationStateFileAsync(canonicalStatePath, cancellationToken).ConfigureAwait(false);
            _legacyFallbackDisabled = migrationState.LegacyFallbackDisabled;
            var legacyStateSnapshot = migrationState.LegacyStateSnapshot;
            _legacyStateSnapshot = legacyStateSnapshot;
            _claimedSectionNames.UnionWith(migrationState.ClaimedSectionNames);
            if (migrationState.CurrentState is not null)
            {
                _currentState = migrationState.CurrentState;
                return true;
            }
        }
        catch (Exception ex) when (ex is JsonException or InvalidOperationException or InvalidDataException)
        {
            // Migration metadata controls whether ambiguous shared state can be adopted.
            // If it is malformed, fail closed rather than reviving legacy deployment state.
            _legacyFallbackDisabled = true;
        }
 
        return false;
    }
 
    private static async Task<MigrationState> LoadMigrationStateFileAsync(
        string canonicalStatePath,
        CancellationToken cancellationToken)
    {
        var migrationStatePath = GetMigrationStatePath(canonicalStatePath);
        if (!File.Exists(migrationStatePath))
        {
            return new(false, [], [], null);
        }
 
        var migrationState = await LoadStateFileAsync(migrationStatePath, cancellationToken).ConfigureAwait(false);
        return ParseMigrationState(migrationState);
    }
 
    private static MigrationState LoadMigrationStateFile(string canonicalStatePath)
    {
        var migrationStatePath = GetMigrationStatePath(canonicalStatePath);
        return File.Exists(migrationStatePath)
            ? ParseMigrationState(LoadStateFile(migrationStatePath))
            : new(false, [], [], null);
    }
 
    private static MigrationState ParseMigrationState(JsonObject migrationState)
    {
        var legacyFallbackDisabled = migrationState[LegacyFallbackDisabledProperty]?.GetValue<bool>() ?? false;
        var legacyStateSnapshot = migrationState[LegacyStateProperty] switch
        {
            null => [],
            JsonObject legacyState => legacyState,
            _ => throw new InvalidDataException($"'{LegacyStateProperty}' must be a JSON object.")
        };
        var claimedSectionNames = migrationState[ClaimedSectionsProperty] switch
        {
            null => [],
            JsonValue claimedSectionsValue => ParseClaimedSectionNames(claimedSectionsValue),
            _ => throw new InvalidDataException($"'{ClaimedSectionsProperty}' must be a JSON string.")
        };
        var currentState = migrationState[CurrentStateProperty] switch
        {
            null => null,
            JsonValue stateValue => JsonNode.Parse(stateValue.GetValue<string>())?.AsObject()
                ?? throw new InvalidDataException($"'{CurrentStateProperty}' must contain a JSON object."),
            _ => throw new InvalidDataException($"'{CurrentStateProperty}' must be a JSON string.")
        };
 
        return new(legacyFallbackDisabled, legacyStateSnapshot, claimedSectionNames, currentState);
    }
 
    private static string[] ParseClaimedSectionNames(JsonValue claimedSectionsValue)
    {
        var claimedSectionNames = JsonSerializer.Deserialize<string?[]>(
            claimedSectionsValue.GetValue<string>());
        if (claimedSectionNames is null ||
            claimedSectionNames.Any(string.IsNullOrWhiteSpace))
        {
            throw new InvalidDataException(
                $"'{ClaimedSectionsProperty}' must contain a JSON array of non-empty strings.");
        }
 
        return [.. claimedSectionNames.Select(static sectionName => sectionName!)];
    }
 
    private Task SaveMigrationStateAsync(string canonicalStatePath, CancellationToken cancellationToken) =>
        SaveMigrationStateAsync(
            canonicalStatePath,
            _currentState,
            _legacyStateSnapshot,
            _claimedSectionNames,
            _legacyFallbackDisabled,
            cancellationToken);
 
    private static Task SaveMigrationStateAsync(
        string canonicalStatePath,
        JsonObject currentState,
        JsonObject legacyStateSnapshot,
        IEnumerable<string> claimedSectionNames,
        bool legacyFallbackDisabled,
        CancellationToken cancellationToken)
    {
        var migrationState = new JsonObject
        {
            [ClaimedSectionsProperty] = JsonSerializer.Serialize(
                claimedSectionNames.Order(StringComparer.Ordinal)),
            [CurrentStateProperty] = currentState.ToJsonString(),
            [LegacyStateProperty] = legacyStateSnapshot.DeepClone(),
            [LegacyFallbackDisabledProperty] = legacyFallbackDisabled
        };
 
        return WriteStateAsync(GetMigrationStatePath(canonicalStatePath), migrationState, cancellationToken);
    }
 
    private sealed record MigrationState(
        bool LegacyFallbackDisabled,
        JsonObject LegacyStateSnapshot,
        string[] ClaimedSectionNames,
        JsonObject? CurrentState);
}