Skip to main content

fuchsia_fatfs/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use crate::filesystem::FatFilesystem;
5use crate::node::Node;
6use anyhow::Error;
7use fatfs::FsOptions;
8use fidl_fuchsia_fs::{AdminRequest, AdminShutdownResponder};
9use std::rc::Rc;
10use std::sync::Arc;
11use vfs::directory::entry::DirectoryEntry;
12use vfs::directory::entry_container::Directory;
13use vfs::execution_scope::ExecutionScope;
14use zx::Status;
15
16pub mod component;
17mod directory;
18mod file;
19mod filesystem;
20mod node;
21mod refs;
22mod types;
23mod util;
24
25pub use directory::FatDirectory;
26pub use util::fatfs_error_to_status;
27
28#[cfg(fuzz)]
29mod fuzzer;
30#[cfg(fuzz)]
31use fuzz::fuzz;
32#[cfg(fuzz)]
33#[fuzz]
34fn fuzz_fatfs(fs: &[u8]) {
35    fuzzer::fuzz_fatfs(fs);
36}
37
38pub use types::Disk;
39
40/// Number of UCS-2 characters that fit in a VFAT LFN.
41/// Note that FAT doesn't support the full range of Unicode characters (UCS-2 is only 16 bits),
42/// and short file names can't encode the full 16-bit range of UCS-2.
43/// This is the minimum possible value. For instance, a 300 byte UTF-8 string could fit inside 255
44/// UCS-2 codepoints (if it had some 16 bit characters), but a 300 byte ASCII string would not fit.
45pub const MAX_FILENAME_LEN: u32 = 255;
46
47// An array used to initialize the FilesystemInfo |name| field. This just spells "fatfs" 0-padded to
48// 32 bytes.
49pub const FATFS_INFO_NAME: [i8; 32] = [
50    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,
51    0, 0, 0, 0, 0,
52];
53
54pub trait RootDirectory: DirectoryEntry + Directory {}
55impl<T: DirectoryEntry + Directory> RootDirectory for T {}
56
57pub struct FatFs {
58    root: Arc<FatDirectory>,
59}
60
61impl FatFs {
62    /// Create a new FatFs using the given ReadWriteSeek as the disk.
63    pub fn new(disk: Box<dyn Disk>, scope: ExecutionScope) -> Result<Self, Error> {
64        let (_inner, root) = FatFilesystem::new(disk, FsOptions::new(), scope)?;
65        Ok(FatFs { root })
66    }
67
68    #[cfg(test)]
69    pub fn from_filesystem(root: Arc<FatDirectory>) -> Self {
70        FatFs { root }
71    }
72
73    #[cfg(any(test, fuzz))]
74    pub fn get_fatfs_root(&self) -> Arc<FatDirectory> {
75        self.root.clone()
76    }
77
78    pub fn filesystem(&self) -> &FatFilesystem {
79        self.root.fs()
80    }
81
82    pub fn is_present(&self) -> bool {
83        self.filesystem().with_disk(|disk| disk.is_present())
84    }
85
86    /// Get the root directory of this filesystem.
87    /// The caller must call close() on the returned entry when it's finished with it.
88    pub fn get_root(&self) -> Result<Arc<FatDirectory>, Status> {
89        // Make sure it's open.
90        self.root.open_ref()?;
91        Ok(self.root.clone())
92    }
93
94    pub fn handle_admin(
95        &self,
96        scope: &ExecutionScope,
97        req: AdminRequest,
98    ) -> Option<AdminShutdownResponder> {
99        match req {
100            AdminRequest::Shutdown { responder } => {
101                scope.shutdown();
102                Some(responder)
103            }
104        }
105    }
106
107    /// Shut down the filesystem.
108    ///
109    /// # Preconditions
110    ///
111    /// This method requires exclusive ownership of the underlying `FatFilesystem` (held via `Rc`).
112    /// The caller must ensure that all other references (e.g., outstanding VFS connections,
113    /// active file/directory handles) have been dropped before calling this.
114    ///
115    /// If there are outstanding references, `Rc::into_inner` will fail, and this method
116    /// will return `Err(Status::BAD_STATE)` without unmounting the filesystem, to prevent
117    /// potential Use-After-Free (UAF) bugs.
118    ///
119    /// Typically, one should shut down the VFS scope and wait for all connections to close
120    /// (e.g., via `scope.wait().await`) before invoking this.
121    pub fn shut_down(self) -> Result<(), Status> {
122        let FatFs { root } = self;
123        let inner = root.fs().clone();
124        root.shut_down()?;
125        drop(root);
126        let fs = Rc::into_inner(inner).ok_or(Status::BAD_STATE)?;
127        fs.shut_down()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::types::{Dir, FileSystem};
135    use anyhow::{Context, anyhow};
136    use fatfs::{FormatVolumeOptions, format_volume};
137    use fidl::endpoints::Proxy;
138    use fidl_fuchsia_io as fio;
139    use futures::future::BoxFuture;
140    use futures::prelude::*;
141    use std::collections::HashMap;
142    use std::io::Write;
143    use std::ops::Deref;
144    use vfs::node::Node;
145
146    #[derive(Debug, PartialEq)]
147    /// Helper class for creating a filesystem layout on a FAT disk programatically.
148    pub enum TestDiskContents {
149        File(String),
150        Dir(HashMap<String, TestDiskContents>),
151    }
152
153    impl From<&str> for TestDiskContents {
154        fn from(string: &str) -> Self {
155            TestDiskContents::File(string.to_owned())
156        }
157    }
158
159    impl TestDiskContents {
160        /// Create a new, empty directory.
161        pub fn dir() -> Self {
162            TestDiskContents::Dir(HashMap::new())
163        }
164
165        /// Add a new child to this directory.
166        pub fn add_child(mut self, name: &str, child: Self) -> Self {
167            match &mut self {
168                TestDiskContents::Dir(map) => map.insert(name.to_owned(), child),
169                _ => panic!("Can't add to a file"),
170            };
171            self
172        }
173
174        /// Add this TestDiskContents to the given fatfs Dir
175        pub fn create(&self, dir: &Dir<'_>) {
176            match self {
177                TestDiskContents::File(_) => {
178                    panic!("Can't have the root directory be a file!");
179                }
180                TestDiskContents::Dir(map) => {
181                    for (name, value) in map.iter() {
182                        value.create_fs_structure(&name, dir);
183                    }
184                }
185            };
186        }
187
188        fn create_fs_structure(&self, name: &str, dir: &Dir<'_>) {
189            match self {
190                TestDiskContents::File(content) => {
191                    let mut file = dir.create_file(name).expect("Creating file to succeed");
192                    file.truncate().expect("Truncate to succeed");
193                    file.write_all(content.as_bytes()).expect("Write to succeed");
194                }
195                TestDiskContents::Dir(map) => {
196                    let new_dir = dir.create_dir(name).expect("Creating directory to succeed");
197                    for (name, value) in map.iter() {
198                        value.create_fs_structure(&name, &new_dir);
199                    }
200                }
201            };
202        }
203
204        pub fn verify(&self, remote: fio::NodeProxy) -> BoxFuture<'_, Result<(), Error>> {
205            // Unfortunately, there is no way to verify from the server side, so we use
206            // the fuchsia.io protocol to check everything is as expected.
207            match self {
208                TestDiskContents::File(content) => {
209                    let remote = fio::FileProxy::new(remote.into_channel().unwrap());
210                    let mut file_contents: Vec<u8> = Vec::with_capacity(content.len());
211
212                    return async move {
213                        loop {
214                            let mut vec = remote
215                                .read(content.len() as u64)
216                                .await
217                                .context("Read failed")?
218                                .map_err(Status::from_raw)
219                                .context("Read error")?;
220                            if vec.len() == 0 {
221                                break;
222                            }
223                            file_contents.append(&mut vec);
224                        }
225
226                        if file_contents.as_slice() != content.as_bytes() {
227                            return Err(anyhow!(
228                                "File contents mismatch: expected {}, got {}",
229                                content,
230                                String::from_utf8_lossy(&file_contents)
231                            ));
232                        }
233                        Ok(())
234                    }
235                    .boxed();
236                }
237                TestDiskContents::Dir(map) => {
238                    let remote = fio::DirectoryProxy::new(remote.into_channel().unwrap());
239                    // TODO(simonshields): we should check that no other files exist, but
240                    // GetDirents() is going to be a pain to deal with.
241
242                    return async move {
243                        for (name, value) in map.iter() {
244                            let (proxy, server_end) =
245                                fidl::endpoints::create_proxy::<fio::NodeMarker>();
246                            remote
247                                .open(
248                                    name,
249                                    fio::PERM_READABLE,
250                                    &Default::default(),
251                                    server_end.into_channel(),
252                                )
253                                .context("Sending open failed")?;
254                            value
255                                .verify(proxy)
256                                .await
257                                .with_context(|| format!("Verifying {}", name))?;
258                        }
259                        Ok(())
260                    }
261                    .boxed();
262                }
263            }
264        }
265    }
266
267    /// Helper class for creating an empty FAT-formatted VMO.
268    pub struct TestFatDisk {
269        fs: FileSystem,
270    }
271
272    impl TestFatDisk {
273        /// Create an empty disk with size at least |size| bytes.
274        pub fn empty_disk(size: u64) -> Self {
275            let mut buffer: Vec<u8> = Vec::with_capacity(size as usize);
276            buffer.resize(size as usize, 0);
277            let cursor = std::io::Cursor::new(buffer.as_mut_slice());
278
279            format_volume(cursor, FormatVolumeOptions::new()).expect("format volume to succeed");
280            let wrapper: Box<dyn Disk> = Box::new(std::io::Cursor::new(buffer));
281            TestFatDisk {
282                fs: fatfs::FileSystem::new(wrapper, FsOptions::new())
283                    .expect("creating FS to succeed"),
284            }
285        }
286
287        /// Get the root directory (as a fatfs Dir).
288        pub fn root_dir<'a>(&'a self) -> Dir<'a> {
289            self.fs.root_dir()
290        }
291
292        /// Convert this TestFatDisk into a FatFs for testing against.
293        pub fn into_fatfs(self) -> FatFs {
294            self.fs.flush().unwrap();
295            let (_filesystem, root_dir) = FatFilesystem::from_filesystem(self.fs);
296            FatFs::from_filesystem(root_dir)
297        }
298    }
299
300    impl Deref for TestFatDisk {
301        type Target = FileSystem;
302
303        fn deref(&self) -> &Self::Target {
304            &self.fs
305        }
306    }
307
308    const TEST_DISK_SIZE: u64 = 2048 << 10;
309
310    #[fuchsia::test]
311    #[ignore] // TODO(https://fxbug.dev/42133844): Clean up tasks to prevent panic on drop in FatfsFileRef
312    async fn test_create_disk() {
313        let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE);
314
315        let structure = TestDiskContents::dir()
316            .add_child("test", "This is a test file".into())
317            .add_child("empty_folder", TestDiskContents::dir());
318
319        structure.create(&disk.root_dir());
320
321        let fatfs = disk.into_fatfs();
322        let root = fatfs.get_root().unwrap();
323        let proxy = vfs::directory::serve_read_only(root.clone(), ExecutionScope::new());
324        root.close();
325
326        structure
327            .verify(fio::NodeProxy::from_channel(proxy.into_channel().unwrap().into()))
328            .await
329            .expect("Verify succeeds");
330    }
331
332    #[fuchsia::test]
333    fn test_unset_date() {
334        let disk = TestFatDisk::empty_disk(TEST_DISK_SIZE);
335        // FAT doesn't give the root directory a created/modified/access time,
336        // so this is a good way to check that we return valid dates for a "zero" date.
337        let root = disk.root_dir();
338        let epoch = fatfs::DateTime {
339            date: fatfs::Date { year: 1980, month: 1, day: 1 },
340            time: fatfs::Time { hour: 0, min: 0, sec: 0, millis: 0 },
341        };
342        assert_eq!(root.created(), epoch);
343        assert_eq!(root.modified(), epoch);
344        assert_eq!(root.accessed(), epoch.date);
345    }
346}