| // 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 { |
| crate::{directory::FatDirectory, filesystem::FatFilesystem, node::Node}, |
| anyhow::Error, |
| fatfs::FsOptions, |
| fidl_fuchsia_fs::{AdminRequest, FilesystemInfo, FilesystemInfoQuery, FsType, QueryRequest}, |
| fidl_fuchsia_io::{OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE}, |
| fuchsia_syslog::{fx_log_err, fx_log_warn}, |
| fuchsia_zircon::{Event, HandleBased, Rights, Status}, |
| std::pin::Pin, |
| std::sync::Arc, |
| vfs::{ |
| directory::{entry::DirectoryEntry, entry_container::Directory}, |
| execution_scope::ExecutionScope, |
| path::Path, |
| }, |
| }; |
| |
| mod directory; |
| mod file; |
| mod filesystem; |
| mod node; |
| mod refs; |
| mod types; |
| mod util; |
| |
| #[cfg(fuzz)] |
| mod fuzzer; |
| #[cfg(fuzz)] |
| use fuzz::fuzz; |
| #[cfg(fuzz)] |
| #[fuzz] |
| fn fuzz_fatfs(fs: &[u8]) { |
| fuzzer::fuzz_fatfs(fs); |
| } |
| |
| pub use types::Disk; |
| |
| /// Number of UCS-2 characters that fit in a VFAT LFN. |
| /// Note that FAT doesn't support the full range of Unicode characters (UCS-2 is only 16 bits), |
| /// and short file names can't encode the full 16-bit range of UCS-2. |
| /// This is the minimum possible value. For instance, a 300 byte UTF-8 string could fit inside 255 |
| /// UCS-2 codepoints (if it had some 16 bit characters), but a 300 byte ASCII string would not fit. |
| const MAX_FILENAME_LEN: u32 = 255; |
| |
| pub trait RootDirectory: DirectoryEntry + Directory {} |
| impl<T: DirectoryEntry + Directory> RootDirectory for T {} |
| |
| pub struct FatFs { |
| inner: Pin<Arc<FatFilesystem>>, |
| root: Arc<FatDirectory>, |
| fs_id: Event, |
| } |
| |
| impl FatFs { |
| /// Create a new FatFs using the given ReadWriteSeek as the disk. |
| pub fn new(disk: Box<dyn Disk>) -> Result<Self, Error> { |
| let (inner, root) = FatFilesystem::new(disk, FsOptions::new())?; |
| Ok(FatFs { inner, root, fs_id: Event::create()? }) |
| } |
| |
| #[cfg(test)] |
| pub fn from_filesystem(inner: Pin<Arc<FatFilesystem>>, root: Arc<FatDirectory>) -> Self { |
| FatFs { inner, root, fs_id: Event::create().expect("Event::create succeeds") } |
| } |
| |
| #[cfg(any(test, fuzz))] |
| pub fn get_fatfs_root(&self) -> Arc<FatDirectory> { |
| self.root.clone() |
| } |
| |
| #[cfg(any(test, fuzz))] |
| pub fn filesystem(&self) -> &FatFilesystem { |
| return &self.inner; |
| } |
| |
| pub fn is_present(&self) -> bool { |
| self.inner.lock().unwrap().with_disk(|disk| disk.is_present()) |
| } |
| |
| /// Get the root directory of this filesystem. |
| /// The caller must call close() on the returned entry when it's finished with it. |
| pub fn get_root(&self) -> Result<Arc<dyn RootDirectory>, Status> { |
| // Make sure it's open. |
| self.root.open_ref(&self.inner.lock().unwrap())?; |
| Ok(self.root.clone()) |
| } |
| |
| fn get_info(&self, query: FilesystemInfoQuery) -> Result<FilesystemInfo, Status> { |
| let mut result = FilesystemInfo::EMPTY; |
| let fs_lock = self.inner.lock().unwrap(); |
| if query.contains(FilesystemInfoQuery::TotalBytes) |
| || query.contains(FilesystemInfoQuery::UsedBytes) |
| { |
| let cluster_size = fs_lock.cluster_size() as u64; |
| let total_clusters = fs_lock.total_clusters()? as u64; |
| if query.contains(FilesystemInfoQuery::TotalBytes) { |
| result.total_bytes = Some(cluster_size * total_clusters); |
| } |
| |
| if query.contains(FilesystemInfoQuery::UsedBytes) { |
| let free_clusters = fs_lock.free_clusters()? as u64; |
| result.used_bytes = Some(cluster_size * (total_clusters - free_clusters)); |
| } |
| } |
| |
| if query.contains(FilesystemInfoQuery::FsId) { |
| result.fs_id = Some(self.fs_id.duplicate_handle(Rights::BASIC)?); |
| } |
| |
| if query.contains(FilesystemInfoQuery::BlockSize) { |
| result.block_size = Some(fs_lock.sector_size()? as u32); |
| } |
| |
| if query.contains(FilesystemInfoQuery::MaxNodeNameSize) { |
| result.max_node_name_size = Some(MAX_FILENAME_LEN); |
| } |
| |
| if query.contains(FilesystemInfoQuery::FsType) { |
| result.fs_type = Some(FsType::Fatfs); |
| } |
| |
| if query.contains(FilesystemInfoQuery::Name) { |
| result.name = Some("fatfs".to_owned()); |
| } |
| |
| Ok(result) |
| } |
| |
| pub fn handle_query(&self, scope: &ExecutionScope, req: QueryRequest) -> Result<(), Error> { |
| match req { |
| QueryRequest::IsNodeInFilesystem { token, responder } => { |
| let result = match scope.token_registry().unwrap().get_container(token.into()) { |
| Ok(Some(_)) => true, |
| _ => false, |
| }; |
| responder.send(result)?; |
| } |
| QueryRequest::GetInfo { query, responder } => { |
| responder.send(&mut self.get_info(query).map_err(|e| e.into_raw()))?; |
| } |
| }; |
| Ok(()) |
| } |
| |
| pub fn handle_admin(&self, scope: &ExecutionScope, req: AdminRequest) -> Result<(), Error> { |
| match req { |
| AdminRequest::Shutdown { responder } => { |
| scope.shutdown(); |
| self.shut_down().unwrap_or_else(|e| fx_log_err!("Shutdown failed {:?}", e)); |
| responder.send()?; |
| } |
| AdminRequest::GetRoot { dir, .. } => { |
| let root = match self.get_root() { |
| Ok(root) => root, |
| Err(e) => { |
| dir.close_with_epitaph(e)?; |
| return Ok(()); |
| } |
| }; |
| |
| root.clone().open( |
| scope.clone(), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| 0, |
| Path::empty(), |
| fidl::endpoints::ServerEnd::new(dir.into_channel()), |
| ); |
| |
| root.close() |
| .unwrap_or_else(|e| fx_log_warn!("Failed to close root directory: {:?}", e)); |
| } |
| }; |
| Ok(()) |
| } |
| |
| /// Shut down the filesystem. |
| pub fn shut_down(&self) -> Result<(), Status> { |
| let mut fs = self.inner.lock().unwrap(); |
| self.root.shut_down(&fs)?; |
| fs.shut_down() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::types::{Dir, FileSystem}, |
| anyhow::{anyhow, Context, Error}, |
| fatfs::{format_volume, FormatVolumeOptions, FsOptions}, |
| fidl::endpoints::Proxy, |
| fidl_fuchsia_io::{DirectoryProxy, FileProxy, NodeMarker, NodeProxy}, |
| fuchsia_async as fasync, |
| fuchsia_zircon::{AsHandleRef, Status}, |
| futures::{future::BoxFuture, prelude::*}, |
| std::{collections::HashMap, io::Write, ops::Deref}, |
| vfs::{execution_scope::ExecutionScope, path::Path}, |
| }; |
| |
| #[derive(Debug, PartialEq)] |
| /// Helper class for creating a filesystem layout on a FAT disk programatically. |
| pub enum TestDiskContents { |
| File(String), |
| Dir(HashMap<String, TestDiskContents>), |
| } |
| |
| impl From<&str> for TestDiskContents { |
| fn from(string: &str) -> Self { |
| TestDiskContents::File(string.to_owned()) |
| } |
| } |
| |
| impl TestDiskContents { |
| /// Create a new, empty directory. |
| pub fn dir() -> Self { |
| TestDiskContents::Dir(HashMap::new()) |
| } |
| |
| /// Add a new child to this directory. |
| pub fn add_child(mut self, name: &str, child: Self) -> Self { |
| match &mut self { |
| TestDiskContents::Dir(map) => map.insert(name.to_owned(), child), |
| _ => panic!("Can't add to a file"), |
| }; |
| self |
| } |
| |
| /// Add this TestDiskContents to the given fatfs Dir |
| pub fn create(&self, dir: &Dir<'_>) { |
| match self { |
| TestDiskContents::File(_) => { |
| panic!("Can't have the root directory be a file!"); |
| } |
| TestDiskContents::Dir(map) => { |
| for (name, value) in map.iter() { |
| value.create_fs_structure(&name, dir); |
| } |
| } |
| }; |
| } |
| |
| fn create_fs_structure(&self, name: &str, dir: &Dir<'_>) { |
| match self { |
| TestDiskContents::File(content) => { |
| let mut file = dir.create_file(name).expect("Creating file to succeed"); |
| file.truncate().expect("Truncate to succeed"); |
| file.write(content.as_bytes()).expect("Write to succeed"); |
| } |
| TestDiskContents::Dir(map) => { |
| let new_dir = dir.create_dir(name).expect("Creating directory to succeed"); |
| for (name, value) in map.iter() { |
| value.create_fs_structure(&name, &new_dir); |
| } |
| } |
| }; |
| } |
| |
| pub fn verify(&self, remote: NodeProxy) -> BoxFuture<'_, Result<(), Error>> { |
| // Unfortunately, there is no way to verify from the server side, so we use |
| // the fuchsia.io protocol to check everything is as expected. |
| match self { |
| TestDiskContents::File(content) => { |
| let remote = FileProxy::new(remote.into_channel().unwrap()); |
| let mut file_contents: Vec<u8> = Vec::with_capacity(content.len()); |
| |
| return async move { |
| loop { |
| let (status, mut vec) = |
| remote.read(content.len() as u64).await.context("Read failed")?; |
| let status = Status::from_raw(status); |
| if status != Status::OK { |
| // Note that we don't assert here to make the error message nicer. |
| return Err(anyhow!("Failed to read: {:?}", status)); |
| } |
| if vec.len() == 0 { |
| break; |
| } |
| file_contents.append(&mut vec); |
| } |
| |
| if file_contents.as_slice() != content.as_bytes() { |
| return Err(anyhow!( |
| "File contents mismatch: expected {}, got {}", |
| content, |
| String::from_utf8_lossy(&file_contents) |
| )); |
| } |
| Ok(()) |
| } |
| .boxed(); |
| } |
| TestDiskContents::Dir(map) => { |
| let remote = DirectoryProxy::new(remote.into_channel().unwrap()); |
| // TODO(simonshields): we should check that no other files exist, but |
| // GetDirents() is going to be a pain to deal with. |
| |
| return async move { |
| for (name, value) in map.iter() { |
| let (proxy, server_end) = |
| fidl::endpoints::create_proxy::<NodeMarker>().unwrap(); |
| remote |
| .open(OPEN_RIGHT_READABLE, 0, name, server_end) |
| .context("Sending open failed")?; |
| value.verify(proxy).await.context(format!("Verifying {}", name))?; |
| } |
| Ok(()) |
| } |
| .boxed(); |
| } |
| } |
| } |
| } |
| |
| /// Helper class for creating an empty FAT-formatted VMO. |
| pub struct TestFatDisk { |
| fs: FileSystem, |
| } |
| |
| impl TestFatDisk { |
| /// Create an empty disk with size at least |size| bytes. |
| pub fn empty_disk(size: u64) -> Self { |
| let mut buffer: Vec<u8> = Vec::with_capacity(size as usize); |
| buffer.resize(size as usize, 0); |
| let cursor = std::io::Cursor::new(buffer.as_mut_slice()); |
| |
| format_volume(cursor, FormatVolumeOptions::new()).expect("format volume to succeed"); |
| let wrapper: Box<dyn Disk> = Box::new(std::io::Cursor::new(buffer)); |
| TestFatDisk { |
| fs: fatfs::FileSystem::new(wrapper, FsOptions::new()) |
| .expect("creating FS to succeed"), |
| } |
| } |
| |
| /// Get the root directory (as a fatfs Dir). |
| pub fn root_dir<'a>(&'a self) -> Dir<'a> { |
| self.fs.root_dir() |
| } |
| |
| /// Convert this TestFatDisk into a FatFs for testing against. |
| pub fn into_fatfs(self) -> FatFs { |
| self.fs.flush().unwrap(); |
| let (filesystem, root_dir) = FatFilesystem::from_filesystem(self.fs); |
| FatFs::from_filesystem(filesystem, root_dir) |
| } |
| } |
| |
| impl Deref for TestFatDisk { |
| type Target = FileSystem; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.fs |
| } |
| } |
| |
| const TEST_DISK_SIZE: u64 = 2048 << 10; |
| |
| #[fasync::run_singlethreaded(test)] |
| #[ignore] // TODO(fxbug.dev/56138): Clean up tasks to prevent panic on drop in FatfsFileRef |
| async fn test_create_disk() { |
| let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE); |
| |
| let structure = TestDiskContents::dir() |
| .add_child("test", "This is a test file".into()) |
| .add_child("empty_folder", TestDiskContents::dir()); |
| |
| structure.create(&disk.root_dir()); |
| |
| let fatfs = disk.into_fatfs(); |
| let scope = ExecutionScope::new(); |
| let (proxy, remote) = fidl::endpoints::create_proxy::<NodeMarker>().unwrap(); |
| let root = fatfs.get_root().expect("get_root OK"); |
| root.clone().open(scope, OPEN_RIGHT_READABLE, 0, Path::empty(), remote); |
| root.close().expect("Close OK"); |
| |
| structure.verify(proxy).await.expect("Verify succeeds"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_get_info_empty_disk() { |
| let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE); |
| let fatfs = disk.into_fatfs(); |
| |
| let result = fatfs |
| .get_info(FilesystemInfoQuery::all() - FilesystemInfoQuery::FsId) |
| .expect("get_info succeeds"); |
| assert_eq!( |
| result, |
| FilesystemInfo { |
| // An empty FAT disk formatted by fatfs uses 46 sectors for filesystem data: |
| // * 32 reserved sectors (for the BPB, etc.) |
| // * 7 sectors for each of the two FATs. |
| total_bytes: Some(TEST_DISK_SIZE - (46 * 512)), |
| used_bytes: Some(0), |
| total_nodes: None, |
| used_nodes: None, |
| free_shared_pool_bytes: None, |
| fs_id: None, |
| block_size: Some(512), |
| max_node_name_size: Some(MAX_FILENAME_LEN), |
| fs_type: Some(FsType::Fatfs), |
| name: Some("fatfs".to_string()), |
| device_path: None, |
| ..FilesystemInfo::EMPTY |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_get_info_not_empty_disk() { |
| let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE); |
| let contents = TestDiskContents::dir().add_child("file", "some text".into()); |
| contents.create(&disk.root_dir()); |
| let cluster_size = disk.cluster_size(); |
| let fatfs = disk.into_fatfs(); |
| |
| let result = fatfs.get_info(FilesystemInfoQuery::UsedBytes).expect("get_info succeeds"); |
| assert_eq!( |
| result, |
| FilesystemInfo { |
| total_bytes: None, |
| // 1 cluster allocated for the file. |
| used_bytes: Some(cluster_size as u64), |
| total_nodes: None, |
| used_nodes: None, |
| free_shared_pool_bytes: None, |
| fs_id: None, |
| block_size: None, |
| max_node_name_size: None, |
| fs_type: None, |
| name: None, |
| device_path: None, |
| ..FilesystemInfo::EMPTY |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_get_info_fs_id() { |
| let fatfs = TestFatDisk::empty_disk(TEST_DISK_SIZE).into_fatfs(); |
| let result = fatfs.get_info(FilesystemInfoQuery::FsId).expect("get_info succeeds"); |
| let result2 = fatfs.get_info(FilesystemInfoQuery::FsId).expect("get_info succeeds"); |
| let koid = result.fs_id.unwrap().get_koid().expect("get_koid succeeds"); |
| let koid2 = result2.fs_id.unwrap().get_koid().expect("get_koid succeeds"); |
| assert_eq!(koid, koid2); |
| } |
| } |