1use crate::common::{inherit_rights_for_clone, send_on_open_with_error};
6use crate::directory::common::check_child_connection_flags;
7use crate::directory::entry_container::{Directory, DirectoryWatcher};
8use crate::directory::traversal_position::TraversalPosition;
9use crate::directory::{read_dirents, DirectoryOptions};
10use crate::execution_scope::{yield_to_executor, ExecutionScope};
11use crate::node::OpenNode;
12use crate::object_request::Representation;
13use crate::path::Path;
14
15use anyhow::Error;
16use fidl::endpoints::ServerEnd;
17use fidl_fuchsia_io as fio;
18use std::convert::TryInto as _;
19use storage_trace::{self as trace, TraceFutureExt};
20use zx_status::Status;
21
22use crate::common::CreationMode;
23use crate::{ObjectRequest, ObjectRequestRef, ProtocolsExt};
24
25pub enum ConnectionState {
27 Alive,
29 Closed,
31}
32
33pub(in crate::directory) struct BaseConnection<DirectoryType: Directory> {
38 pub(in crate::directory) scope: ExecutionScope,
41
42 pub(in crate::directory) directory: OpenNode<DirectoryType>,
43
44 pub(in crate::directory) options: DirectoryOptions,
46
47 seek: TraversalPosition,
62}
63
64impl<DirectoryType: Directory> BaseConnection<DirectoryType> {
65 pub(in crate::directory) fn new(
69 scope: ExecutionScope,
70 directory: OpenNode<DirectoryType>,
71 options: DirectoryOptions,
72 ) -> Self {
73 BaseConnection { scope, directory, options, seek: Default::default() }
74 }
75
76 pub(in crate::directory) async fn handle_request(
79 &mut self,
80 request: fio::DirectoryRequest,
81 ) -> Result<ConnectionState, Error> {
82 match request {
83 #[cfg(fuchsia_api_level_at_least = "26")]
84 fio::DirectoryRequest::DeprecatedClone { flags, object, control_handle: _ } => {
85 trace::duration!(c"storage", c"Directory::DeprecatedClone");
86 self.handle_deprecated_clone(flags, object);
87 }
88 #[cfg(not(fuchsia_api_level_at_least = "26"))]
89 fio::DirectoryRequest::Clone { flags, object, control_handle: _ } => {
90 trace::duration!(c"storage", c"Directory::Clone");
91 self.handle_deprecated_clone(flags, object);
92 }
93 #[cfg(fuchsia_api_level_at_least = "26")]
94 fio::DirectoryRequest::Clone { request, control_handle: _ } => {
95 trace::duration!(c"storage", c"Directory::Clone");
96 self.handle_clone(request.into_channel());
97 }
98 #[cfg(not(fuchsia_api_level_at_least = "26"))]
99 fio::DirectoryRequest::Clone2 { request, control_handle: _ } => {
100 trace::duration!(c"storage", c"Directory::Clone2");
101 self.handle_clone(request.into_channel());
102 }
103 fio::DirectoryRequest::Close { responder } => {
104 trace::duration!(c"storage", c"Directory::Close");
105 responder.send(Ok(()))?;
106 return Ok(ConnectionState::Closed);
107 }
108 fio::DirectoryRequest::GetConnectionInfo { responder } => {
109 trace::duration!(c"storage", c"Directory::GetConnectionInfo");
110 responder.send(fio::ConnectionInfo {
111 rights: Some(self.options.rights),
112 ..Default::default()
113 })?;
114 }
115 fio::DirectoryRequest::GetAttr { responder } => {
116 async move {
117 let (status, attrs) = crate::common::io2_to_io1_attrs(
118 self.directory.as_ref(),
119 self.options.rights,
120 )
121 .await;
122 responder.send(status.into_raw(), &attrs)
123 }
124 .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
125 .await?;
126 }
127 fio::DirectoryRequest::GetAttributes { query, responder } => {
128 async move {
129 let attrs = self.directory.get_attributes(query).await;
131 responder.send(
132 attrs
133 .as_ref()
134 .map(|attrs| (&attrs.mutable_attributes, &attrs.immutable_attributes))
135 .map_err(|status| status.into_raw()),
136 )
137 }
138 .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttributes"))
139 .await?;
140 }
141 fio::DirectoryRequest::UpdateAttributes { payload: _, responder } => {
142 trace::duration!(c"storage", c"Directory::UpdateAttributes");
143 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
145 }
146 fio::DirectoryRequest::ListExtendedAttributes { iterator, .. } => {
147 trace::duration!(c"storage", c"Directory::ListExtendedAttributes");
148 iterator.close_with_epitaph(Status::NOT_SUPPORTED)?;
149 }
150 fio::DirectoryRequest::GetExtendedAttribute { responder, .. } => {
151 trace::duration!(c"storage", c"Directory::GetExtendedAttribute");
152 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
153 }
154 fio::DirectoryRequest::SetExtendedAttribute { responder, .. } => {
155 trace::duration!(c"storage", c"Directory::SetExtendedAttribute");
156 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
157 }
158 fio::DirectoryRequest::RemoveExtendedAttribute { responder, .. } => {
159 trace::duration!(c"storage", c"Directory::RemoveExtendedAttribute");
160 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
161 }
162 #[cfg(fuchsia_api_level_at_least = "NEXT")]
163 fio::DirectoryRequest::GetFlags { responder } => {
164 trace::duration!(c"storage", c"Directory::GetFlags");
165 responder.send(Ok(fio::Flags::from(&self.options)))?;
166 }
167 #[cfg(fuchsia_api_level_at_least = "NEXT")]
168 fio::DirectoryRequest::SetFlags { flags: _, responder } => {
169 trace::duration!(c"storage", c"Directory::SetFlags");
170 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
171 }
172 #[cfg(fuchsia_api_level_at_least = "NEXT")]
173 fio::DirectoryRequest::DeprecatedGetFlags { responder } => {
174 trace::duration!(c"storage", c"Directory::DeprecatedGetFlags");
175 responder.send(Status::OK.into_raw(), self.options.to_io1())?;
176 }
177 #[cfg(fuchsia_api_level_at_least = "NEXT")]
178 fio::DirectoryRequest::DeprecatedSetFlags { flags: _, responder } => {
179 trace::duration!(c"storage", c"Directory::DeprecatedSetFlags");
180 responder.send(Status::NOT_SUPPORTED.into_raw())?;
181 }
182 #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
183 fio::DirectoryRequest::GetFlags { responder } => {
184 trace::duration!(c"storage", c"Directory::GetFlags");
185 responder.send(Status::OK.into_raw(), self.options.to_io1())?;
186 }
187 #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
188 fio::DirectoryRequest::SetFlags { flags: _, responder } => {
189 trace::duration!(c"storage", c"Directory::SetFlags");
190 responder.send(Status::NOT_SUPPORTED.into_raw())?;
191 }
192 #[cfg(fuchsia_api_level_at_least = "NEXT")]
193 fio::DirectoryRequest::DeprecatedOpen {
194 flags,
195 mode: _,
196 path,
197 object,
198 control_handle: _,
199 } => {
200 {
201 trace::duration!(c"storage", c"Directory::Open");
202 self.handle_deprecated_open(flags, path, object);
203 }
204 yield_to_executor().await;
207 }
208 #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
209 fio::DirectoryRequest::Open { flags, mode: _, path, object, control_handle: _ } => {
210 {
211 trace::duration!(c"storage", c"Directory::Open");
212 self.handle_deprecated_open(flags, path, object);
213 }
214 yield_to_executor().await;
217 }
218 fio::DirectoryRequest::AdvisoryLock { request: _, responder } => {
219 trace::duration!(c"storage", c"Directory::AdvisoryLock");
220 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
221 }
222 fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
223 async move {
224 let (status, entries) = self.handle_read_dirents(max_bytes).await;
225 responder.send(status.into_raw(), entries.as_slice())
226 }
227 .trace(trace::trace_future_args!(c"storage", c"Directory::ReadDirents"))
228 .await?;
229 }
230 fio::DirectoryRequest::Rewind { responder } => {
231 trace::duration!(c"storage", c"Directory::Rewind");
232 self.seek = Default::default();
233 responder.send(Status::OK.into_raw())?;
234 }
235 fio::DirectoryRequest::Link { src, dst_parent_token, dst, responder } => {
236 async move {
237 let status: Status = self.handle_link(&src, dst_parent_token, dst).await.into();
238 responder.send(status.into_raw())
239 }
240 .trace(trace::trace_future_args!(c"storage", c"Directory::Link"))
241 .await?;
242 }
243 fio::DirectoryRequest::Watch { mask, options, watcher, responder } => {
244 trace::duration!(c"storage", c"Directory::Watch");
245 let status = if options != 0 {
246 Status::INVALID_ARGS
247 } else {
248 let watcher = watcher.try_into()?;
249 self.handle_watch(mask, watcher).into()
250 };
251 responder.send(status.into_raw())?;
252 }
253 fio::DirectoryRequest::Query { responder } => {
254 let () = responder.send(fio::DIRECTORY_PROTOCOL_NAME.as_bytes())?;
255 }
256 fio::DirectoryRequest::QueryFilesystem { responder } => {
257 trace::duration!(c"storage", c"Directory::QueryFilesystem");
258 match self.directory.query_filesystem() {
259 Err(status) => responder.send(status.into_raw(), None)?,
260 Ok(info) => responder.send(0, Some(&info))?,
261 }
262 }
263 fio::DirectoryRequest::Unlink { name: _, options: _, responder } => {
264 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
265 }
266 fio::DirectoryRequest::GetToken { responder } => {
267 responder.send(Status::NOT_SUPPORTED.into_raw(), None)?;
268 }
269 fio::DirectoryRequest::Rename { src: _, dst_parent_token: _, dst: _, responder } => {
270 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
271 }
272 fio::DirectoryRequest::SetAttr { flags: _, attributes: _, responder } => {
273 responder.send(Status::NOT_SUPPORTED.into_raw())?;
274 }
275 fio::DirectoryRequest::Sync { responder } => {
276 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
277 }
278 fio::DirectoryRequest::CreateSymlink { responder, .. } => {
279 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
280 }
281 #[cfg(fuchsia_api_level_at_least = "NEXT")]
282 fio::DirectoryRequest::Open { path, mut flags, options, object, control_handle: _ } => {
283 {
284 if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
286 flags &= !fio::Flags::PERM_INHERIT_WRITE;
287 }
288 if !self.options.rights.contains(fio::Rights::EXECUTE) {
289 flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
290 }
291
292 ObjectRequest::new(flags, &options, object)
293 .handle_async(async |req| self.handle_open(path, flags, req).await)
294 .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
295 .await;
296 }
297 yield_to_executor().await;
300 }
301 #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
302 fio::DirectoryRequest::Open3 {
303 path,
304 mut flags,
305 options,
306 object,
307 control_handle: _,
308 } => {
309 {
310 if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
312 flags &= !fio::Flags::PERM_INHERIT_WRITE;
313 }
314 if !self.options.rights.contains(fio::Rights::EXECUTE) {
315 flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
316 }
317
318 ObjectRequest::new(flags, &options, object)
319 .handle_async(async |req| self.handle_open(path, flags, req).await)
320 .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
321 .await;
322 }
323 yield_to_executor().await;
326 }
327 fio::DirectoryRequest::_UnknownMethod { .. } => (),
328 }
329 Ok(ConnectionState::Alive)
330 }
331
332 fn handle_deprecated_clone(
333 &self,
334 flags: fio::OpenFlags,
335 server_end: ServerEnd<fio::NodeMarker>,
336 ) {
337 let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
338 let flags = match inherit_rights_for_clone(self.options.to_io1(), flags) {
339 Ok(updated) => updated,
340 Err(status) => {
341 send_on_open_with_error(describe, server_end, status);
342 return;
343 }
344 };
345
346 self.directory.clone().open(self.scope.clone(), flags, Path::dot(), server_end);
347 }
348
349 fn handle_clone(&mut self, object: fidl::Channel) {
350 let flags = fio::Flags::from(&self.options);
351 ObjectRequest::new(flags, &Default::default(), object).handle(|req| {
352 self.directory.clone().open3(self.scope.clone(), Path::dot(), flags, req)
353 });
354 }
355
356 fn handle_deprecated_open(
357 &self,
358 mut flags: fio::OpenFlags,
359 path: String,
360 server_end: ServerEnd<fio::NodeMarker>,
361 ) {
362 let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
363
364 let path = match Path::validate_and_split(path) {
365 Ok(path) => path,
366 Err(status) => {
367 send_on_open_with_error(describe, server_end, status);
368 return;
369 }
370 };
371
372 if path.is_dir() {
373 flags |= fio::OpenFlags::DIRECTORY;
374 }
375
376 let flags = match check_child_connection_flags(self.options.to_io1(), flags) {
377 Ok(updated) => updated,
378 Err(status) => {
379 send_on_open_with_error(describe, server_end, status);
380 return;
381 }
382 };
383 if path.is_dot() {
384 if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
385 send_on_open_with_error(describe, server_end, Status::INVALID_ARGS);
386 return;
387 }
388 if flags.intersects(fio::OpenFlags::CREATE_IF_ABSENT) {
389 send_on_open_with_error(describe, server_end, Status::ALREADY_EXISTS);
390 return;
391 }
392 }
393
394 let directory = self.directory.clone();
396 directory.open(self.scope.clone(), flags, path, server_end);
397 }
398
399 async fn handle_open(
400 &self,
401 path: String,
402 flags: fio::Flags,
403 object_request: ObjectRequestRef<'_>,
404 ) -> Result<(), Status> {
405 let path = Path::validate_and_split(path)?;
406
407 if let Some(rights) = flags.rights() {
409 if rights.intersects(!self.options.rights) {
410 return Err(Status::ACCESS_DENIED);
411 }
412 }
413
414 if !object_request.attributes().is_empty()
416 && !self.options.rights.contains(fio::Operations::GET_ATTRIBUTES)
417 {
418 return Err(Status::ACCESS_DENIED);
419 }
420
421 match flags.creation_mode() {
422 CreationMode::Never => {
423 if object_request.create_attributes().is_some() {
424 return Err(Status::INVALID_ARGS);
425 }
426 }
427 CreationMode::UnnamedTemporary | CreationMode::UnlinkableUnnamedTemporary => {
428 if !flags.intersects(fio::Flags::PROTOCOL_FILE) {
430 return Err(Status::NOT_SUPPORTED);
431 }
432 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
434 return Err(Status::ACCESS_DENIED);
435 }
436 }
443 CreationMode::AllowExisting | CreationMode::Always => {
444 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
446 return Err(Status::ACCESS_DENIED);
447 }
448
449 let protocol_flags = flags & fio::MASK_KNOWN_PROTOCOLS;
450 if protocol_flags.is_empty()
453 || (protocol_flags.bits() & (protocol_flags.bits() - 1)) != 0
454 {
455 return Err(Status::INVALID_ARGS);
456 }
457 if !protocol_flags
459 .intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_FILE)
460 {
461 return Err(Status::NOT_SUPPORTED);
462 }
463 }
464 }
465
466 if path.is_dot() && !flags.create_unnamed_temporary_in_directory_path() {
467 if !flags.is_dir_allowed() {
468 return Err(Status::INVALID_ARGS);
469 }
470 if flags.creation_mode() == CreationMode::Always {
471 return Err(Status::ALREADY_EXISTS);
472 }
473 }
474
475 self.directory.clone().open3_async(self.scope.clone(), path, flags, object_request).await
476 }
477
478 async fn handle_read_dirents(&mut self, max_bytes: u64) -> (Status, Vec<u8>) {
479 async {
480 let (new_pos, sealed) =
481 self.directory.read_dirents(&self.seek, read_dirents::Sink::new(max_bytes)).await?;
482 self.seek = new_pos;
483 let read_dirents::Done { buf, status } = *sealed
484 .open()
485 .downcast::<read_dirents::Done>()
486 .map_err(|_: Box<dyn std::any::Any>| {
487 #[cfg(debug)]
488 panic!(
489 "`read_dirents()` returned a `dirents_sink::Sealed`
490 instance that is not an instance of the \
491 `read_dirents::Done`. This is a bug in the \
492 `read_dirents()` implementation."
493 );
494 Status::NOT_SUPPORTED
495 })?;
496 Ok((status, buf))
497 }
498 .await
499 .unwrap_or_else(|status| (status, Vec::new()))
500 }
501
502 async fn handle_link(
503 &self,
504 source_name: &str,
505 target_parent_token: fidl::Handle,
506 target_name: String,
507 ) -> Result<(), Status> {
508 if source_name.contains('/') || target_name.contains('/') {
509 return Err(Status::INVALID_ARGS);
510 }
511
512 if !self.options.rights.contains(fio::RW_STAR_DIR) {
518 return Err(Status::BAD_HANDLE);
519 }
520
521 let target_parent = self
522 .scope
523 .token_registry()
524 .get_owner(target_parent_token)?
525 .ok_or(Err(Status::NOT_FOUND))?;
526
527 target_parent.link(target_name, self.directory.clone().into_any(), source_name).await
528 }
529
530 fn handle_watch(
531 &mut self,
532 mask: fio::WatchMask,
533 watcher: DirectoryWatcher,
534 ) -> Result<(), Status> {
535 let directory = self.directory.clone();
536 directory.register_watcher(self.scope.clone(), mask, watcher)
537 }
538}
539
540impl<DirectoryType: Directory> Representation for BaseConnection<DirectoryType> {
541 type Protocol = fio::DirectoryMarker;
542
543 async fn get_representation(
544 &self,
545 requested_attributes: fio::NodeAttributesQuery,
546 ) -> Result<fio::Representation, Status> {
547 Ok(fio::Representation::Directory(fio::DirectoryInfo {
548 attributes: if requested_attributes.is_empty() {
549 None
550 } else {
551 Some(self.directory.get_attributes(requested_attributes).await?)
552 },
553 ..Default::default()
554 }))
555 }
556
557 async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
558 Ok(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject))
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use crate::directory::immutable::Simple;
566 use assert_matches::assert_matches;
567 use fidl_fuchsia_io as fio;
568 use futures::prelude::*;
569
570 #[fuchsia::test]
571 async fn test_open_not_found() {
572 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
573
574 let dir = Simple::new();
575 dir.open(
576 ExecutionScope::new(),
577 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
578 Path::dot(),
579 ServerEnd::new(dir_server_end.into_channel()),
580 );
581
582 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
583
584 assert_matches!(
586 dir_proxy.deprecated_open(
587 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
588 fio::ModeType::empty(),
589 "foo",
590 node_server_end
591 ),
592 Ok(())
593 );
594
595 assert_matches!(
597 node_proxy.query().await,
598 Err(fidl::Error::ClientChannelClosed {
599 status: Status::NOT_FOUND,
600 protocol_name: "fuchsia.io.Node",
601 ..
602 })
603 );
604 }
605
606 #[fuchsia::test]
607 async fn test_open_not_found_event_stream() {
608 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
609
610 let dir = Simple::new();
611 dir.open(
612 ExecutionScope::new(),
613 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
614 Path::dot(),
615 ServerEnd::new(dir_server_end.into_channel()),
616 );
617
618 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
619
620 assert_matches!(
622 dir_proxy.deprecated_open(
623 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
624 fio::ModeType::empty(),
625 "foo",
626 node_server_end
627 ),
628 Ok(())
629 );
630
631 let mut event_stream = node_proxy.take_event_stream();
633 assert_matches!(
634 event_stream.try_next().await,
635 Err(fidl::Error::ClientChannelClosed {
636 status: Status::NOT_FOUND,
637 protocol_name: "fuchsia.io.Node",
638 ..
639 })
640 );
641 assert_matches!(event_stream.try_next().await, Ok(None));
642 }
643
644 #[fuchsia::test]
645 async fn test_open_with_describe_not_found() {
646 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
647
648 let dir = Simple::new();
649 dir.open(
650 ExecutionScope::new(),
651 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
652 Path::dot(),
653 ServerEnd::new(dir_server_end.into_channel()),
654 );
655
656 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
657
658 assert_matches!(
660 dir_proxy.deprecated_open(
661 fio::OpenFlags::DIRECTORY
662 | fio::OpenFlags::DESCRIBE
663 | fio::OpenFlags::RIGHT_READABLE,
664 fio::ModeType::empty(),
665 "foo",
666 node_server_end,
667 ),
668 Ok(())
669 );
670
671 assert_matches!(
673 node_proxy.query().await,
674 Err(fidl::Error::ClientChannelClosed {
675 status: Status::NOT_FOUND,
676 protocol_name: "fuchsia.io.Node",
677 ..
678 })
679 );
680 }
681
682 #[fuchsia::test]
683 async fn test_open_describe_not_found_event_stream() {
684 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
685
686 let dir = Simple::new();
687 dir.open(
688 ExecutionScope::new(),
689 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
690 Path::dot(),
691 ServerEnd::new(dir_server_end.into_channel()),
692 );
693
694 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
695
696 assert_matches!(
698 dir_proxy.deprecated_open(
699 fio::OpenFlags::DIRECTORY
700 | fio::OpenFlags::DESCRIBE
701 | fio::OpenFlags::RIGHT_READABLE,
702 fio::ModeType::empty(),
703 "foo",
704 node_server_end,
705 ),
706 Ok(())
707 );
708
709 let mut event_stream = node_proxy.take_event_stream();
711 assert_matches!(
712 event_stream.try_next().await,
713 Ok(Some(fio::NodeEvent::OnOpen_ {
714 s,
715 info: None,
716 }))
717 if Status::from_raw(s) == Status::NOT_FOUND
718 );
719 assert_matches!(
720 event_stream.try_next().await,
721 Err(fidl::Error::ClientChannelClosed {
722 status: Status::NOT_FOUND,
723 protocol_name: "fuchsia.io.Node",
724 ..
725 })
726 );
727 assert_matches!(event_stream.try_next().await, Ok(None));
728 }
729}