| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::root_dir::RootDir, |
| anyhow::Context as _, |
| async_trait::async_trait, |
| fidl::{endpoints::ServerEnd, HandleBased as _}, |
| fidl_fuchsia_io as fio, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_zircon as zx, |
| once_cell::sync::OnceCell, |
| std::sync::Arc, |
| vfs::{ |
| common::send_on_open_with_error, directory::entry::EntryInfo, |
| execution_scope::ExecutionScope, path::Path as VfsPath, |
| }, |
| }; |
| |
| /// Location of MetaFile contents within a meta.far |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| pub(crate) struct MetaFileLocation { |
| pub(crate) offset: u64, |
| pub(crate) length: u64, |
| } |
| |
| pub(crate) struct MetaFile { |
| root_dir: Arc<RootDir>, |
| location: MetaFileLocation, |
| vmo: OnceCell<zx::Vmo>, |
| } |
| |
| impl MetaFile { |
| pub(crate) fn new(root_dir: Arc<RootDir>, location: MetaFileLocation) -> Self { |
| MetaFile { root_dir, location, vmo: OnceCell::new() } |
| } |
| |
| async fn vmo(&self) -> Result<&zx::Vmo, anyhow::Error> { |
| Ok(if let Some(vmo) = self.vmo.get() { |
| vmo |
| } else { |
| let far_vmo = self.root_dir.meta_far_vmo().await.context("getting far vmo")?; |
| // The FAR spec requires 4 KiB alignment of content chunks [1], so offset will |
| // always be page-aligned, because pages are required [2] to be a power of 2 and at |
| // least 4 KiB. |
| // [1] https://fuchsia.dev/fuchsia-src/concepts/source_code/archive_format#content_chunk |
| // [2] https://fuchsia.dev/fuchsia-src/reference/syscalls/system_get_page_size |
| // TODO(fxbug.dev/82006) Need to manually zero the end of the VMO if |
| // zx_system_get_page_size() > 4K. |
| assert_eq!(zx::system_get_page_size(), 4096); |
| let vmo = far_vmo |
| .create_child( |
| zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE, |
| self.location.offset, |
| self.location.length, |
| ) |
| .context("creating MetaFile VMO")?; |
| self.vmo.get_or_init(|| vmo) |
| }) |
| } |
| } |
| |
| impl vfs::directory::entry::DirectoryEntry for MetaFile { |
| fn open( |
| self: Arc<Self>, |
| scope: ExecutionScope, |
| flags: fio::OpenFlags, |
| _mode: u32, |
| path: VfsPath, |
| server_end: ServerEnd<fio::NodeMarker>, |
| ) { |
| if !path.is_empty() { |
| let () = send_on_open_with_error(flags, server_end, zx::Status::NOT_DIR); |
| return; |
| } |
| |
| if flags.intersects( |
| fio::OpenFlags::RIGHT_WRITABLE |
| | fio::OpenFlags::RIGHT_EXECUTABLE |
| | fio::OpenFlags::CREATE |
| | fio::OpenFlags::CREATE_IF_ABSENT |
| | fio::OpenFlags::TRUNCATE |
| | fio::OpenFlags::APPEND, |
| ) { |
| let () = send_on_open_with_error(flags, server_end, zx::Status::NOT_SUPPORTED); |
| return; |
| } |
| |
| let () = vfs::file::connection::io1::FileConnection::<Self>::create_connection( |
| scope.clone(), |
| self, |
| flags, |
| server_end, |
| // readable/writable do not override what's set in flags, they merely tell the |
| // FileConnection that it's valid to open the file readable/writable. |
| true, /*=readable*/ |
| false, /*=writable*/ |
| false, /*=executable*/ |
| ); |
| } |
| |
| fn entry_info(&self) -> vfs::directory::entry::EntryInfo { |
| EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File) |
| } |
| } |
| |
| #[async_trait] |
| impl vfs::file::File for MetaFile { |
| async fn open(&self, _flags: fio::OpenFlags) -> Result<(), zx::Status> { |
| Ok(()) |
| } |
| |
| async fn read_at(&self, offset_chunk: u64, buffer: &mut [u8]) -> Result<u64, zx::Status> { |
| let offset_chunk = std::cmp::min(offset_chunk, self.location.length); |
| let offset_far = offset_chunk + self.location.offset; |
| let count = std::cmp::min( |
| crate::usize_to_u64_safe(buffer.len()), |
| self.location.length - offset_chunk, |
| ); |
| let bytes = self |
| .root_dir |
| .meta_far |
| .read_at(count, offset_far) |
| .await |
| .map_err(|e: fidl::Error| { |
| fx_log_err!("meta.far read_at fidl error: {:#}", e); |
| zx::Status::INTERNAL |
| })? |
| .map_err(zx::Status::from_raw) |
| .map_err(|e: zx::Status| { |
| fx_log_err!("meta.far read_at protocol error: {:#}", e); |
| e |
| })?; |
| let () = buffer[..bytes.len()].copy_from_slice(&bytes); |
| Ok(crate::usize_to_u64_safe(bytes.len())) |
| } |
| |
| async fn write_at(&self, _offset: u64, _content: &[u8]) -> Result<u64, zx::Status> { |
| Err(zx::Status::NOT_SUPPORTED) |
| } |
| |
| async fn append(&self, _content: &[u8]) -> Result<(u64, u64), zx::Status> { |
| Err(zx::Status::NOT_SUPPORTED) |
| } |
| |
| async fn truncate(&self, _length: u64) -> Result<(), zx::Status> { |
| Err(zx::Status::NOT_SUPPORTED) |
| } |
| |
| async fn get_buffer( |
| &self, |
| flags: fio::VmoFlags, |
| ) -> Result<fidl_fuchsia_mem::Buffer, zx::Status> { |
| if flags.intersects( |
| fio::VmoFlags::WRITE | fio::VmoFlags::EXECUTE | fio::VmoFlags::SHARED_BUFFER, |
| ) { |
| return Err(zx::Status::NOT_SUPPORTED); |
| } |
| |
| let vmo = self.vmo().await.map_err(|e: anyhow::Error| { |
| fx_log_err!("Failed to get MetaFile VMO during get_buffer: {:#}", e); |
| zx::Status::INTERNAL |
| })?; |
| |
| if flags.contains(fio::VmoFlags::PRIVATE_CLONE) { |
| let vmo = vmo |
| .create_child( |
| zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE, |
| 0, /*offset*/ |
| self.location.length, |
| ) |
| .map_err(|e: zx::Status| { |
| fx_log_err!("Failed to create private child VMO during get_buffer: {:#}", e); |
| e |
| })?; |
| Ok(fidl_fuchsia_mem::Buffer { vmo, size: self.location.length }) |
| } else { |
| let rights = zx::Rights::BASIC |
| | zx::Rights::MAP |
| | zx::Rights::PROPERTY |
| | if flags.contains(fio::VmoFlags::READ) { |
| zx::Rights::READ |
| } else { |
| zx::Rights::NONE |
| }; |
| let vmo = vmo.duplicate_handle(rights).map_err(|e: zx::Status| { |
| fx_log_err!("Failed to clone VMO handle during get_buffer: {:#}", e); |
| e |
| })?; |
| Ok(fidl_fuchsia_mem::Buffer { vmo, size: self.location.length }) |
| } |
| } |
| |
| async fn get_size(&self) -> Result<u64, zx::Status> { |
| Ok(self.location.length) |
| } |
| |
| async fn get_attrs(&self) -> Result<fio::NodeAttributes, zx::Status> { |
| Ok(fio::NodeAttributes { |
| mode: fio::MODE_TYPE_FILE |
| | vfs::common::rights_to_posix_mode_bits( |
| true, // read |
| false, // write |
| false, // execute |
| ), |
| id: 1, |
| content_size: self.location.length, |
| storage_size: self.location.length, |
| link_count: 1, |
| creation_time: 0, |
| modification_time: 0, |
| }) |
| } |
| |
| async fn set_attrs( |
| &self, |
| _flags: fio::NodeAttributeFlags, |
| _attrs: fio::NodeAttributes, |
| ) -> Result<(), zx::Status> { |
| Err(zx::Status::NOT_SUPPORTED) |
| } |
| |
| async fn close(&self) -> Result<(), zx::Status> { |
| Ok(()) |
| } |
| |
| async fn sync(&self) -> Result<(), zx::Status> { |
| Err(zx::Status::NOT_SUPPORTED) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| assert_matches::assert_matches, |
| fidl::{endpoints::Proxy as _, AsHandleRef as _}, |
| fuchsia_pkg_testing::{blobfs::Fake as FakeBlobfs, PackageBuilder}, |
| futures::stream::StreamExt as _, |
| std::convert::{TryFrom as _, TryInto as _}, |
| vfs::{directory::entry::DirectoryEntry, file::File}, |
| }; |
| |
| const TEST_FILE_CONTENTS: [u8; 4] = [0, 1, 2, 3]; |
| |
| struct TestEnv { |
| _blobfs_fake: FakeBlobfs, |
| } |
| |
| impl TestEnv { |
| async fn new() -> (Self, MetaFile) { |
| let pkg = PackageBuilder::new("pkg") |
| .add_resource_at("meta/file", &TEST_FILE_CONTENTS[..]) |
| .build() |
| .await |
| .unwrap(); |
| let (metafar_blob, _) = pkg.contents(); |
| let (blobfs_fake, blobfs_client) = FakeBlobfs::new(); |
| blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents); |
| let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap(); |
| let location = *root_dir.meta_files.get("meta/file").unwrap(); |
| (TestEnv { _blobfs_fake: blobfs_fake }, MetaFile::new(Arc::new(root_dir), location)) |
| } |
| } |
| |
| fn node_to_file_proxy(proxy: fio::NodeProxy) -> fio::FileProxy { |
| fio::FileProxy::from_channel(proxy.into_channel().unwrap()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn vmo() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| // VMO is readable |
| let vmo = meta_file.vmo().await.unwrap(); |
| let mut buf = [0u8; 8]; |
| vmo.read(&mut buf, 0).unwrap(); |
| assert_eq!(buf, [0, 1, 2, 3, 0, 0, 0, 0]); |
| assert_eq!( |
| vmo.get_content_size().unwrap(), |
| u64::try_from(TEST_FILE_CONTENTS.len()).unwrap() |
| ); |
| |
| // VMO not writable |
| assert_eq!(vmo.write(&[0], 0), Err(zx::Status::ACCESS_DENIED)); |
| |
| // Accessing the VMO caches it |
| assert!(meta_file.vmo.get().is_some()); |
| |
| // Accessing the VMO through the cached path works |
| let vmo = meta_file.vmo().await.unwrap(); |
| let mut buf = [0u8; 8]; |
| vmo.read(&mut buf, 0).unwrap(); |
| assert_eq!(buf, [0, 1, 2, 3, 0, 0, 0, 0]); |
| assert_eq!(vmo.write(&[0], 0), Err(zx::Status::ACCESS_DENIED)); |
| assert_eq!( |
| vmo.get_content_size().unwrap(), |
| u64::try_from(TEST_FILE_CONTENTS.len()).unwrap() |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn directory_entry_open_rejects_non_empty_path() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let (proxy, server_end) = fidl::endpoints::create_proxy().unwrap(); |
| |
| DirectoryEntry::open( |
| Arc::new(meta_file), |
| ExecutionScope::new(), |
| fio::OpenFlags::DESCRIBE, |
| 0, |
| VfsPath::validate_and_split("non-empty").unwrap(), |
| server_end, |
| ); |
| |
| assert_matches!( |
| node_to_file_proxy(proxy).take_event_stream().next().await, |
| Some(Ok(fio::FileEvent::OnOpen_{ s, info: None})) |
| if s == zx::Status::NOT_DIR.into_raw() |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn directory_entry_open_rejects_disallowed_flags() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let meta_file = Arc::new(meta_file); |
| |
| for forbidden_flag in [ |
| fio::OpenFlags::RIGHT_WRITABLE, |
| fio::OpenFlags::RIGHT_EXECUTABLE, |
| fio::OpenFlags::CREATE, |
| fio::OpenFlags::CREATE_IF_ABSENT, |
| fio::OpenFlags::TRUNCATE, |
| fio::OpenFlags::APPEND, |
| ] { |
| let (proxy, server_end) = fidl::endpoints::create_proxy().unwrap(); |
| DirectoryEntry::open( |
| Arc::clone(&meta_file), |
| ExecutionScope::new(), |
| fio::OpenFlags::DESCRIBE | forbidden_flag, |
| 0, |
| VfsPath::dot(), |
| server_end, |
| ); |
| |
| assert_matches!( |
| node_to_file_proxy(proxy).take_event_stream().next().await, |
| Some(Ok(fio::FileEvent::OnOpen_{ s, info: None})) |
| if s == zx::Status::NOT_SUPPORTED.into_raw() |
| ); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn directory_entry_open_succeeds() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let (proxy, server_end) = fidl::endpoints::create_proxy().unwrap(); |
| |
| DirectoryEntry::open( |
| Arc::new(meta_file), |
| ExecutionScope::new(), |
| fio::OpenFlags::DESCRIBE, |
| 0, |
| VfsPath::dot(), |
| server_end, |
| ); |
| |
| assert_matches!( |
| node_to_file_proxy(proxy).take_event_stream().next().await, |
| Some(Ok(fio::FileEvent::OnOpen_ { s, info: Some(_) })) |
| if s == zx::Status::OK.into_raw() |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn directory_entry_entry_info() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!( |
| DirectoryEntry::entry_info(&meta_file), |
| EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_open() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::open(&meta_file, fio::OpenFlags::empty()).await, Ok(())); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_read_at_adjusts_offset() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let mut buffer = [0u8]; |
| |
| for (i, e) in TEST_FILE_CONTENTS.iter().enumerate() { |
| assert_eq!(File::read_at(&meta_file, i.try_into().unwrap(), &mut buffer).await, Ok(1)); |
| assert_eq!(&buffer, &[*e]); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_read_at_past_end_returns_no_bytes() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let mut buffer = [0u8]; |
| |
| for i in 0..=1 { |
| assert_eq!( |
| File::read_at( |
| &meta_file, |
| u64::try_from(TEST_FILE_CONTENTS.len()).unwrap() + i, |
| &mut buffer |
| ) |
| .await, |
| Ok(0) |
| ); |
| assert_eq!(&buffer, &[0]); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_read_at_caps_count() { |
| let (_env, meta_file) = TestEnv::new().await; |
| let mut buffer = [0u8; 5]; |
| |
| assert_eq!(File::read_at(&meta_file, 2, &mut buffer).await, Ok(2)); |
| assert_eq!(&buffer, &[2, 3, 0, 0, 0]); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_write_at() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::write_at(&meta_file, 0, &[]).await, Err(zx::Status::NOT_SUPPORTED)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_append() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::append(&meta_file, &[]).await, Err(zx::Status::NOT_SUPPORTED)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_truncate() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::truncate(&meta_file, 0).await, Err(zx::Status::NOT_SUPPORTED)); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_get_buffer_rejects_unsupported_flags() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| for flag in [fio::VmoFlags::WRITE, fio::VmoFlags::EXECUTE, fio::VmoFlags::SHARED_BUFFER] { |
| assert_eq!(File::get_buffer(&meta_file, flag).await, Err(zx::Status::NOT_SUPPORTED)); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_get_buffer_private() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| let fidl_fuchsia_mem::Buffer { vmo, size } = |
| File::get_buffer(&meta_file, fio::VmoFlags::PRIVATE_CLONE) |
| .await |
| .expect("get_buffer should succeed"); |
| |
| assert_eq!(size, u64::try_from(TEST_FILE_CONTENTS.len()).unwrap()); |
| // VMO is readable |
| let mut buf = [0u8; 8]; |
| vmo.read(&mut buf, 0).unwrap(); |
| assert_eq!(buf, [0, 1, 2, 3, 0, 0, 0, 0]); |
| assert_eq!( |
| vmo.get_content_size().unwrap(), |
| u64::try_from(TEST_FILE_CONTENTS.len()).unwrap() |
| ); |
| |
| // VMO not writable |
| assert_eq!(vmo.write(&[0], 0), Err(zx::Status::ACCESS_DENIED)); |
| |
| // VMO is not shared |
| assert_eq!(vmo.count_info().unwrap().handle_count, 1); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_get_buffer_not_private() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| let fidl_fuchsia_mem::Buffer { vmo, size } = |
| File::get_buffer(&meta_file, fio::VmoFlags::READ) |
| .await |
| .expect("get_buffer should succeed"); |
| |
| assert_eq!(size, u64::try_from(TEST_FILE_CONTENTS.len()).unwrap()); |
| // VMO is readable |
| let mut buf = [0u8; 8]; |
| vmo.read(&mut buf, 0).unwrap(); |
| assert_eq!(buf, [0, 1, 2, 3, 0, 0, 0, 0]); |
| assert_eq!( |
| vmo.get_content_size().unwrap(), |
| u64::try_from(TEST_FILE_CONTENTS.len()).unwrap() |
| ); |
| |
| // VMO not writable |
| assert_eq!(vmo.write(&[0], 0), Err(zx::Status::ACCESS_DENIED)); |
| |
| // VMO is shared |
| assert_eq!(vmo.count_info().unwrap().handle_count, 2); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_get_size() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!( |
| File::get_size(&meta_file).await, |
| Ok(u64::try_from(TEST_FILE_CONTENTS.len()).unwrap()) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_get_attrs() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!( |
| File::get_attrs(&meta_file).await, |
| Ok(fio::NodeAttributes { |
| mode: fio::MODE_TYPE_FILE | 0o400, |
| id: 1, |
| content_size: meta_file.location.length, |
| storage_size: meta_file.location.length, |
| link_count: 1, |
| creation_time: 0, |
| modification_time: 0, |
| }) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_set_attrs() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!( |
| File::set_attrs( |
| &meta_file, |
| fio::NodeAttributeFlags::empty(), |
| fio::NodeAttributes { |
| mode: 0, |
| id: 0, |
| content_size: 0, |
| storage_size: 0, |
| link_count: 0, |
| creation_time: 0, |
| modification_time: 0, |
| }, |
| ) |
| .await, |
| Err(zx::Status::NOT_SUPPORTED) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_close() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::close(&meta_file).await, Ok(())); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn file_sync() { |
| let (_env, meta_file) = TestEnv::new().await; |
| |
| assert_eq!(File::sync(&meta_file).await, Err(zx::Status::NOT_SUPPORTED)); |
| } |
| } |