diff --git a/README.md b/README.md index 958ef08..d6b1f9b 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,9 @@ descriptor; generated descriptors contain neither those references nor Rust trait identity. Dispatch is qualified by interface reference and operation name and targets a -canonical Worldspace path. An `ObjectProvider` resolves that path exactly once -inside `with_object`; the host then checks the supplied object validator, +canonical Worldspace path. An `ObjectProvider` validates that path before +resolution, maps malformed paths to `InvalidRequest`, then resolves a valid path +exactly once inside `with_object`; the host checks the supplied object validator, current interface membership, interface validator, descriptor operation, operation-specific object-validator policy, arguments, and authorization in canonical precedence. Path and children remain provider resolution context and diff --git a/crates/wip-host/src/lib.rs b/crates/wip-host/src/lib.rs index 8dea870..b0be438 100644 --- a/crates/wip-host/src/lib.rs +++ b/crates/wip-host/src/lib.rs @@ -540,6 +540,8 @@ pub trait ObjectProvider { /// Fetches the canonical object projection currently published at `path`. fn fetch(&self, path: &str) -> Result { + wip_protocol::validate_path(path) + .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?; self.with_object(path, PublishedObject::protocol_object) .ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published")) .and_then(|object| { @@ -552,12 +554,15 @@ pub trait ObjectProvider { /// Resolves and executes a canonical path-targeted operation request. /// - /// Failure checks follow the exact canonical precedence: path, supplied - /// object validator mismatch, membership, interface validator requirement - /// and mismatch, operation existence, object validator requirement, - /// arguments, then authorization. No failed precondition invokes the typed - /// implementation. + /// Structural path validation precedes provider resolution. For a valid + /// request, failure checks follow the exact canonical precedence: path, + /// supplied object validator mismatch, membership, interface validator + /// requirement and mismatch, operation existence, object validator + /// requirement, arguments, then authorization. No failed precondition + /// invokes the provider callback or typed implementation. fn call_operation(&self, request: CallRequest) -> Result { + wip_protocol::validate_path(&request.target.path) + .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?; let path = request.target.path.clone(); self.with_object(&path, |publication| { publication.dispatch_resolved(request, |object, interface, operation| { @@ -592,6 +597,8 @@ impl Error for PublicationError {} /// Stable host dispatch failure kind. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CallErrorKind { + /// The request is structurally invalid. + InvalidRequest, /// The target path is not currently published. NotFound, /// The selected interface is not exposed by the current object. @@ -671,6 +678,7 @@ impl CallError { #[must_use] pub fn protocol_error(&self) -> ProtocolError { let code = match self.kind { + CallErrorKind::InvalidRequest => ProtocolErrorCode::InvalidRequest, CallErrorKind::NotFound => ProtocolErrorCode::NotFound, CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch, CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound, diff --git a/crates/wip-host/tests/host.rs b/crates/wip-host/tests/host.rs index 11495e0..7436a89 100644 --- a/crates/wip-host/tests/host.rs +++ b/crates/wip-host/tests/host.rs @@ -440,6 +440,37 @@ fn path_dispatch_is_interface_qualified_and_resolves_once_per_call() { fn call_failures_follow_canonical_precedence_without_execution() { let (_, provider, hidden) = registry_and_provider(); + let resolutions = provider.resolutions.get(); + for invalid_path in [ + "relative", + "/trailing/", + "/double//segment", + "/dot/./segment", + "/parent/../segment", + ] { + assert_failure( + provider + .call_operation(request( + invalid_path, + Some(b"stale"), + hidden.reference(), + Some(b"stale"), + "missing", + BTreeMap::new(), + )) + .unwrap_err(), + CallErrorKind::InvalidRequest, + ProtocolErrorCode::InvalidRequest, + ); + } + assert_eq!(provider.resolutions.get(), resolutions); + assert_failure( + provider.fetch("/invalid/").unwrap_err(), + CallErrorKind::InvalidRequest, + ProtocolErrorCode::InvalidRequest, + ); + assert_eq!(provider.resolutions.get(), resolutions); + assert_failure( provider .call_operation(request(