apply method

SemanticTree? apply(
  1. List<int> bytes
)

Applies one wire payload and returns the reconstructed tree, or null if the payload is malformed or a patch arrives before any full frame (a desync the caller should ignore, keeping the last good tree).

Implementation

SemanticTree? apply(List<int> bytes) {
  if (bytes.length > maxWirePayloadLength) return null;
  final Object? decoded;
  try {
    decoded = jsonDecode(utf8.decode(bytes));
  } on FormatException {
    return null;
  }
  if (decoded is! Map) return null;
  if (decoded['v'] != semanticsWireVersion) return null;

  late final Map<String, Map<String, Object?>> candidate;
  late final String candidateRootId;
  late final bool candidateWasFull;
  final candidateChangedIds = <String>{};
  final candidateRemovedIds = <String>{};

  switch (decoded['mode']) {
    case 'full':
      final nodes = decoded['nodes'];
      if (nodes is! List) return null;
      if (nodes.length > maxSemanticWireNodes) return null;
      final root = decoded['root'];
      if (root is! String || !_semanticWireIdIsValid(root)) return null;
      candidate = <String, Map<String, Object?>>{};
      for (final node in nodes) {
        final normalized = _normalizeNode(node);
        if (normalized == null) return null;
        candidate[normalized.id] = normalized.node;
      }
      candidateRootId = root;
      candidateWasFull = true;
    case 'patch':
      if (!_hasState) return null;
      switch (_applyContentPatch(decoded)) {
        case _ContentPatchApplied(:final tree):
          return tree;
        case _ContentPatchRejected():
          return null;
        case _NotAContentPatch():
          break;
      }
      candidate = Map<String, Map<String, Object?>>.of(_flat);
      final set = decoded['set'];
      if (set != null) {
        if (set is! List || set.length > maxSemanticWireNodes) return null;
        for (final node in set) {
          final normalized = _normalizeNode(node);
          if (normalized == null) return null;
          candidate[normalized.id] = normalized.node;
          candidateChangedIds.add(normalized.id);
        }
      }
      final removed = decoded['removed'];
      if (removed != null) {
        if (removed is! List || removed.length > maxSemanticWireNodes) {
          return null;
        }
        for (final id in removed) {
          if (id is! String || !_semanticWireIdIsValid(id)) return null;
          candidate.remove(id);
          candidateRemovedIds.add(id);
        }
      }
      final root = decoded['root'];
      if (root != null &&
          (root is! String || !_semanticWireIdIsValid(root))) {
        return null;
      }
      candidateRootId = root is String ? root : _rootId;
      candidateWasFull = false;
    default:
      return null;
  }

  if (candidate.length > maxSemanticWireNodes) return null;
  var edgeCount = 0;
  for (final node in candidate.values) {
    final childIds = node['childIds'];
    if (childIds is List<String>) {
      edgeCount += childIds.length;
      if (edgeCount > maxSemanticWireEdges) return null;
    }
  }

  // Build against the candidate map without touching the retained mirror.
  // [visited] is GLOBAL for this reconstruction and is never popped. A repeat
  // of a node that itself has children is a cycle or branching DAG, and is
  // rejected before it can expand exponentially under the old path-local
  // cycle guard. Repeated leaves remain representable: Fleury intentionally
  // tolerates duplicate semantic ids long enough for inspection tooling to
  // diagnose them as ambiguous, and a leaf cannot recurse. The occurrence
  // count below keeps even a very wide repeated-leaf fan-out bounded.
  final visited = <String>{};
  var invalidGraph = false;
  var depthLimited = false;
  var nestedNodeCount = 0;
  Map<String, Object?>? nest(String id, int depth) {
    if (depth >= maxSemanticTreeDepth) {
      depthLimited = true;
      return null;
    }
    final flat = candidate[id];
    if (flat == null) return null;
    nestedNodeCount++;
    if (nestedNodeCount > maxSemanticWireNodes) {
      invalidGraph = true;
      return null;
    }
    final json = <String, Object?>{
      for (final entry in flat.entries)
        if (entry.key != 'childIds') entry.key: entry.value,
    };
    final childIds = flat['childIds'];
    if (!visited.add(id)) {
      if (childIds is List<String> && childIds.isNotEmpty) {
        invalidGraph = true;
        return null;
      }
      return json;
    }
    if (childIds is List<String>) {
      final children = <Map<String, Object?>>[];
      for (final childId in childIds) {
        final child = nest(childId, depth + 1);
        if (invalidGraph) return null;
        if (child != null) children.add(child);
      }
      if (children.isNotEmpty) json['children'] = children;
    }
    return json;
  }

  final nested = nest(candidateRootId, 0);
  if (nested == null || invalidGraph) return null;
  final reachableNodeByteLengths = <String, int>{};
  var reachableNodeBytes = 0;
  for (final id in visited) {
    final node = candidate[id]!;
    final retained = _flat[id];
    final length = identical(node, retained)
        ? _flatNodeByteLengths[id] ?? _semanticNodeWireLength(node)
        : _semanticNodeWireLength(node);
    reachableNodeByteLengths[id] = length;
    reachableNodeBytes += length;
  }
  if (_semanticFullPayloadLength(
        candidateRootId,
        nodeBytes: reachableNodeBytes,
        nodeCount: visited.length,
      ) >
      maxWirePayloadLength) {
    return null;
  }
  final SemanticTree tree;
  try {
    tree = SemanticInspectionSnapshot.fromJson(<String, Object?>{
      'schemaVersion': SemanticInspectionSnapshot.currentSchemaVersion,
      'root': nested,
    }).toSemanticTree();
  } on Object {
    // A reconstructed node was missing a required field (e.g. a corrupt or
    // hostile patch dropped a node's role) or had a field of the wrong shape.
    // Reject this frame rather than throw; no retained state has changed, so
    // the last good tree remains a valid base for the next patch.
    return null;
  }

  // Commit only after reconstruction AND inspection parsing both succeed.
  // Keeping reachable nodes only prevents an attacker from accumulating
  // orphaned nodes through a long stream of bounded patches.
  final previousIds = _flat.keys.toSet();
  final reachable = <String, Map<String, Object?>>{
    for (final id in visited) id: candidate[id]!,
  };
  _flat
    ..clear()
    ..addAll(reachable);
  _flatNodeByteLengths
    ..clear()
    ..addAll(reachableNodeByteLengths);
  _flatBytes = reachableNodeBytes;
  _index(tree);
  _contentPatchesSafe = !depthLimited && _flat.keys.every(_nodes.containsKey);
  _contentReplacements = null;
  final rootChanged = candidateRootId != _rootId;
  _rootId = candidateRootId;
  _hasState = true;
  _wasFull = candidateWasFull;
  if (candidateWasFull) {
    _changedIds = visited.toList(growable: false);
    _removedIds = const <String>[];
  } else {
    if (rootChanged) candidateChangedIds.add(candidateRootId);
    final removed = <String>{
      ...candidateRemovedIds,
      for (final id in previousIds)
        if (!visited.contains(id)) id,
    }..removeAll(visited);
    _changedIds = candidateChangedIds
        .where(visited.contains)
        .toList(growable: false);
    _removedIds = removed.toList(growable: false);
  }
  return tree;
}