//! External non-root scope regression coverage, sharing the memory backend. use super::*; struct ScopedObject { inner: Object, reference: Option, } impl WipObject for ScopedObject { fn name(&self) -> &str { self.inner.name() } fn reference(&self) -> Option<&str> { self.reference.as_deref() } fn validator(&self) -> Option<&Validator> { self.inner.validator() } fn requires_validator(&self, _: &InterfaceRef, operation: &str) -> bool { operation == "read" } } impl FileRead for ScopedObject { fn read(&self, offset: Option, length: Option) -> wip::OperationResult { FileRead::read(&self.inner, offset, length) } } struct ScopedComposition { shared: Arc, associations: BTreeMap, Scope)>, interfaces: Vec>, with_refs: bool, } impl Composition for ScopedComposition { type Object = ScopedObject; fn scope( &self, path: &str, entry: &Entry, context: &ExecutionContext, ) -> Result, HostError> { assert!(matches!( self.shared.transaction.try_lock(), Err(TryLockError::WouldBlock) )); assert_eq!(path, entry.path); self.shared.record("scope", path, context); assert_eq!( entry.validator, Entry::snapshot(entry.path.clone(), entry.node.clone()).validator ); let nodes = self.shared.nodes.lock().unwrap(); // Self-scope also requires current membership; ordinary pinned-leaf // execution intentionally need not check its path mapping. Ok(self.associations.get(path).and_then(|(identity, scope)| { (Arc::ptr_eq(identity, &entry.node) && nodes .get(path) .is_some_and(|node| Arc::ptr_eq(node, identity))) .then(|| scope.clone()) })) } fn publish( &self, entry: Entry, context: &ExecutionContext, ) -> Result, HostError> { assert!(matches!( self.shared.transaction.try_lock(), Err(TryLockError::WouldBlock) )); self.shared.record("scoped_publish", &entry.path, context); let path = entry.path.clone(); let regular = entry.kind() == EntryKind::Regular; let reference = self .with_refs .then(|| format!("published-object-{}", entry.node.id)); let mut publication = PublishedObject::new(ScopedObject { inner: Object { entry, custom: self.interfaces[0].reference().clone(), shared: self.shared.clone(), context: context.clone(), }, reference, }); // Containers expose no interfaces but remain valid fetch subjects. if regular { for interface in &self.interfaces { if interface .reference() .protocol_reference() .validate_for_path(&path) .is_ok() { publication = publication.expose(interface).unwrap(); } } } Ok(publication) } } struct ScopedPolicy; impl ProjectionPolicy for ScopedPolicy { type Principal = Principal; fn object_visible(&self, principal: &Principal, entry: &Entry) -> bool { !entry.hidden && (!matches!(principal, Principal::Limited) || entry.path != "/secret") } fn interface_visible(&self, principal: &Principal, interface: &InterfaceRef) -> bool { !(matches!(principal, Principal::Limited) && interface.protocol_reference().scope == "/added") } } type ScopedEngine = Projection; fn scoped_engine(shared: Arc, with_refs: bool) -> ScopedEngine { configured_engine(shared, with_refs, None) } fn configured_engine( shared: Arc, with_refs: bool, interface_validator: Option, ) -> ScopedEngine { let mut registry = InterfaceRegistry::new(); let mut associations = BTreeMap::new(); let mut interfaces = Vec::new(); for path in ["/", "/secret", "/secret/nested", "/added"] { let scope = Scope::new(path).unwrap(); let validator = if path == "/secret" { interface_validator.clone() } else { None }; interfaces.push( registry .register( &scope, "file.read", wip_fs_server::FileReadDefinition, validator, ) .unwrap(), ); associations.insert(path.into(), (shared.node(path), scope)); } Projection::new( MemoryBackend::new(shared.clone()), registry, ScopedComposition { shared, associations, interfaces, with_refs, }, ScopedPolicy, ResourceLimits::default(), ) } fn reference(scope: &str) -> wip_protocol::InterfaceReference { wip_protocol::InterfaceReference { scope: scope.into(), name: "file.read".into(), } } fn request(path: &str, scope: &str, scope_ref: Option) -> CallOperationRequest { CallOperationRequest { target: Target { path: path.into(), validator: None, }, interface: InterfaceTarget { reference: reference(scope), scope_ref, validator: None, }, operation: "read".into(), arguments: BTreeMap::new(), } } #[test] fn nonroot_observe_fetch_call_with_optional_refs_over_http() { for with_refs in [false, true] { let fixture = Fixture::new(); let engine = scoped_engine(fixture.shared.clone(), with_refs); let view = engine.view(&Principal::Admin); let limits = ResourceLimits::default(); let endpoint = Endpoint::parse("https://example.test/scopes").unwrap(); let adapter = HttpAdapter::new(&view, &endpoint, limits); for (path, scope) in [("/secret/nested/data", "/secret"), ("/added", "/added")] { let observe = observe_request(path, 0); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &observe, limits.wire).unwrap(), &ExecutionContext::new(), ); let DecodedResponse::Success(observed) = decode_observe_response(&observe, &response, limits.wire).unwrap() else { panic!("observation must succeed"); }; assert!(observed.object.interfaces.contains(&reference(scope))); let fetch = fetch_request(&reference(scope)); let response = adapter.handle_http_with_context( encode_fetch_interface_request(&endpoint, &fetch, limits.wire).unwrap(), &ExecutionContext::new(), ); let DecodedResponse::Success(fetched) = decode_fetch_interface_response(&fetch, &response, limits.wire).unwrap() else { panic!("fetch must succeed"); }; assert_eq!( fetched.scope_ref, with_refs.then(|| format!("published-object-{}", fixture.shared.node(scope).id)) ); let mut call = request(path, scope, fetched.scope_ref); call.target.validator = observed.object.validator; let response = adapter.handle_http_with_context( encode_call_operation_request(&endpoint, &call, &fetched.descriptor, limits.wire) .unwrap(), &ExecutionContext::new(), ); let DecodedResponse::Success(result) = decode_call_operation_response(&call, &fetched.descriptor, &response, limits.wire) .unwrap() else { panic!("call must succeed"); }; assert_eq!(result.result, Value::Bytes(b"hello memory".to_vec())); if !with_refs { call.interface.scope_ref = Some("ref-not-provided-by-publication".into()); assert_eq!( view.call_operation(call).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); } } assert!( view.observe_object("/secret") .unwrap() .interfaces .is_empty() ); assert_eq!( view.observe_object("/secret/nested/data") .unwrap() .interfaces, [ reference("/"), reference("/secret"), reference("/secret/nested") ] ); let limited = engine.view(&Principal::Limited); for scope in ["/secret", "/secret/nested", "/added"] { assert_eq!( limited .fetch_interface(&fetch_request(&reference(scope))) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); } assert_eq!( limited .observe_object("/secret/nested/data") .unwrap_err() .kind(), wip_host::CallErrorKind::NotFound ); assert!( !limited .observe_object("/added") .unwrap() .interfaces .contains(&reference("/added")) ); } } #[test] fn scope_refs_and_object_validator_precedence_use_current_atomic_lineage() { let fixture = Fixture::new(); let engine = scoped_engine(fixture.shared.clone(), true); let view = engine.view(&Principal::Admin); let mut call = request("/secret/nested/data", "/secret", Some("stale".into())); call.target.validator = Some(vec![0]); assert_eq!( view.call_operation(call.clone()).unwrap_err().kind(), wip_host::CallErrorKind::ValidatorMismatch ); call.target.validator = view.observe_object(&call.target.path).unwrap().validator; assert_eq!( view.call_operation(call.clone()).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); call.interface.scope_ref = view .fetch_interface(&fetch_request(&call.interface.reference)) .unwrap() .scope_ref; // Scope state changes do not retire its lifetime; the published ref is not // its validator, and the hook runs on refreshed metadata under the lock. fixture.shared.controls.lock().unwrap().bump_ancestor = Some("/secret".into()); fixture.shared.seen.lock().unwrap().clear(); view.call_operation(call).unwrap(); let seen = fixture.shared.seen.lock().unwrap(); let resolves: Vec<_> = seen .iter() .filter(|event| event.stage == "resolve") .map(|event| event.path.as_str()) .collect(); assert_eq!(resolves, ["/secret/nested/data"]); assert!( seen.iter() .any(|event| event.stage == "scope" && event.path == "/secret") ); } #[test] fn nonroot_interface_validator_checks_follow_scope_ref_checks() { let fixture = Fixture::new(); let engine = configured_engine(fixture.shared.clone(), true, Some(Validator::new(vec![7]))); let view = engine.view(&Principal::Admin); let fetched = view .fetch_interface(&fetch_request(&reference("/secret"))) .unwrap(); assert_eq!(fetched.validator, Some(vec![7])); let mut call = request("/secret/nested/data", "/secret", Some("stale".into())); call.target.validator = view.observe_object(&call.target.path).unwrap().validator; assert_eq!( view.call_operation(call.clone()).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); call.interface.scope_ref = fetched.scope_ref; assert_eq!( view.call_operation(call.clone()).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceValidatorRequired ); call.interface.validator = Some(vec![8]); assert_eq!( view.call_operation(call.clone()).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceValidatorMismatch ); assert!( fixture .shared .node(&call.target.path) .calls .lock() .unwrap() .is_empty() ); call.interface.validator = fetched.validator; view.call_operation(call).unwrap(); } #[test] fn visibility_and_replacement_races_reject_before_dispatch() { for replace in [false, true] { let fixture = Fixture::new(); let engine = scoped_engine(fixture.shared.clone(), false); let view = engine.view(&Principal::Admin); let mut call = request("/secret/nested/data", "/secret", None); call.target.validator = view.observe_object(&call.target.path).unwrap().validator; let leaf = fixture.shared.node(&call.target.path); if replace { fixture.shared.controls.lock().unwrap().replace_ancestor = Some("/secret".into()); } else { fixture.shared.controls.lock().unwrap().hide_ancestor = Some("/secret".into()); } assert_eq!( view.call_operation(call).unwrap_err().kind(), wip_host::CallErrorKind::NotFound ); assert!(leaf.calls.lock().unwrap().is_empty()); assert_eq!( view.fetch_interface(&fetch_request(&reference("/secret"))) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); } } #[test] fn deletion_replacement_and_self_scope_do_not_reuse_registry_lifetimes() { for with_refs in [false, true] { let fixture = Fixture::new(); let engine = scoped_engine(fixture.shared.clone(), with_refs); let view = engine.view(&Principal::Admin); let old_scope = fixture.shared.node("/secret"); let old_ref = view .fetch_interface(&fetch_request(&reference("/secret"))) .unwrap() .scope_ref; fixture.shared.nodes.lock().unwrap().remove("/secret"); assert_eq!( view.fetch_interface(&fetch_request(&reference("/secret"))) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); // Reuse even the same id/public ref: refs cannot attest a lifetime. fixture.shared.nodes.lock().unwrap().insert( "/secret".into(), Arc::new(Node { id: old_scope.id, kind: EntryKind::Directory, data: Mutex::new(old_scope.data.lock().unwrap().clone()), calls: Mutex::new(Vec::new()), }), ); assert_eq!( view.fetch_interface(&fetch_request(&reference("/secret"))) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); let mut call = request("/secret/nested/data", "/secret", old_ref); call.target.validator = view.observe_object(&call.target.path).unwrap().validator; assert_eq!( view.call_operation(call).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); assert!( fixture .shared .node("/secret/nested/data") .calls .lock() .unwrap() .is_empty() ); // Fresh registration/engine attests the new publication. let republished = scoped_engine(fixture.shared.clone(), with_refs); republished .view(&Principal::Admin) .fetch_interface(&fetch_request(&reference("/secret"))) .unwrap(); // Scope == target: ordinary execution pins the old leaf, but cannot // attest it as the current scope after its mapping is replaced. let leaf = fixture.shared.node("/added"); let mut call = request("/added", "/added", None); call.target.validator = view.observe_object("/added").unwrap().validator; fixture.shared.controls.lock().unwrap().replace_mapping = true; assert_eq!( view.call_operation(call).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); assert!(leaf.calls.lock().unwrap().is_empty()); assert_eq!( view.fetch_interface(&fetch_request(&reference("/added"))) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); } }