blob: 5d7d90869a819d70b81cf1a5833fbdaa7dec84fe [file] [log] [blame]
// 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);
}
}
}