fix(client): validate suppressed tree objects

This commit is contained in:
2026-09-30 09:45:53 +09:00
parent b4e38fc2c2
commit e1998fb8fd
2 changed files with 72 additions and 10 deletions
+25 -10
View File
@@ -1707,6 +1707,29 @@ fn object_ownership_revision(state: &Session, key: u64) -> Option<RequestId> {
.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<u64, ClientError> {
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)
+47
View File
@@ -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]