| // 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, FatFilesystemInner}, |
| node::Node, |
| refs::FatfsFileRef, |
| types::File, |
| util::{dos_to_unix_time, fatfs_error_to_status, unix_to_dos_time}, |
| }, |
| async_trait::async_trait, |
| fidl_fuchsia_io as fio, |
| fuchsia_zircon::{self as zx, Status}, |
| libc::{S_IRUSR, S_IWUSR}, |
| std::{ |
| cell::UnsafeCell, |
| fmt::Debug, |
| io::{Read, Seek, Write}, |
| pin::Pin, |
| sync::{Arc, RwLock}, |
| }, |
| vfs::{ |
| attributes, |
| execution_scope::ExecutionScope, |
| file::{FidlIoConnection, File as VfsFile, FileIo as VfsFileIo, FileOptions, SyncMode}, |
| ObjectRequestRef, |
| }, |
| }; |
| |
| fn extend(file: &mut File<'_>, mut current: u64, target: u64) -> Result<(), Status> { |
| let zeros = vec![0; 8192]; |
| while current < target { |
| let to_do = (std::cmp::min(target, (current + 8192) / 8192 * 8192) - current) as usize; |
| let written = file.write(&zeros[..to_do]).map_err(fatfs_error_to_status)? as u64; |
| if written == 0 { |
| return Err(Status::NO_SPACE); |
| } |
| current += written; |
| } |
| Ok(()) |
| } |
| |
| fn seek_for_write(file: &mut File<'_>, offset: u64) -> Result<(), Status> { |
| if offset > fatfs::MAX_FILE_SIZE as u64 { |
| return Err(Status::INVALID_ARGS); |
| } |
| let real_offset = file.seek(std::io::SeekFrom::Start(offset)).map_err(fatfs_error_to_status)?; |
| if real_offset == offset { |
| return Ok(()); |
| } |
| assert!(real_offset < offset); |
| let result = extend(file, real_offset, offset); |
| if let Err(e) = result { |
| // Return the file to its original size. |
| file.seek(std::io::SeekFrom::Start(real_offset)).map_err(fatfs_error_to_status)?; |
| file.truncate().map_err(fatfs_error_to_status)?; |
| return Err(e); |
| } |
| Ok(()) |
| } |
| |
| struct FatFileData { |
| name: String, |
| parent: Option<Arc<FatDirectory>>, |
| } |
| |
| /// Represents a single file on the disk. |
| pub struct FatFile { |
| file: UnsafeCell<FatfsFileRef>, |
| filesystem: Pin<Arc<FatFilesystem>>, |
| data: RwLock<FatFileData>, |
| } |
| |
| // The only member that isn't `Sync + Send` is the `file` member. |
| // `file` is protected by the lock on `filesystem`, so we can safely |
| // implement Sync + Send for FatFile. |
| unsafe impl Sync for FatFile {} |
| unsafe impl Send for FatFile {} |
| |
| impl FatFile { |
| /// Create a new FatFile. |
| pub(crate) fn new( |
| file: FatfsFileRef, |
| parent: Arc<FatDirectory>, |
| filesystem: Pin<Arc<FatFilesystem>>, |
| name: String, |
| ) -> Arc<Self> { |
| Arc::new(FatFile { |
| file: UnsafeCell::new(file), |
| filesystem, |
| data: RwLock::new(FatFileData { parent: Some(parent), name }), |
| }) |
| } |
| |
| /// Borrow the underlying Fatfs File mutably. |
| pub(crate) fn borrow_file_mut<'a>(&self, fs: &'a FatFilesystemInner) -> Option<&mut File<'a>> { |
| // Safe because the file is protected by the lock on fs. |
| unsafe { self.file.get().as_mut() }.unwrap().borrow_mut(fs) |
| } |
| |
| pub fn borrow_file<'a>(&self, fs: &'a FatFilesystemInner) -> Result<&File<'a>, Status> { |
| // Safe because the file is protected by the lock on fs. |
| unsafe { self.file.get().as_ref() }.unwrap().borrow(fs).ok_or(Status::BAD_HANDLE) |
| } |
| |
| async fn write_or_append( |
| &self, |
| offset: Option<u64>, |
| content: &[u8], |
| ) -> Result<(u64, u64), Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file_mut(&fs_lock).ok_or(Status::BAD_HANDLE)?; |
| let mut file_offset = match offset { |
| Some(offset) => { |
| seek_for_write(file, offset)?; |
| offset |
| } |
| None => file.seek(std::io::SeekFrom::End(0)).map_err(fatfs_error_to_status)?, |
| }; |
| let mut total_written = 0; |
| while total_written < content.len() { |
| let written = file.write(&content[total_written..]).map_err(fatfs_error_to_status)?; |
| if written == 0 { |
| break; |
| } |
| total_written += written; |
| file_offset += written as u64; |
| let result = file.write(&content[total_written..]).map_err(fatfs_error_to_status); |
| match result { |
| Ok(0) => break, |
| Ok(written) => { |
| total_written += written; |
| file_offset += written as u64; |
| } |
| Err(e) => { |
| if total_written > 0 { |
| break; |
| } |
| return Err(e); |
| } |
| } |
| } |
| self.filesystem.mark_dirty(); |
| Ok((total_written as u64, file_offset)) |
| } |
| |
| pub(crate) fn create_connection( |
| self: Arc<Self>, |
| scope: ExecutionScope, |
| flags: fio::OpenFlags, |
| object_request: ObjectRequestRef<'_>, |
| ) -> Result<(), zx::Status> { |
| object_request.spawn_connection(scope, self, flags, FidlIoConnection::create) |
| } |
| } |
| |
| impl Node for FatFile { |
| /// Flush to disk and invalidate the reference that's contained within this FatFile. |
| /// Any operations on the file will return Status::BAD_HANDLE until it is re-attached. |
| fn detach(&self, fs: &FatFilesystemInner) { |
| // Safe because we hold the fs lock. |
| let file = unsafe { self.file.get().as_mut() }.unwrap(); |
| // This causes a flush to disk when the underlying fatfs File is dropped. |
| file.take(fs); |
| } |
| |
| /// Attach to the given parent and re-open the underlying `FatfsFileRef` this file represents. |
| fn attach( |
| &self, |
| new_parent: Arc<FatDirectory>, |
| name: &str, |
| fs: &FatFilesystemInner, |
| ) -> Result<(), Status> { |
| let mut data = self.data.write().unwrap(); |
| data.name = name.to_owned(); |
| // Safe because we hold the fs lock. |
| let file = unsafe { self.file.get().as_mut() }.unwrap(); |
| // Safe because we have a reference to the FatFilesystem. |
| unsafe { file.maybe_reopen(fs, &new_parent, name)? }; |
| data.parent.replace(new_parent); |
| Ok(()) |
| } |
| |
| fn did_delete(&self) { |
| self.data.write().unwrap().parent.take(); |
| } |
| |
| fn open_ref(&self, fs_lock: &FatFilesystemInner) -> Result<(), Status> { |
| let data = self.data.read().unwrap(); |
| let file_ref = unsafe { self.file.get().as_mut() }.unwrap(); |
| unsafe { file_ref.open(&fs_lock, data.parent.as_deref(), &data.name) } |
| } |
| |
| /// Close the underlying FatfsFileRef, regardless of the number of open connections. |
| fn shut_down(&self, fs: &FatFilesystemInner) -> Result<(), Status> { |
| unsafe { self.file.get().as_mut() }.unwrap().take(fs); |
| Ok(()) |
| } |
| |
| fn flush_dir_entry(&self, fs: &FatFilesystemInner) -> Result<(), Status> { |
| if let Some(file) = self.borrow_file_mut(fs) { |
| file.flush_dir_entry().map_err(fatfs_error_to_status)? |
| } |
| Ok(()) |
| } |
| |
| fn close_ref(&self, fs: &FatFilesystemInner) { |
| unsafe { self.file.get().as_mut() }.unwrap().close(fs); |
| } |
| } |
| |
| #[async_trait] |
| impl vfs::node::Node for FatFile { |
| async fn get_attrs(&self) -> Result<fio::NodeAttributes, Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file(&fs_lock)?; |
| let content_size = file.len() as u64; |
| let creation_time = dos_to_unix_time(file.created()); |
| let modification_time = dos_to_unix_time(file.modified()); |
| |
| // Figure out the storage size by rounding content_size up to the nearest |
| // multiple of cluster_size. |
| let cluster_size = fs_lock.cluster_size() as u64; |
| let storage_size = ((content_size + cluster_size - 1) / cluster_size) * cluster_size; |
| |
| Ok(fio::NodeAttributes { |
| mode: fio::MODE_TYPE_FILE | S_IRUSR | S_IWUSR, |
| id: fio::INO_UNKNOWN, |
| content_size, |
| storage_size, |
| link_count: 1, |
| creation_time, |
| modification_time, |
| }) |
| } |
| |
| // TODO(https://fxbug.dev/324112547): add new io2 attributes, e.g. change time, access time. |
| async fn get_attributes( |
| &self, |
| requested_attributes: fio::NodeAttributesQuery, |
| ) -> Result<fio::NodeAttributes2, Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file(&fs_lock)?; |
| let content_size = file.len() as u64; |
| let creation_time = dos_to_unix_time(file.created()); |
| let modification_time = dos_to_unix_time(file.modified()); |
| |
| // Figure out the storage size by rounding content_size up to the nearest |
| // multiple of cluster_size. |
| let cluster_size = fs_lock.cluster_size() as u64; |
| let storage_size = ((content_size + cluster_size - 1) / cluster_size) * cluster_size; |
| |
| Ok(attributes!( |
| requested_attributes, |
| Mutable { |
| creation_time: creation_time, |
| modification_time: modification_time, |
| mode: 0, |
| uid: 0, |
| gid: 0, |
| rdev: 0 |
| }, |
| Immutable { |
| protocols: fio::NodeProtocolKinds::FILE, |
| abilities: fio::Operations::GET_ATTRIBUTES |
| | fio::Operations::UPDATE_ATTRIBUTES |
| | fio::Operations::READ_BYTES |
| | fio::Operations::WRITE_BYTES, |
| content_size: content_size, |
| storage_size: storage_size, |
| link_count: 1, |
| id: fio::INO_UNKNOWN, |
| } |
| )) |
| } |
| |
| fn close(self: Arc<Self>) { |
| self.close_ref(&self.filesystem.lock().unwrap()); |
| } |
| |
| fn query_filesystem(&self) -> Result<fio::FilesystemInfo, Status> { |
| self.filesystem.query_filesystem() |
| } |
| |
| fn will_clone(&self) { |
| self.open_ref(&self.filesystem.lock().unwrap()).unwrap(); |
| } |
| } |
| |
| impl Debug for FatFile { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("FatFile").field("name", &self.data.read().unwrap().name).finish() |
| } |
| } |
| |
| impl VfsFile for FatFile { |
| fn writable(&self) -> bool { |
| return true; |
| } |
| |
| async fn open_file(&self, _options: &FileOptions) -> Result<(), Status> { |
| Ok(()) |
| } |
| |
| async fn truncate(&self, length: u64) -> Result<(), Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file_mut(&fs_lock).ok_or(Status::BAD_HANDLE)?; |
| seek_for_write(file, length)?; |
| file.truncate().map_err(fatfs_error_to_status)?; |
| self.filesystem.mark_dirty(); |
| Ok(()) |
| } |
| |
| async fn get_backing_memory(&self, _flags: fio::VmoFlags) -> Result<zx::Vmo, Status> { |
| Err(Status::NOT_SUPPORTED) |
| } |
| |
| // Unfortunately, fatfs has deprecated the "set_created" and "set_modified" methods, |
| // saying that a TimeProvider should be used instead. There doesn't seem to be a good way to |
| // use a TimeProvider to change the creation/modification time of a file after the fact, |
| // so we need to use the deprecated methods. |
| #[allow(deprecated)] |
| async fn set_attrs( |
| &self, |
| flags: fio::NodeAttributeFlags, |
| attrs: fio::NodeAttributes, |
| ) -> Result<(), Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file_mut(&fs_lock).ok_or(Status::BAD_HANDLE)?; |
| |
| let mut needs_flush = false; |
| if flags.contains(fio::NodeAttributeFlags::CREATION_TIME) { |
| file.set_created(unix_to_dos_time(attrs.creation_time)); |
| needs_flush = true; |
| } |
| if flags.contains(fio::NodeAttributeFlags::MODIFICATION_TIME) { |
| file.set_modified(unix_to_dos_time(attrs.modification_time)); |
| needs_flush = true; |
| } |
| |
| if needs_flush { |
| file.flush().map_err(fatfs_error_to_status)?; |
| self.filesystem.mark_dirty(); |
| } |
| Ok(()) |
| } |
| |
| async fn update_attributes( |
| &self, |
| _attributes: fio::MutableNodeAttributes, |
| ) -> Result<(), Status> { |
| Err(Status::NOT_SUPPORTED) |
| } |
| |
| async fn get_size(&self) -> Result<u64, Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file(&fs_lock)?; |
| Ok(file.len() as u64) |
| } |
| |
| async fn sync(&self, _mode: SyncMode) -> Result<(), Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file_mut(&fs_lock).ok_or(Status::BAD_HANDLE)?; |
| |
| file.flush().map_err(fatfs_error_to_status)?; |
| Ok(()) |
| } |
| } |
| |
| impl VfsFileIo for FatFile { |
| async fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result<u64, Status> { |
| let fs_lock = self.filesystem.lock().unwrap(); |
| let file = self.borrow_file_mut(&fs_lock).ok_or(Status::BAD_HANDLE)?; |
| |
| let real_offset = |
| file.seek(std::io::SeekFrom::Start(offset)).map_err(fatfs_error_to_status)?; |
| // Technically, we don't need to do this because the read should return zero bytes later, |
| // but it's better to be explicit. |
| if real_offset != offset { |
| return Ok(0); |
| } |
| let mut total_read = 0; |
| while total_read < buffer.len() { |
| let read = file.read(&mut buffer[total_read..]).map_err(fatfs_error_to_status)?; |
| if read == 0 { |
| break; |
| } |
| total_read += read; |
| } |
| Ok(total_read as u64) |
| } |
| |
| async fn write_at(&self, offset: u64, content: &[u8]) -> Result<u64, Status> { |
| self.write_or_append(Some(offset), content).await.map(|r| r.0) |
| } |
| |
| async fn append(&self, content: &[u8]) -> Result<(u64, u64), Status> { |
| self.write_or_append(None, content).await |
| } |
| } |
| |
| impl vfs::node::IsDirectory for FatFile { |
| fn is_directory(&self) -> bool { |
| false |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| // We only test things here that aren't covered by fs_tests. |
| use { |
| super::*, |
| crate::{ |
| node::{Closer, FatNode}, |
| tests::{TestDiskContents, TestFatDisk}, |
| }, |
| }; |
| |
| const TEST_DISK_SIZE: u64 = 2048 << 10; // 2048K |
| const TEST_FILE_CONTENT: &str = "test file contents"; |
| |
| struct TestFile(Arc<FatFile>); |
| |
| impl TestFile { |
| fn new() -> Self { |
| let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE); |
| let structure = |
| TestDiskContents::dir().add_child("test_file", TEST_FILE_CONTENT.into()); |
| structure.create(&disk.root_dir()); |
| |
| let fs = disk.into_fatfs(); |
| let dir = fs.get_fatfs_root(); |
| let mut closer = Closer::new(&fs.filesystem()); |
| dir.open_ref(&fs.filesystem().lock().unwrap()).expect("open_ref failed"); |
| closer.add(FatNode::Dir(dir.clone())); |
| let file = match dir |
| .open_child("test_file", fio::OpenFlags::empty(), &mut closer) |
| .expect("Open to succeed") |
| { |
| FatNode::File(f) => f, |
| val => panic!("Unexpected value {:?}", val), |
| }; |
| file.open_ref(&fs.filesystem().lock().unwrap()).expect("open_ref failed"); |
| TestFile(file) |
| } |
| } |
| |
| impl Drop for TestFile { |
| fn drop(&mut self) { |
| self.0.close_ref(&self.0.filesystem.lock().unwrap()); |
| } |
| } |
| |
| impl std::ops::Deref for TestFile { |
| type Target = Arc<FatFile>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_read_at() { |
| let file = TestFile::new(); |
| // Note: fatfs incorrectly casts u64 to i64, which causes this value to wrap |
| // around and become negative, which causes seek() in read_at() to fail. |
| // The error is not particularly important, because fat has a maximum 32-bit file size. |
| // An error like this will only happen if an application deliberately seeks to a (very) |
| // out-of-range position or reads at a nonsensical offset. |
| let mut buffer = [0u8; 512]; |
| let err = file.read_at(u64::MAX - 30, &mut buffer).await.expect_err("Read fails"); |
| assert_eq!(err, Status::INVALID_ARGS); |
| } |
| |
| #[fuchsia::test] |
| async fn test_get_attrs() { |
| let file = TestFile::new(); |
| let attrs = vfs::node::Node::get_attrs(&**file).await.expect("get_attrs succeeds"); |
| assert_eq!(attrs.mode, fio::MODE_TYPE_FILE | S_IRUSR | S_IWUSR); |
| assert_eq!(attrs.id, fio::INO_UNKNOWN); |
| assert_eq!(attrs.content_size, TEST_FILE_CONTENT.len() as u64); |
| assert!(attrs.storage_size > TEST_FILE_CONTENT.len() as u64); |
| assert_eq!(attrs.link_count, 1); |
| } |
| } |