| // 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, InsensitiveStringRef}, |
| node::{Closer, FatNode, Node}, |
| refs::FatfsDirRef, |
| types::{Dir, Disk, FileSystem}, |
| util::fatfs_error_to_status, |
| FATFS_INFO_NAME, MAX_FILENAME_LEN, VFS_TYPE_FATFS, |
| }, |
| anyhow::Error, |
| async_trait::async_trait, |
| fatfs::{self, validate_filename, DefaultTimeProvider, FsOptions, LossyOemCpConverter}, |
| fidl_fuchsia_io as fio, |
| fuchsia_async::{Task, Time, Timer}, |
| fuchsia_zircon::{AsHandleRef, Event}, |
| fuchsia_zircon::{Duration, Status}, |
| std::{ |
| any::Any, |
| marker::PhantomPinned, |
| pin::Pin, |
| sync::{Arc, LockResult, Mutex, MutexGuard}, |
| }, |
| vfs::{ |
| filesystem::{Filesystem, FilesystemRename}, |
| path::Path, |
| }, |
| }; |
| |
| pub struct FatFilesystemInner { |
| filesystem: Option<FileSystem>, |
| // We don't implement unpin: we want `filesystem` to be pinned so that we can be sure |
| // references to filesystem objects (see refs.rs) will remain valid across different locks. |
| _pinned: PhantomPinned, |
| } |
| |
| impl FatFilesystemInner { |
| /// Get the root fatfs Dir. |
| pub fn root_dir(&self) -> Dir<'_> { |
| self.filesystem.as_ref().unwrap().root_dir() |
| } |
| |
| pub fn with_disk<F, T>(&self, func: F) -> T |
| where |
| F: FnOnce(&Box<dyn Disk>) -> T, |
| { |
| self.filesystem.as_ref().unwrap().with_disk(func) |
| } |
| |
| pub fn shut_down(&mut self) -> Result<(), Status> { |
| self.filesystem.take().ok_or(Status::BAD_STATE)?.unmount().map_err(fatfs_error_to_status) |
| } |
| |
| pub fn cluster_size(&self) -> u32 { |
| self.filesystem.as_ref().map_or(0, |f| f.cluster_size()) |
| } |
| |
| pub fn total_clusters(&self) -> Result<u32, Status> { |
| Ok(self |
| .filesystem |
| .as_ref() |
| .ok_or(Status::BAD_STATE)? |
| .stats() |
| .map_err(fatfs_error_to_status)? |
| .total_clusters()) |
| } |
| |
| pub fn free_clusters(&self) -> Result<u32, Status> { |
| Ok(self |
| .filesystem |
| .as_ref() |
| .ok_or(Status::BAD_STATE)? |
| .stats() |
| .map_err(fatfs_error_to_status)? |
| .free_clusters()) |
| } |
| |
| pub fn sector_size(&self) -> Result<u16, Status> { |
| Ok(self |
| .filesystem |
| .as_ref() |
| .ok_or(Status::BAD_STATE)? |
| .stats() |
| .map_err(fatfs_error_to_status)? |
| .sector_size()) |
| } |
| } |
| |
| pub struct FatFilesystem { |
| inner: Mutex<FatFilesystemInner>, |
| dirty_task: Mutex<Option<(Time, Task<()>)>>, |
| fs_id: Event, |
| } |
| |
| impl FatFilesystem { |
| /// Create a new FatFilesystem. |
| pub fn new( |
| disk: Box<dyn Disk>, |
| options: FsOptions<DefaultTimeProvider, LossyOemCpConverter>, |
| ) -> Result<(Pin<Arc<Self>>, Arc<FatDirectory>), Error> { |
| let inner = Mutex::new(FatFilesystemInner { |
| filesystem: Some(fatfs::FileSystem::new(disk, options)?), |
| _pinned: PhantomPinned, |
| }); |
| let result = Arc::pin(FatFilesystem { |
| inner, |
| dirty_task: Mutex::new(None), |
| fs_id: Event::create()?, |
| }); |
| Ok((result.clone(), result.root_dir())) |
| } |
| |
| #[cfg(test)] |
| pub fn from_filesystem(filesystem: FileSystem) -> (Pin<Arc<Self>>, Arc<FatDirectory>) { |
| let inner = |
| Mutex::new(FatFilesystemInner { filesystem: Some(filesystem), _pinned: PhantomPinned }); |
| let result = Arc::pin(FatFilesystem { |
| inner, |
| dirty_task: Mutex::new(None), |
| fs_id: Event::create().unwrap(), |
| }); |
| (result.clone(), result.root_dir()) |
| } |
| |
| pub fn fs_id(&self) -> &Event { |
| &self.fs_id |
| } |
| |
| /// Get the FatDirectory that represents the root directory of this filesystem. |
| /// Note this should only be called once per filesystem, otherwise multiple conflicting |
| /// FatDirectories will exist. |
| /// We only call it from new() and from_filesystem(). |
| fn root_dir(self: Pin<Arc<Self>>) -> Arc<FatDirectory> { |
| // We start with an empty FatfsDirRef and an open_count of zero. |
| let dir = FatfsDirRef::empty(); |
| FatDirectory::new(dir, None, self, "/".to_owned()) |
| } |
| |
| /// Try and lock the underlying filesystem. Returns a LockResult, see `Mutex::lock`. |
| pub fn lock(&self) -> LockResult<MutexGuard<'_, FatFilesystemInner>> { |
| self.inner.lock() |
| } |
| |
| /// Mark the filesystem as dirty. This will cause the disk to automatically be flushed after |
| /// one second, and cancel any previous pending flushes. |
| pub fn mark_dirty(self: &Pin<Arc<Self>>) { |
| let deadline = Time::after(Duration::from_seconds(1)); |
| match &mut *self.dirty_task.lock().unwrap() { |
| Some((time, _)) => *time = deadline, |
| x @ None => { |
| let this = self.clone(); |
| *x = Some(( |
| deadline, |
| Task::spawn(async move { |
| loop { |
| let deadline; |
| { |
| let mut task = this.dirty_task.lock().unwrap(); |
| deadline = task.as_ref().unwrap().0; |
| if Time::now() >= deadline { |
| *task = None; |
| break; |
| } |
| } |
| Timer::new(deadline).await; |
| } |
| let _ = this.lock().unwrap().filesystem.as_ref().map(|f| f.flush()); |
| }), |
| )); |
| } |
| } |
| } |
| |
| /// Do a simple rename of the file, without unlinking dst. |
| /// This assumes that either "dst" and "src" are the same file, or that "dst" has already been |
| /// unlinked. |
| fn rename_internal( |
| &self, |
| filesystem: &FatFilesystemInner, |
| src_dir: &Arc<FatDirectory>, |
| src_name: &str, |
| dst_dir: &Arc<FatDirectory>, |
| dst_name: &str, |
| existing: ExistingRef<'_, '_>, |
| ) -> Result<(), Status> { |
| // We're ready to go: remove the entry from the source cache, and close the reference to |
| // the underlying file (this ensures all pending writes, etc. have been flushed). |
| // We remove the entry with rename() below, and hold the filesystem lock so nothing will |
| // put the entry back in the cache. After renaming we also re-attach the entry to its |
| // parent. |
| |
| // Do the rename. |
| let src_fatfs_dir = src_dir.borrow_dir(&filesystem)?; |
| let dst_fatfs_dir = dst_dir.borrow_dir(&filesystem)?; |
| |
| match existing { |
| ExistingRef::None => { |
| src_fatfs_dir |
| .rename(src_name, &dst_fatfs_dir, dst_name) |
| .map_err(fatfs_error_to_status)?; |
| } |
| ExistingRef::File(file) => { |
| src_fatfs_dir |
| .rename_over_file(src_name, &dst_fatfs_dir, dst_name, file) |
| .map_err(fatfs_error_to_status)?; |
| } |
| ExistingRef::Dir(dir) => { |
| src_fatfs_dir |
| .rename_over_dir(src_name, &dst_fatfs_dir, dst_name, dir) |
| .map_err(fatfs_error_to_status)?; |
| } |
| } |
| |
| src_dir.did_remove(src_name); |
| dst_dir.did_add(dst_name); |
| |
| src_dir.fs().mark_dirty(); |
| |
| // TODO: do the watcher event for existing. |
| |
| Ok(()) |
| } |
| |
| /// Helper for rename which returns FatNodes that need to be dropped without the fs lock held. |
| fn rename_locked( |
| &self, |
| filesystem: &FatFilesystemInner, |
| src_dir: &Arc<FatDirectory>, |
| src_name: &str, |
| dst_dir: &Arc<FatDirectory>, |
| dst_name: &str, |
| src_is_dir: bool, |
| closer: &mut Closer<'_>, |
| ) -> Result<(), Status> { |
| // Renaming a file to itself is trivial, but we do it after we've checked that the file |
| // exists and that src and dst have the same type. |
| if Arc::ptr_eq(&src_dir, &dst_dir) |
| && (&src_name as &dyn InsensitiveStringRef) == (&dst_name as &dyn InsensitiveStringRef) |
| { |
| if src_name != dst_name { |
| // Cases don't match - we don't unlink, but we still need to fix the file's LFN. |
| return self.rename_internal( |
| &filesystem, |
| src_dir, |
| src_name, |
| dst_dir, |
| dst_name, |
| ExistingRef::None, |
| ); |
| } |
| return Ok(()); |
| } |
| |
| if let Some(src_node) = src_dir.cache_get(src_name) { |
| // We can't move a directory into itself. |
| if let FatNode::Dir(ref dir) = src_node { |
| if Arc::ptr_eq(&dir, &dst_dir) { |
| return Err(Status::INVALID_ARGS); |
| } |
| } |
| src_node.flush_dir_entry(filesystem)?; |
| } |
| |
| let mut dir; |
| let mut file; |
| let mut existing_node = dst_dir.cache_get(dst_name); |
| let existing = match existing_node { |
| None => { |
| dst_dir.open_ref(filesystem)?; |
| closer.add(FatNode::Dir(dst_dir.clone())); |
| match dst_dir.find_child(filesystem, dst_name)? { |
| Some(ref dir_entry) => { |
| if dir_entry.is_dir() { |
| dir = Some(dir_entry.to_dir()); |
| ExistingRef::Dir(dir.as_mut().unwrap()) |
| } else { |
| file = Some(dir_entry.to_file()); |
| ExistingRef::File(file.as_mut().unwrap()) |
| } |
| } |
| None => ExistingRef::None, |
| } |
| } |
| Some(ref mut node) => { |
| node.open_ref(filesystem)?; |
| closer.add(node.clone()); |
| match node { |
| FatNode::Dir(ref mut node_dir) => { |
| ExistingRef::Dir(node_dir.borrow_dir_mut(filesystem).unwrap()) |
| } |
| FatNode::File(ref mut node_file) => { |
| ExistingRef::File(node_file.borrow_file_mut(filesystem).unwrap()) |
| } |
| } |
| } |
| }; |
| |
| match existing { |
| ExistingRef::File(_) => { |
| if src_is_dir { |
| return Err(Status::NOT_DIR); |
| } |
| } |
| ExistingRef::Dir(_) => { |
| if !src_is_dir { |
| return Err(Status::NOT_FILE); |
| } |
| } |
| ExistingRef::None => {} |
| } |
| |
| self.rename_internal(&filesystem, src_dir, src_name, dst_dir, dst_name, existing)?; |
| |
| if let Some(_) = existing_node { |
| dst_dir.cache_remove(&filesystem, &dst_name).unwrap().did_delete(); |
| } |
| |
| // We suceeded in renaming, so now move the nodes around. |
| if let Some(node) = src_dir.remove_child(&filesystem, &src_name) { |
| dst_dir |
| .add_child(&filesystem, dst_name.to_owned(), node) |
| .unwrap_or_else(|e| panic!("Rename failed, but fatfs says it didn't? - {:?}", e)); |
| } |
| |
| Ok(()) |
| } |
| |
| pub fn query_filesystem(&self) -> Result<fio::FilesystemInfo, Status> { |
| let fs_lock = self.lock().unwrap(); |
| |
| let cluster_size = fs_lock.cluster_size() as u64; |
| let total_clusters = fs_lock.total_clusters()? as u64; |
| let free_clusters = fs_lock.free_clusters()? as u64; |
| let total_bytes = cluster_size * total_clusters; |
| let used_bytes = cluster_size * (total_clusters - free_clusters); |
| |
| Ok(fio::FilesystemInfo { |
| total_bytes, |
| used_bytes, |
| total_nodes: 0, |
| used_nodes: 0, |
| free_shared_pool_bytes: 0, |
| fs_id: self.fs_id().get_koid()?.raw_koid(), |
| block_size: cluster_size as u32, |
| max_filename_size: MAX_FILENAME_LEN, |
| fs_type: VFS_TYPE_FATFS, |
| padding: 0, |
| name: FATFS_INFO_NAME, |
| }) |
| } |
| } |
| |
| #[async_trait] |
| impl FilesystemRename for FatFilesystem { |
| async fn rename( |
| &self, |
| src_dir: Arc<dyn Any + Sync + Send + 'static>, |
| src_path: Path, |
| dst_dir: Arc<dyn Any + Sync + Send + 'static>, |
| dst_path: Path, |
| ) -> Result<(), Status> { |
| let src_dir = src_dir.downcast::<FatDirectory>().map_err(|_| Status::INVALID_ARGS)?; |
| let dst_dir = dst_dir.downcast::<FatDirectory>().map_err(|_| Status::INVALID_ARGS)?; |
| if dst_dir.is_deleted() { |
| // Can't rename into a deleted folder. |
| return Err(Status::NOT_FOUND); |
| } |
| |
| let src_name = src_path.peek().unwrap(); |
| validate_filename(src_name).map_err(fatfs_error_to_status)?; |
| let dst_name = dst_path.peek().unwrap(); |
| validate_filename(dst_name).map_err(fatfs_error_to_status)?; |
| |
| let mut closer = Closer::new(&self); |
| let filesystem = self.inner.lock().unwrap(); |
| |
| // Figure out if src is a directory. |
| let entry = src_dir.find_child(&filesystem, &src_name)?; |
| if entry.is_none() { |
| // No such src (if we don't return NOT_FOUND here, fatfs will return it when we |
| // call rename() later). |
| return Err(Status::NOT_FOUND); |
| } |
| let src_is_dir = entry.unwrap().is_dir(); |
| if (dst_path.is_dir() || src_path.is_dir()) && !src_is_dir { |
| // The caller wanted a directory (src or dst), but src is not a directory. This is |
| // an error. |
| return Err(Status::NOT_DIR); |
| } |
| |
| self.rename_locked( |
| &filesystem, |
| &src_dir, |
| src_name, |
| &dst_dir, |
| dst_name, |
| src_is_dir, |
| &mut closer, |
| ) |
| } |
| } |
| |
| impl Filesystem for FatFilesystem { |
| fn block_size(&self) -> u32 { |
| self.inner.lock().unwrap().cluster_size() |
| } |
| } |
| |
| pub(crate) enum ExistingRef<'a, 'b> { |
| None, |
| File(&'a mut crate::types::File<'b>), |
| Dir(&'a mut crate::types::Dir<'b>), |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::tests::{TestDiskContents, TestFatDisk}, |
| fidl::endpoints::Proxy, |
| scopeguard::defer, |
| vfs::{directory::entry::DirectoryEntry, execution_scope::ExecutionScope, path::Path}, |
| }; |
| |
| const TEST_DISK_SIZE: u64 = 2048 << 10; // 2048K |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| #[ignore] // TODO(fxbug.dev/56138): Clean up tasks to prevent panic on drop in FatfsFileRef |
| async fn test_automatic_flush() { |
| let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE); |
| let structure = TestDiskContents::dir().add_child("test", "Hello".into()); |
| structure.create(&disk.root_dir()); |
| |
| let fs = disk.into_fatfs(); |
| let dir = fs.get_fatfs_root(); |
| dir.open_ref(&fs.filesystem().lock().unwrap()).unwrap(); |
| defer! { dir.close_ref(&fs.filesystem().lock().unwrap()) }; |
| |
| let scope = ExecutionScope::new(); |
| let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>().unwrap(); |
| dir.clone().open( |
| scope.clone(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE, |
| 0, |
| Path::validate_and_split("test").unwrap(), |
| server_end, |
| ); |
| |
| assert!(fs.filesystem().dirty_task.lock().unwrap().is_none()); |
| let file = fio::FileProxy::new(proxy.into_channel().unwrap()); |
| file.write("hello there".as_bytes()).await.unwrap().map_err(Status::from_raw).unwrap(); |
| { |
| let fs_lock = fs.filesystem().lock().unwrap(); |
| // fs should be dirty until the timer expires. |
| assert!(fs_lock.filesystem.as_ref().unwrap().is_dirty()); |
| } |
| // Wait some time for the flush to happen. Don't hold the lock while waiting, otherwise |
| // the flush will get stuck waiting on the lock. |
| Timer::new(Time::after(Duration::from_millis(1500))).await; |
| { |
| let fs_lock = fs.filesystem().lock().unwrap(); |
| assert_eq!(fs_lock.filesystem.as_ref().unwrap().is_dirty(), false); |
| } |
| } |
| } |