| // Copyright 2020 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 { |
| anyhow::{Context, Error}, |
| fidl::endpoints::Proxy, |
| fidl_fuchsia_hardware_block_partition::{Guid, PartitionProxy}, |
| fidl_fuchsia_io::{DirectoryProxy, CLONE_FLAG_SAME_RIGHTS}, |
| fuchsia_fatfs::FatFs, |
| fuchsia_syslog::fx_log_info, |
| fuchsia_zircon as zx, |
| remote_block_device::RemoteBlockDevice, |
| std::ops::Deref, |
| vfs::{execution_scope::ExecutionScope, registry::token_registry}, |
| }; |
| |
| const MICROSOFT_BASIC_DATA_GUID: [u8; 16] = [ |
| 0xa2, 0xa0, 0xd0, 0xeb, 0xe5, 0xb9, 0x33, 0x44, 0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7, |
| ]; |
| const BLOCK_DEVICE_DIR: &str = "/dev/class/block"; |
| |
| pub struct FatDevice { |
| fs: FatFs, |
| pub scope: ExecutionScope, |
| } |
| |
| impl FatDevice { |
| /// Try and create a new FatDevice, searching for partitions in /dev/class/block. |
| pub async fn new() -> Result<Option<Self>, Error> { |
| let (local, remote) = zx::Channel::create()?; |
| fdio::service_connect(BLOCK_DEVICE_DIR, remote)?; |
| Self::new_at(local).await |
| } |
| |
| /// Try and create a new FatDevice, searching for partitions in the given channel. |
| async fn new_at(dir: zx::Channel) -> Result<Option<Self>, Error> { |
| let dir_proxy = DirectoryProxy::new(fidl::AsyncChannel::from_channel(dir)?); |
| let partition = match Self::find_fat_partition(&dir_proxy).await? { |
| Some(value) => value, |
| None => return Ok(None), |
| }; |
| |
| let dir = dir_proxy.into_channel().unwrap().into_zx_channel(); |
| let (block_channel, remote) = zx::Channel::create()?; |
| fdio::service_connect_at(&dir, &partition, remote)?; |
| let device = |
| Box::new(remote_block_device::Cache::new(RemoteBlockDevice::new_sync(block_channel)?)?); |
| // TODO(simonshields): if this fails, we could try looking for another partition. |
| let fs = FatFs::new(device)?; |
| |
| let registry = token_registry::Simple::new(); |
| let scope = ExecutionScope::build().token_registry(registry).new(); |
| |
| Ok(Some(FatDevice { fs, scope })) |
| } |
| |
| pub fn is_present(&self) -> bool { |
| self.fs.is_present() |
| } |
| |
| /// Find a partition with the "Microsoft Basic Data" GUID, which may contain a FAT partition. |
| async fn find_fat_partition(dir_proxy: &DirectoryProxy) -> Result<Option<String>, Error> { |
| let children = files_async::readdir(&dir_proxy).await?; |
| let (channel, remote) = zx::Channel::create()?; |
| dir_proxy.clone(CLONE_FLAG_SAME_RIGHTS, fidl::endpoints::ServerEnd::new(remote))?; |
| |
| for entry in children.iter() { |
| let guid = match Self::get_guid_at(&channel, &entry.name).await { |
| Ok(Some(guid)) => guid, |
| Ok(None) => { |
| // If there's no guid, skip the device. |
| continue; |
| } |
| Err(_) => { |
| // If this happens, it probably just means that the block device wasn't a |
| // partition. Skip it. |
| continue; |
| } |
| }; |
| |
| fx_log_info!("Found block device {:?} with guid {:?}", entry.name, guid); |
| if guid.value == MICROSOFT_BASIC_DATA_GUID { |
| return Ok(Some(entry.name.clone())); |
| } |
| } |
| // TODO(fxbug.dev/58577): should we set up a watcher with a timeout before giving up completely? |
| |
| Ok(None) |
| } |
| |
| /// Get the type GUID for the file with name |name| in |dir|. |
| async fn get_guid_at(dir: &zx::Channel, name: &str) -> Result<Option<Box<Guid>>, Error> { |
| let (local, remote) = zx::Channel::create().context("Creating channel")?; |
| fdio::service_connect_at(dir, name, remote)?; |
| |
| let proxy = PartitionProxy::new(fidl::AsyncChannel::from_channel(local)?); |
| |
| let (status, guid) = proxy.get_type_guid().await?; |
| zx::Status::ok(status).context("Getting GUID")?; |
| Ok(guid) |
| } |
| } |
| |
| impl Deref for FatDevice { |
| type Target = FatFs; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.fs |
| } |
| } |
| |
| #[cfg(test)] |
| pub mod test { |
| use { |
| super::*, |
| fidl::endpoints::ServerEnd, |
| fidl_fuchsia_hardware_block::BlockMarker, |
| fidl_fuchsia_hardware_block_partition::{Guid, PartitionMarker, PartitionRequest}, |
| fidl_fuchsia_io::{DirectoryMarker, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE}, |
| fuchsia_async as fasync, |
| fuchsia_component::server::ServiceFs, |
| futures::prelude::*, |
| ramdevice_client::RamdiskClient, |
| std::io::Write, |
| vfs::path::Path, |
| }; |
| |
| /// Dictates the FIDL protocol a MockPartition should speak. |
| enum MockPartitionMode { |
| Partition, |
| Block, |
| } |
| |
| /// Represents a block device, for unit testing. |
| struct MockPartition { |
| /// GUID to return for GetTypeGuid. |
| type_guid: [u8; 16], |
| /// FIDL protocol to speak. |
| mode: MockPartitionMode, |
| /// Channel to respond to requests on. |
| channel: zx::Channel, |
| } |
| |
| /// GUID for an EFI system partition. |
| const EFI_SYSTEM_PART_GUID: [u8; 16] = [ |
| 0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, |
| 0x3b, |
| ]; |
| |
| impl MockPartition { |
| /// Make a new FAT partition. |
| pub fn fat(channel: zx::Channel) -> Self { |
| MockPartition { |
| type_guid: MICROSOFT_BASIC_DATA_GUID, |
| mode: MockPartitionMode::Partition, |
| channel, |
| } |
| } |
| |
| /// Make a new Block protocol device. |
| pub fn block(channel: zx::Channel) -> Self { |
| MockPartition { |
| type_guid: MICROSOFT_BASIC_DATA_GUID, |
| mode: MockPartitionMode::Block, |
| channel, |
| } |
| } |
| |
| /// Make a new non-FAT partition. |
| pub fn not_fat(channel: zx::Channel) -> Self { |
| MockPartition { |
| type_guid: EFI_SYSTEM_PART_GUID, |
| mode: MockPartitionMode::Partition, |
| channel, |
| } |
| } |
| |
| /// Handle requests on the supplied channel. |
| pub async fn serve(self) { |
| match self.mode { |
| MockPartitionMode::Partition => self.serve_partition().await, |
| MockPartitionMode::Block => self.serve_block().await, |
| }; |
| } |
| |
| async fn serve_partition(self) { |
| let server_end: ServerEnd<PartitionMarker> = |
| ServerEnd::new(fidl::Channel::from(self.channel)); |
| let mut stream = server_end.into_stream().unwrap(); |
| while let Some(req) = stream.try_next().await.unwrap() { |
| match req { |
| PartitionRequest::GetTypeGuid { responder } => { |
| responder |
| .send( |
| zx::Status::OK.into_raw(), |
| Some(&mut Guid { value: self.type_guid }), |
| ) |
| .expect("Send succeeds"); |
| } |
| _ => panic!("Unsupported request!"), |
| } |
| } |
| } |
| |
| async fn serve_block(self) { |
| let server_end: ServerEnd<BlockMarker> = |
| ServerEnd::new(fidl::Channel::from(self.channel)); |
| let mut stream = server_end.into_stream().unwrap(); |
| while let Ok(Some(_)) = stream.try_next().await { |
| panic!("Did not expect to get any requests!"); |
| } |
| // We expect Err(), because the client speaks the wrong protocol. The connection will |
| // be dropped, which should be handled gracefully by the client. |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_get_guid_succeeds() { |
| let mut fs = ServiceFs::new(); |
| fs.dir("dev").add_service_at("000", |chan| Some(MockPartition::fat(chan))); |
| let (local, remote) = zx::Channel::create().expect("create channel OK"); |
| |
| fs.serve_connection(remote).unwrap(); |
| let _fs_task = fasync::Task::spawn(fs.for_each(|part| async { part.serve().await })); |
| |
| let (dev_dir, remote) = zx::Channel::create().expect("create channel OK"); |
| fdio::open_at(&local, "dev", fidl_fuchsia_io::OPEN_RIGHT_READABLE, remote) |
| .expect("Open OK"); |
| let result = FatDevice::get_guid_at(&dev_dir, "000").await.expect("get guid succeeds"); |
| assert_eq!(result.unwrap().value, MICROSOFT_BASIC_DATA_GUID); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_find_fat_partition_succeeds() { |
| let mut fs = ServiceFs::new(); |
| fs.dir("dev") |
| .add_service_at("000", |chan| Some(MockPartition::not_fat(chan))) |
| .add_service_at("001", |chan| Some(MockPartition::block(chan))) |
| .add_service_at("002", |chan| Some(MockPartition::fat(chan))); |
| let (local, remote) = zx::Channel::create().expect("create channel OK"); |
| |
| fs.serve_connection(remote).unwrap(); |
| let _fs_task = fasync::Task::spawn(fs.for_each(|part| async { part.serve().await })); |
| |
| let (dev_dir, remote) = zx::Channel::create().expect("create channel OK"); |
| fdio::open_at(&local, "dev", fidl_fuchsia_io::OPEN_RIGHT_READABLE, remote) |
| .expect("Open OK"); |
| let dev_dir = DirectoryProxy::new(fidl::AsyncChannel::from_channel(dev_dir).unwrap()); |
| let result = FatDevice::find_fat_partition(&dev_dir).await; |
| assert_eq!(result.expect("Find partition succeeds"), Some("002".to_owned())); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_find_no_fat_partition_succeeds() { |
| let mut fs = ServiceFs::new(); |
| fs.dir("dev") |
| .add_service_at("000", |chan| Some(MockPartition::not_fat(chan))) |
| .add_service_at("001", |chan| Some(MockPartition::block(chan))) |
| .add_service_at("002", |chan| Some(MockPartition::not_fat(chan))); |
| let (local, remote) = zx::Channel::create().expect("create channel OK"); |
| |
| fs.serve_connection(remote).unwrap(); |
| let _fs_task = fasync::Task::spawn(fs.for_each(|part| async { part.serve().await })); |
| |
| let (dev_dir, remote) = zx::Channel::create().expect("create channel OK"); |
| fdio::open_at(&local, "dev", fidl_fuchsia_io::OPEN_RIGHT_READABLE, remote) |
| .expect("Open OK"); |
| let dev_dir = DirectoryProxy::new(fidl::AsyncChannel::from_channel(dev_dir).unwrap()); |
| let result = FatDevice::find_fat_partition(&dev_dir).await; |
| assert_eq!(result.expect("Find partition succeeds"), None); |
| } |
| |
| pub fn create_ramdisk() -> RamdiskClient { |
| isolated_driver_manager::launch_isolated_driver_manager() |
| .expect("Launching isolated driver manager succeeds"); |
| ramdevice_client::wait_for_device("/dev/misc/ramctl", std::time::Duration::from_secs(10)) |
| .expect("ramctl did not appear"); |
| |
| ramdevice_client::RamdiskClientBuilder::new(512, 1024 * 1024) |
| .guid(MICROSOFT_BASIC_DATA_GUID) |
| .build() |
| .expect("Create ramdisk client succeeds") |
| } |
| |
| pub fn format(channel: zx::Channel) { |
| // Create a filesystem on the ramdisk. |
| let device = Box::new( |
| remote_block_device::Cache::new(RemoteBlockDevice::new_sync(channel).unwrap()).unwrap(), |
| ); |
| fatfs::format_volume(device, fatfs::FormatVolumeOptions::new()) |
| .expect("Format volume succeeds"); |
| } |
| |
| pub fn setup_test_fs(channel: zx::Channel, name: &str) { |
| let device = Box::new( |
| remote_block_device::Cache::new(RemoteBlockDevice::new_sync(channel).unwrap()).unwrap(), |
| ); |
| let fs = fatfs::FileSystem::new(device, fatfs::FsOptions::new()) |
| .expect("Create filesystem succeeds"); |
| |
| { |
| let dir = fs.root_dir().create_dir("test").expect("Create dir succeeds"); |
| let mut file = dir.create_file("blah").expect("Create file succeeds"); |
| file.write("hello, world!".as_bytes()).expect("Write succeds"); |
| |
| fs.root_dir().create_dir(name).expect("Create dir succeeds"); |
| } |
| |
| fs.unmount().expect("Unmount succeeds"); |
| } |
| |
| fn open_root(dev: &FatDevice, channel: zx::Channel) { |
| let root = dev.get_root().unwrap(); |
| root.clone().open( |
| dev.scope.clone(), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| 0, |
| Path::empty(), |
| fidl::endpoints::ServerEnd::new(channel), |
| ); |
| root.close().unwrap(); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_mount_device_succeeds() { |
| let ramdisk = create_ramdisk(); |
| let channel = ramdisk.open().expect("Opening ramdisk succeeds"); |
| format(channel); |
| let channel = ramdisk.open().expect("Opening ramdisk succeeds"); |
| setup_test_fs(channel, "mount_device"); |
| |
| let dev = |
| FatDevice::new().await.expect("Create fat device OK").expect("Found a fat device"); |
| |
| let (proxy, remote) = fidl::endpoints::create_proxy::<DirectoryMarker>().unwrap(); |
| open_root(&dev, remote.into_channel()); |
| |
| let mut children: Vec<_> = files_async::readdir(&proxy) |
| .await |
| .expect("Readdir succeeds") |
| .into_iter() |
| .map(|ent| ent.name) |
| .collect(); |
| children.sort(); |
| assert_eq!(children, vec!["mount_device", "test"]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_mount_invalid_device_fails() { |
| // This ramdisk will have the right GUID, but no FAT partition. |
| let _ramdisk = create_ramdisk(); |
| |
| match FatDevice::new().await { |
| Ok(_) => panic!("Expected FatDevice::new() to fail"), |
| Err(_) => {} |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_multiple_devices_opens_first() { |
| let ramdisk1 = create_ramdisk(); |
| let channel = ramdisk1.open().expect("Opening ramdisk succeeds"); |
| format(channel); |
| let channel = ramdisk1.open().expect("Opening ramdisk succeeds"); |
| setup_test_fs(channel, "ramdisk1"); |
| |
| let ramdisk2 = create_ramdisk(); |
| let channel = ramdisk2.open().expect("Opening ramdisk succeeds"); |
| format(channel); |
| let channel = ramdisk2.open().expect("Opening ramdisk succeeds"); |
| setup_test_fs(channel, "ramdisk2"); |
| |
| let dev = |
| FatDevice::new().await.expect("Create fat device OK").expect("Found a fat device"); |
| |
| let (proxy, remote) = fidl::endpoints::create_proxy::<DirectoryMarker>().unwrap(); |
| open_root(&dev, remote.into_channel()); |
| |
| let mut children: Vec<_> = files_async::readdir(&proxy) |
| .await |
| .expect("Readdir succeeds") |
| .into_iter() |
| .map(|ent| ent.name) |
| .collect(); |
| children.sort(); |
| assert_eq!(children, vec!["ramdisk1", "test"]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_multiple_devices_opens_first_when_invalid() { |
| let _ramdisk1 = create_ramdisk(); |
| |
| let ramdisk2 = create_ramdisk(); |
| let channel = ramdisk2.open().expect("Opening ramdisk succeeds"); |
| format(channel); |
| let channel = ramdisk2.open().expect("Opening ramdisk succeeds"); |
| setup_test_fs(channel, "ramdisk2"); |
| |
| // Currently, we expect this to fail, because `ramdisk1` has the right GUID but is not |
| // formatted correctly. |
| match FatDevice::new().await { |
| Ok(_) => panic!("Expected FatDevice::new() to fail"), |
| Err(_) => {} |
| } |
| } |
| } |