feat: unify object observation API
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::CStr;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
@@ -17,8 +16,8 @@ use wip_host::{
|
||||
};
|
||||
use wip_interface as wip;
|
||||
use wip_protocol::{
|
||||
FetchInterfaceRequest, FetchInterfaceResponse, FetchTreeRequest, FetchTreeResponse, Object,
|
||||
ObjectTreeNode, ProtocolError, ProtocolErrorCode,
|
||||
FetchInterfaceRequest, FetchInterfaceResponse, ObjectObservation, ObserveRequest,
|
||||
ObserveResponse, ProtocolError, ProtocolErrorCode,
|
||||
};
|
||||
|
||||
use crate::interfaces::{
|
||||
@@ -223,37 +222,60 @@ impl Projection {
|
||||
.protocol_response()
|
||||
}
|
||||
|
||||
pub(crate) fn fetch_tree(
|
||||
pub(crate) fn observe(
|
||||
&self,
|
||||
context: &ExecutionContext,
|
||||
request: &FetchTreeRequest,
|
||||
) -> Result<FetchTreeResponse, ProtocolError> {
|
||||
request: &ObserveRequest,
|
||||
) -> Result<ObserveResponse, ProtocolError> {
|
||||
request.validate().map_err(|_| ProtocolError {
|
||||
code: ProtocolErrorCode::InvalidRequest,
|
||||
message: "invalid fetch_tree request".into(),
|
||||
message: "invalid observe request".into(),
|
||||
})?;
|
||||
if request.depth > self.limits.max_tree_depth || request.limit > self.limits.max_tree_nodes
|
||||
{
|
||||
if request.depth > self.limits.max_observation_depth {
|
||||
return Err(ProjectionError::Resource.protocol());
|
||||
}
|
||||
|
||||
let root = self
|
||||
.resolve(context, &request.path)
|
||||
.map_err(ProjectionError::protocol)?;
|
||||
let root_object = self
|
||||
.publication(root.clone(), context.clone())
|
||||
.protocol_object();
|
||||
let mut objects = vec![root_object];
|
||||
let mut children: Vec<Vec<usize>> = vec![Vec::new()];
|
||||
let mut queue = VecDeque::from([(0_usize, root, 0_u32)]);
|
||||
let mut truncated = false;
|
||||
let mut node_count = 0_u32;
|
||||
let response = self.observe_entry(context, root, 0, request.depth, &mut node_count)?;
|
||||
request
|
||||
.validate_response(&response)
|
||||
.map_err(|_| ProtocolError {
|
||||
code: ProtocolErrorCode::Internal,
|
||||
message: "Host produced an invalid observation response".into(),
|
||||
})?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
while let Some((parent_index, parent, depth)) = queue.pop_front() {
|
||||
check_context(context).map_err(ProjectionError::protocol)?;
|
||||
if parent.kind != EntryKind::Directory {
|
||||
continue;
|
||||
}
|
||||
let directory_fd = reopen(parent.fd(), OFlags::RDONLY | OFlags::DIRECTORY)
|
||||
fn observe_entry(
|
||||
&self,
|
||||
context: &ExecutionContext,
|
||||
entry: Entry,
|
||||
depth: u32,
|
||||
maximum_depth: u32,
|
||||
node_count: &mut u32,
|
||||
) -> Result<ObjectObservation, ProtocolError> {
|
||||
check_context(context).map_err(ProjectionError::protocol)?;
|
||||
*node_count = node_count.saturating_add(1);
|
||||
if *node_count > self.limits.max_observation_nodes {
|
||||
return Err(ProjectionError::Resource.protocol());
|
||||
}
|
||||
|
||||
let object = self
|
||||
.publication(entry.clone(), context.clone())
|
||||
.protocol_object();
|
||||
if depth == maximum_depth {
|
||||
return Ok(ObjectObservation {
|
||||
object,
|
||||
children: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut children = Vec::new();
|
||||
if entry.kind == EntryKind::Directory {
|
||||
let directory_fd = reopen(entry.fd(), OFlags::RDONLY | OFlags::DIRECTORY)
|
||||
.map_err(ProjectionError::protocol)?;
|
||||
let mut buffer = [MaybeUninit::<u8>::uninit(); 8192];
|
||||
let mut directory = RawDir::new(directory_fd, &mut buffer);
|
||||
@@ -271,50 +293,25 @@ impl Projection {
|
||||
if name.contains('/') || name.is_empty() || name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
let child = match self.open_child(context, parent.fd(), name) {
|
||||
let child = match self.open_child(context, entry.fd(), name) {
|
||||
Ok(child) => child,
|
||||
Err(ProjectionError::NotFound) => continue,
|
||||
Err(error) => return Err(error.protocol()),
|
||||
};
|
||||
if depth == request.depth || objects.len() == request.limit as usize {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
let child_index = objects.len();
|
||||
objects.push(
|
||||
self.publication(child.clone(), context.clone())
|
||||
.protocol_object(),
|
||||
);
|
||||
children.push(Vec::new());
|
||||
children[parent_index].push(child_index);
|
||||
if child.kind == EntryKind::Directory {
|
||||
queue.push_back((child_index, child, depth + 1));
|
||||
}
|
||||
}
|
||||
if truncated && objects.len() == request.limit as usize {
|
||||
break;
|
||||
children.push(self.observe_entry(
|
||||
context,
|
||||
child,
|
||||
depth + 1,
|
||||
maximum_depth,
|
||||
node_count,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
fn build(index: usize, objects: &[Object], children: &[Vec<usize>]) -> ObjectTreeNode {
|
||||
ObjectTreeNode {
|
||||
object: objects[index].clone(),
|
||||
children: children[index]
|
||||
.iter()
|
||||
.map(|child| build(*child, objects, children))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
let response = FetchTreeResponse {
|
||||
root: build(0, &objects, &children),
|
||||
truncated,
|
||||
};
|
||||
response.validate_for(request).map_err(|_| ProtocolError {
|
||||
code: ProtocolErrorCode::Internal,
|
||||
message: "Host produced an invalid tree response".into(),
|
||||
})?;
|
||||
Ok(response)
|
||||
Ok(ObjectObservation {
|
||||
object,
|
||||
children: Some(children),
|
||||
})
|
||||
}
|
||||
|
||||
fn publication(
|
||||
@@ -809,7 +806,7 @@ mod tests {
|
||||
use wip_host::{
|
||||
CallErrorKind, CallRequest, ExecutionContext, InterfaceTarget, ObjectProvider, Target,
|
||||
};
|
||||
use wip_protocol::{FetchTreeRequest, ProtocolErrorCode, Value};
|
||||
use wip_protocol::{ObserveRequest, ProtocolErrorCode, Value};
|
||||
|
||||
use super::{FailureInjection, FlockOperation, Projection};
|
||||
use crate::{AccessMode, ResourceLimits};
|
||||
@@ -850,23 +847,29 @@ mod tests {
|
||||
.expect("create symlink");
|
||||
let projection = projection(temporary.path(), AccessMode::ReadOnly);
|
||||
|
||||
let root = projection.fetch("/").expect("fetch root");
|
||||
let root = projection.observe_object("/").expect("observe root");
|
||||
assert_eq!(root.name, "");
|
||||
assert_eq!(root.description.as_deref(), Some("Filesystem directory"));
|
||||
assert!(root.interfaces.is_empty());
|
||||
assert!(root.validator.is_none());
|
||||
|
||||
let file = projection.fetch("/file.txt").expect("fetch file");
|
||||
let file = projection
|
||||
.observe_object("/file.txt")
|
||||
.expect("observe file");
|
||||
assert_eq!(file.interfaces.len(), 1);
|
||||
assert!(file.validator.is_some());
|
||||
assert!(file.r#ref.is_none());
|
||||
|
||||
let directory = projection.fetch("/dir").expect("fetch directory");
|
||||
let directory = projection
|
||||
.observe_object("/dir")
|
||||
.expect("observe directory");
|
||||
assert!(directory.interfaces.is_empty());
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let link = projection.fetch("/link").expect("fetch link itself");
|
||||
let link = projection
|
||||
.observe_object("/link")
|
||||
.expect("observe link itself");
|
||||
assert_eq!(
|
||||
link.description.as_deref(),
|
||||
Some("Symbolic link (not followed)")
|
||||
@@ -874,7 +877,7 @@ mod tests {
|
||||
assert_eq!(link.interfaces.len(), 1);
|
||||
assert_eq!(
|
||||
projection
|
||||
.fetch("/link/shadow")
|
||||
.observe_object("/link/shadow")
|
||||
.expect_err("must not follow")
|
||||
.kind(),
|
||||
CallErrorKind::NotFound
|
||||
@@ -892,18 +895,21 @@ mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
projection.fetch("/missing").expect_err("missing").kind(),
|
||||
projection
|
||||
.observe_object("/missing")
|
||||
.expect_err("missing")
|
||||
.kind(),
|
||||
CallErrorKind::NotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_are_bounded_and_tree_truncation_is_exact() {
|
||||
fn reads_are_bounded_and_observation_is_complete() {
|
||||
let temporary = tempdir().expect("temporary root");
|
||||
fs::write(temporary.path().join("a"), b"abcdef").expect("write file");
|
||||
fs::write(temporary.path().join("b"), b"b").expect("write file");
|
||||
let projection = projection(temporary.path(), AccessMode::ReadOnly);
|
||||
let file = projection.fetch("/a").expect("fetch file");
|
||||
let file = projection.observe_object("/a").expect("observe file");
|
||||
let result = call(
|
||||
&projection,
|
||||
"/a",
|
||||
@@ -919,30 +925,17 @@ mod tests {
|
||||
assert_eq!(result.result, Value::Bytes(b"cde".to_vec()));
|
||||
|
||||
let complete = projection
|
||||
.fetch_tree(
|
||||
.observe(
|
||||
&ExecutionContext::new(),
|
||||
&FetchTreeRequest {
|
||||
&ObserveRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
limit: 3,
|
||||
},
|
||||
)
|
||||
.expect("complete tree");
|
||||
assert!(!complete.truncated);
|
||||
assert_eq!(complete.root.children.len(), 2);
|
||||
|
||||
let limited = projection
|
||||
.fetch_tree(
|
||||
&ExecutionContext::new(),
|
||||
&FetchTreeRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
limit: 2,
|
||||
},
|
||||
)
|
||||
.expect("limited tree");
|
||||
assert!(limited.truncated);
|
||||
assert_eq!(limited.root.children.len(), 1);
|
||||
.expect("complete observation");
|
||||
let children = complete.children.as_ref().expect("root children observed");
|
||||
assert_eq!(children.len(), 2);
|
||||
assert!(children.iter().all(|child| child.children.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -951,7 +944,7 @@ mod tests {
|
||||
let path = temporary.path().join("file");
|
||||
fs::write(&path, b"old").expect("write file");
|
||||
let projection = projection(temporary.path(), AccessMode::ReadWrite);
|
||||
let observed = projection.fetch("/file").expect("fetch file");
|
||||
let observed = projection.observe_object("/file").expect("observe file");
|
||||
assert_eq!(observed.interfaces.len(), 2);
|
||||
let write_interface = observed.interfaces[1].clone();
|
||||
|
||||
@@ -981,7 +974,9 @@ mod tests {
|
||||
assert_eq!(stale.kind(), CallErrorKind::ValidatorMismatch);
|
||||
assert_eq!(fs::read(&path).expect("read replacement"), b"replacement");
|
||||
|
||||
let current = projection.fetch("/file").expect("fetch replacement");
|
||||
let current = projection
|
||||
.observe_object("/file")
|
||||
.expect("observe replacement");
|
||||
let result = call(
|
||||
&projection,
|
||||
"/file",
|
||||
@@ -1001,7 +996,7 @@ mod tests {
|
||||
let path = temporary.path().join("file");
|
||||
fs::write(&path, b"old").expect("write file");
|
||||
let projection = projection(temporary.path(), AccessMode::ReadWrite);
|
||||
let observed = projection.fetch("/file").expect("fetch file");
|
||||
let observed = projection.observe_object("/file").expect("observe file");
|
||||
let request = || {
|
||||
call(
|
||||
&projection,
|
||||
@@ -1029,25 +1024,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_directory_stops_after_proving_truncation() {
|
||||
fn large_directory_fails_without_partial_observation() {
|
||||
let temporary = tempdir().expect("temporary root");
|
||||
for index in 0..2_000 {
|
||||
fs::write(temporary.path().join(format!("entry-{index}")), b"x")
|
||||
.expect("write directory entry");
|
||||
}
|
||||
let projection = projection(temporary.path(), AccessMode::ReadOnly);
|
||||
let tree = projection
|
||||
.fetch_tree(
|
||||
let error = projection
|
||||
.observe(
|
||||
&ExecutionContext::new(),
|
||||
&FetchTreeRequest {
|
||||
&ObserveRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
limit: 2,
|
||||
},
|
||||
)
|
||||
.expect("bounded tree");
|
||||
assert!(tree.truncated);
|
||||
assert_eq!(tree.root.children.len(), 1);
|
||||
.expect_err("complete observation exceeds node budget");
|
||||
assert_eq!(error.code, ProtocolErrorCode::ResourceLimitExceeded);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -1065,22 +1058,22 @@ mod tests {
|
||||
let projection = projection(temporary.path(), AccessMode::ReadOnly);
|
||||
|
||||
let tree = projection
|
||||
.fetch_tree(
|
||||
.observe(
|
||||
&ExecutionContext::new(),
|
||||
&FetchTreeRequest {
|
||||
&ObserveRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
limit: 10,
|
||||
},
|
||||
)
|
||||
.expect("list root");
|
||||
assert_eq!(tree.root.children.len(), 1);
|
||||
assert_eq!(tree.root.children[0].object.name, "socket");
|
||||
let children = tree.children.as_ref().expect("root children observed");
|
||||
assert_eq!(children.len(), 1);
|
||||
assert_eq!(children[0].object.name, "socket");
|
||||
assert_eq!(
|
||||
tree.root.children[0].object.description.as_deref(),
|
||||
children[0].object.description.as_deref(),
|
||||
Some("Unsupported filesystem entry")
|
||||
);
|
||||
assert!(tree.root.children[0].object.interfaces.is_empty());
|
||||
assert!(children[0].object.interfaces.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1091,12 +1084,11 @@ mod tests {
|
||||
let cancellation = wip_host::CancellationToken::new();
|
||||
cancellation.cancel();
|
||||
let error = projection
|
||||
.fetch_tree(
|
||||
.observe(
|
||||
&ExecutionContext::new().with_cancellation(cancellation),
|
||||
&FetchTreeRequest {
|
||||
&ObserveRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
limit: 2,
|
||||
},
|
||||
)
|
||||
.expect_err("cancelled traversal");
|
||||
@@ -1108,7 +1100,7 @@ mod tests {
|
||||
let temporary = tempdir().expect("temporary root");
|
||||
let projection = projection(temporary.path(), AccessMode::ReadOnly);
|
||||
let error = projection
|
||||
.fetch("/bad\0name")
|
||||
.observe_object("/bad\0name")
|
||||
.expect_err("NUL cannot name a backend entry");
|
||||
assert_eq!(error.kind(), CallErrorKind::NotFound);
|
||||
}
|
||||
@@ -1144,7 +1136,7 @@ mod tests {
|
||||
};
|
||||
let projection = Projection::open(temporary.path(), AccessMode::ReadOnly, limits)
|
||||
.expect("open projection");
|
||||
let observed = projection.fetch("/file").expect("fetch file");
|
||||
let observed = projection.observe_object("/file").expect("observe file");
|
||||
let started = std::time::Instant::now();
|
||||
let error = call(
|
||||
&projection,
|
||||
|
||||
Reference in New Issue
Block a user