blob: 1fc7f2fd1e09cae947ed8f03e959f5ba37f7528e [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::{filesystem::FatFilesystem, node::Node},
anyhow::Error,
fatfs::FsOptions,
fidl_fuchsia_fs::{AdminRequest, AdminShutdownResponder},
fuchsia_zircon::Status,
std::pin::Pin,
std::sync::Arc,
vfs::{
directory::{entry::DirectoryEntry, entry_container::Directory},
execution_scope::ExecutionScope,
},
};
mod directory;
mod file;
mod filesystem;
mod node;
mod refs;
mod types;
mod util;
pub use {directory::FatDirectory, util::fatfs_error_to_status};
#[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.
pub const MAX_FILENAME_LEN: u32 = 255;
// An array used to initialize the FilesystemInfo |name| field. This just spells "fatfs" 0-padded to
// 32 bytes.
pub const FATFS_INFO_NAME: [i8; 32] = [
0x66, 0x61, 0x74, 0x66, 0x73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
];
pub trait RootDirectory: DirectoryEntry + Directory {}
impl<T: DirectoryEntry + Directory> RootDirectory for T {}
pub struct FatFs {
inner: Pin<Arc<FatFilesystem>>,
root: Arc<FatDirectory>,
}
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 })
}
#[cfg(test)]
pub fn from_filesystem(inner: Pin<Arc<FatFilesystem>>, root: Arc<FatDirectory>) -> Self {
FatFs { inner, root }
}
#[cfg(any(test, fuzz))]
pub fn get_fatfs_root(&self) -> Arc<FatDirectory> {
self.root.clone()
}
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<FatDirectory>, Status> {
// Make sure it's open.
self.root.open_ref(&self.inner.lock().unwrap())?;
Ok(self.root.clone())
}
pub async fn handle_admin(
&self,
scope: &ExecutionScope,
req: AdminRequest,
) -> Option<AdminShutdownResponder> {
match req {
AdminRequest::Shutdown { responder } => {
scope.shutdown();
Some(responder)
}
}
}
/// 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},
fatfs::{format_volume, FormatVolumeOptions},
fidl::endpoints::Proxy,
fidl_fuchsia_io as fio,
futures::{future::BoxFuture, prelude::*},
std::{collections::HashMap, io::Write, ops::Deref},
vfs::{node::Node, 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_all(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: fio::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 = fio::FileProxy::new(remote.into_channel().unwrap());
let mut file_contents: Vec<u8> = Vec::with_capacity(content.len());
return async move {
loop {
let mut vec = remote
.read(content.len() as u64)
.await
.context("Read failed")?
.map_err(Status::from_raw)
.context("Read error")?;
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 = fio::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::<fio::NodeMarker>().unwrap();
remote
.open(
fio::OpenFlags::RIGHT_READABLE,
fio::ModeType::empty(),
name,
server_end,
)
.context("Sending open failed")?;
value
.verify(proxy)
.await
.with_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;
#[fuchsia::test]
#[ignore] // TODO(https://fxbug.dev/42133844): 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::<fio::NodeMarker>().unwrap();
let root = fatfs.get_root().expect("get_root OK");
root.clone().open(scope, fio::OpenFlags::RIGHT_READABLE, Path::dot(), remote);
root.close();
structure.verify(proxy).await.expect("Verify succeeds");
}
#[fuchsia::test]
fn test_unset_date() {
let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE);
// FAT doesn't give the root directory a created/modified/access time,
// so this is a good way to check that we return valid dates for a "zero" date.
let root = disk.root_dir();
let epoch = fatfs::DateTime {
date: fatfs::Date { year: 1980, month: 1, day: 1 },
time: fatfs::Time { hour: 0, min: 0, sec: 0, millis: 0 },
};
assert_eq!(root.created(), epoch);
assert_eq!(root.modified(), epoch);
assert_eq!(root.accessed(), epoch.date);
}
}