apply method
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;
}