diff --git a/crates/wip-client/src/lib.rs b/crates/wip-client/src/lib.rs index aa3d166..2465727 100644 --- a/crates/wip-client/src/lib.rs +++ b/crates/wip-client/src/lib.rs @@ -1707,6 +1707,29 @@ fn object_ownership_revision(state: &Session, key: u64) -> Option { .max() } +fn validate_object_consistency(state: &Session, object: &Object) -> Result<(), ClientError> { + let Some(reference) = &object.r#ref else { + return Ok(()); + }; + let Some(existing) = state + .refs + .get(reference) + .and_then(|key| state.objects.get(key)) + else { + return Ok(()); + }; + if existing.object.validator.is_some() + && existing.object.validator == object.validator + && existing.object != *object + { + return Err(ClientError::new( + ClientErrorKind::InvalidResponse, + "object representation changed for the same ref and validator", + )); + } + Ok(()) +} + fn insert_object( state: &mut Session, path: &str, @@ -1714,6 +1737,7 @@ fn insert_object( maximum: usize, revision: RequestId, ) -> Result { + validate_object_consistency(state, &object)?; ensure_entry_slot(state, path, maximum)?; let existing_path_entry = state.entries.get(path).cloned(); let existing_path_key = existing_path_entry @@ -1731,16 +1755,6 @@ fn insert_object( }); let key = if let Some(reference) = &object.r#ref { if let Some(key) = state.refs.get(reference).copied() { - if let Some(existing) = state.objects.get(&key) - && existing.object.validator.is_some() - && existing.object.validator == object.validator - && existing.object != object - { - return Err(ClientError::new( - ClientErrorKind::InvalidResponse, - "object representation changed for the same ref and validator", - )); - } key } else { make_object_slot(state, maximum, replaceable_key)? @@ -2017,6 +2031,7 @@ fn apply_tree( } for (path, object, _) in &nodes { + validate_object_consistency(state, object)?; let newer_owner = state .entries .get(path) diff --git a/crates/wip-client/tests/runtime.rs b/crates/wip-client/tests/runtime.rs index 3b539b1..d2c877f 100644 --- a/crates/wip-client/tests/runtime.rs +++ b/crates/wip-client/tests/runtime.rs @@ -327,6 +327,53 @@ fn normalized_aliases_preserve_newer_request_ownership() { client.object(&session, "/b/shared").unwrap().validator, Some(b"newest".to_vec()) ); + + observe_object( + &mut client, + &session, + "/child", + object("child", Some("child-ref"), b"stable"), + ); + let older_tree = client.prepare_tree(&session, "/", 1, 4).unwrap(); + let newer_child = client.prepare_fetch(&session, "/child").unwrap(); + client + .complete( + newer_child.id, + fetch_response("/child", &object("child", Some("child-ref"), b"stable")), + ) + .unwrap(); + let mut contradictory = object("child", Some("child-ref"), b"stable"); + contradictory.description = Some("same-path contradiction".into()); + let root_request = FetchTreeRequest { + path: "/".into(), + depth: 1, + limit: 4, + }; + let error = client + .complete( + older_tree.id, + encode_fetch_tree_response( + &root_request, + &FetchTreeResponse { + root: ObjectTreeNode { + object: object("", None, b"root"), + children: vec![ObjectTreeNode { + object: contradictory, + children: vec![], + }], + }, + truncated: false, + }, + wire_limits(), + ) + .unwrap(), + ) + .unwrap_err(); + assert_eq!(error.kind, ClientErrorKind::InvalidResponse); + assert_eq!( + client.object(&session, "/child").unwrap().state, + ObservationState::Fresh + ); } #[test]