summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorAlejandro Soto <alejandro@34project.org>2021-12-23 18:57:18 -0600
committerAlejandro Soto <alejandro@34project.org>2021-12-23 19:26:39 -0600
commitfc6f4052648a77a66f6bd50ffd1647992cb68b10 (patch)
treeee735850ee63a94c355f920aa168ea1969cca96a /examples
Initial commit
I started this project on February 2021, but postponed further development until now. The only modification introduced since then is try_trait_v2 (try_trait no longer exists).
Diffstat (limited to '')
-rw-r--r--examples/ext2.rs559
1 files changed, 559 insertions, 0 deletions
diff --git a/examples/ext2.rs b/examples/ext2.rs
new file mode 100644
index 0000000..01bbf4b
--- /dev/null
+++ b/examples/ext2.rs
@@ -0,0 +1,559 @@
+/* Read-only ext2 (rev 1.0) implementation.
+ *
+ * This is not really async, since the whole backing storage
+ * is mmap()ed for simplicity, and then treated as a regular
+ * slice (likely unsound, I don't care). Some yields are
+ * springled in a few places in order to emulate true async
+ * operations.
+ *
+ * Reference: <https://www.nongnu.org/ext2-doc/ext2.html>
+ */
+
+#![feature(arbitrary_self_types)]
+
+#[cfg(target_endian = "big")]
+compile_error!("This example assumes a little-endian system");
+
+use std::{
+ ffi::{CStr, OsStr},
+ fs::File,
+ mem::size_of,
+ os::unix::{ffi::OsStrExt, io::AsRawFd},
+ path::{Path, PathBuf},
+ time::{Duration, UNIX_EPOCH},
+};
+
+use nix::{
+ dir::Type,
+ errno::Errno,
+ sys::mman::{mmap, MapFlags, ProtFlags},
+ sys::stat::Mode,
+ unistd::{Gid, Uid},
+};
+
+use blown_fuse::{
+ fs::Fuse,
+ io::{Attrs, Entry, FsInfo},
+ mount::{mount_sync, Options},
+ ops::{Init, Lookup, Readdir, Readlink, Statfs},
+ Done, Ino, Reply, TimeToLive,
+};
+
+use async_trait::async_trait;
+use bytemuck::{cast_slice, from_bytes, try_from_bytes};
+use bytemuck_derive::{Pod, Zeroable};
+use clap::{App, Arg};
+use futures_util::stream::{self, Stream, TryStreamExt};
+use smallvec::SmallVec;
+use tokio::{self, runtime::Runtime};
+use uuid::Uuid;
+
+const EXT2_ROOT: Ino = Ino(2);
+
+type Op<'o, O> = blown_fuse::Op<'o, Ext2, O>;
+
+#[derive(Copy, Clone)]
+struct Farc {
+ ino: Ino,
+ inode: &'static Inode,
+}
+
+impl std::ops::Deref for Farc {
+ type Target = Inode;
+
+ fn deref(&self) -> &Self::Target {
+ self.inode
+ }
+}
+
+struct Ext2 {
+ backing: &'static [u8],
+ superblock: &'static Superblock,
+}
+
+#[derive(Pod, Zeroable, Copy, Clone)]
+#[repr(C)]
+struct Superblock {
+ s_inodes_count: u32,
+ s_blocks_count: u32,
+ s_r_blocks_count: u32,
+ s_free_blocks_count: u32,
+ s_free_inodes_count: u32,
+ s_first_data_block: u32,
+ s_log_block_size: u32,
+ s_log_frag_size: i32,
+ s_blocks_per_group: u32,
+ s_frags_per_group: u32,
+ s_inodes_per_group: u32,
+ s_mtime: u32,
+ s_wtime: u32,
+ s_mnt_count: u16,
+ s_max_mnt_count: u16,
+ s_magic: u16,
+ s_state: u16,
+ s_errors: u16,
+ s_minor_rev_level: u16,
+ s_lastcheck: u32,
+ s_checkinterval: u32,
+ s_creator_os: u32,
+ s_rev_level: u32,
+ s_def_resuid: u16,
+ s_def_resgid: u16,
+ s_first_ino: u32,
+ s_inode_size: u16,
+ s_block_group_nr: u16,
+ s_feature_compat: u32,
+ s_feature_incompat: u32,
+ s_feature_ro_compat: u32,
+ s_uuid: [u8; 16],
+ s_volume_name: [u8; 16],
+ s_last_mounted: [u8; 64],
+}
+
+#[derive(Pod, Zeroable, Copy, Clone)]
+#[repr(C)]
+struct GroupDescriptor {
+ bg_block_bitmap: u32,
+ bg_inode_bitmap: u32,
+ bg_inode_table: u32,
+ bg_free_blocks_count: u16,
+ bg_free_inodes_count: u16,
+ bg_used_dirs_count: u16,
+ bg_pad: u16,
+ bg_reserved: [u32; 3],
+}
+
+#[derive(Pod, Zeroable, Copy, Clone)]
+#[repr(C)]
+struct Inode {
+ i_mode: u16,
+ i_uid: u16,
+ i_size: u32,
+ i_atime: u32,
+ i_ctime: u32,
+ i_mtime: u32,
+ i_dtime: u32,
+ i_gid: u16,
+ i_links_count: u16,
+ i_blocks: u32,
+ i_flags: u32,
+ i_osd1: u32,
+ i_block: [u32; 15],
+ i_generation: u32,
+ i_file_acl: u32,
+ i_dir_acl: u32,
+ i_faddr: u32,
+ i_osd2: [u32; 3],
+}
+
+#[derive(Pod, Zeroable, Copy, Clone)]
+#[repr(C)]
+struct LinkedEntry {
+ inode: u32,
+ rec_len: u16,
+ name_len: u8,
+ file_type: u8,
+}
+
+impl Ext2 {
+ fn directory_stream(
+ &self,
+ inode: Farc,
+ start: u64,
+ ) -> impl Stream<Item = Result<Entry<'static, Farc>, Errno>> + '_ {
+ stream::try_unfold(start, move |mut position| async move {
+ loop {
+ if position == inode.i_size as u64 {
+ break Ok(None); // End of stream
+ }
+
+ let bytes = self.seek_contiguous(&inode, position)?;
+ let (header, bytes) = bytes.split_at(size_of::<LinkedEntry>());
+ let header: &LinkedEntry = from_bytes(header);
+
+ position += header.rec_len as u64;
+ if header.inode == 0 {
+ continue; // Unused entry
+ }
+
+ let inode = self.inode(Ino(header.inode as u64))?;
+ let name = OsStr::from_bytes(&bytes[..header.name_len as usize]).into();
+
+ let entry = Entry {
+ inode,
+ name,
+ offset: position,
+ ttl: TimeToLive::MAX,
+ };
+
+ break Ok(Some((entry, position)));
+ }
+ })
+ }
+
+ fn inode(&self, Ino(ino): Ino) -> Result<Farc, Errno> {
+ if ino == 0 {
+ log::error!("Attempted to access the null (0) inode");
+ return Err(Errno::EIO);
+ }
+
+ let index = (ino - 1) as usize;
+ let inodes_per_group = self.superblock.s_inodes_per_group as usize;
+ let (block, index) = (index / inodes_per_group, index % inodes_per_group);
+
+ let table_base = self.group_descriptors()?[block].bg_inode_table as usize;
+ let inode_size = self.superblock.s_inode_size as usize;
+
+ let inodes_per_block = self.block_size() / inode_size;
+ let block = table_base + index / inodes_per_block;
+
+ let start = index % inodes_per_block * inode_size;
+ let end = start + size_of::<Inode>();
+
+ Ok(Farc {
+ ino: Ino(ino),
+ inode: from_bytes(&self.block(block)?[start..end]),
+ })
+ }
+
+ fn seek_contiguous(&self, inode: &Farc, position: u64) -> Result<&'static [u8], Errno> {
+ let block_size = self.block_size();
+ let position = position as usize;
+ let (direct, offset) = (position / block_size, position % block_size);
+
+ let out_of_bounds = || {
+ log::error!("Offset {} out of bounds in inode {}", position, inode.ino);
+ };
+
+ let chase = |indices: &[usize]| {
+ let root: &[u8] = cast_slice(&inode.inode.i_block);
+ indices
+ .iter()
+ .try_fold(root, |ptrs, index| {
+ let ptrs: &[u32] = cast_slice(ptrs);
+ let block = ptrs[*index];
+
+ if block > 0 {
+ self.block(ptrs[*index] as usize)
+ } else {
+ out_of_bounds();
+ Err(Errno::EIO)
+ }
+ })
+ .map(|block| &block[offset..])
+ };
+
+ const DIRECT_PTRS: usize = 12;
+
+ if direct < DIRECT_PTRS {
+ return chase(&[direct]);
+ }
+
+ let ptrs_per_block = block_size / size_of::<u32>();
+ let (level1, level1_index) = {
+ let indirect = direct - DIRECT_PTRS;
+ (indirect / ptrs_per_block, indirect % ptrs_per_block)
+ };
+
+ if level1 == 0 {
+ return chase(&[DIRECT_PTRS, level1_index]);
+ }
+
+ let (level2, level2_index) = (level1 / ptrs_per_block, level1 % ptrs_per_block);
+ if level2 == 0 {
+ return chase(&[DIRECT_PTRS + 1, level2_index, level1_index]);
+ }
+
+ let (level3, level3_index) = (level2 / ptrs_per_block, level2 % ptrs_per_block);
+ if level3 == 0 {
+ chase(&[DIRECT_PTRS + 2, level3_index, level2_index, level1_index])
+ } else {
+ out_of_bounds();
+ Err(Errno::EIO)
+ }
+ }
+
+ fn group_descriptors(&self) -> Result<&'static [GroupDescriptor], Errno> {
+ let start = (self.superblock.s_first_data_block + 1) as usize;
+ let groups = (self.superblock.s_blocks_count / self.superblock.s_blocks_per_group) as usize;
+ let descriptors_per_block = self.block_size() / size_of::<GroupDescriptor>();
+ let table_blocks = (groups + descriptors_per_block - 1) / descriptors_per_block;
+
+ self.blocks(start..start + table_blocks)
+ .map(|blocks| &cast_slice(blocks)[..groups])
+ }
+
+ fn block(&self, n: usize) -> Result<&'static [u8], Errno> {
+ self.blocks(n..n + 1)
+ }
+
+ fn blocks(&self, range: std::ops::Range<usize>) -> Result<&'static [u8], Errno> {
+ let block_size = self.block_size();
+ let (start, end) = (range.start * block_size, range.end * block_size);
+
+ if self.backing.len() >= end {
+ Ok(&self.backing[start..end])
+ } else {
+ log::error!("Bad block range: ({}..{})", range.start, range.end);
+ Err(Errno::EIO)
+ }
+ }
+
+ fn block_size(&self) -> usize {
+ 1024usize << self.superblock.s_log_block_size
+ }
+}
+
+#[async_trait]
+impl Fuse for Ext2 {
+ type Farc = Farc;
+ type Inode = Inode;
+
+ async fn init<'o>(&self, reply: Reply<'o, Ext2, Init>) -> Done<'o> {
+ let label = &self.superblock.s_volume_name;
+ let label = &label[..=label.iter().position(|byte| *byte == b'\0').unwrap_or(0)];
+ let label = CStr::from_bytes_with_nul(label)
+ .ok()
+ .map(|label| {
+ let label = label.to_string_lossy();
+ if !label.is_empty() {
+ label
+ } else {
+ "(empty)".into()
+ }
+ })
+ .unwrap_or("(bad)".into());
+
+ log::info!("UUID: {}", Uuid::from_bytes(self.superblock.s_uuid));
+ log::info!("Label: {}", label.escape_debug());
+
+ if let Ok(root) = self.inode(EXT2_ROOT) {
+ log::info!("Mounted successfully");
+ reply.root(root)
+ } else {
+ log::error!("Failed to retrieve the root inode");
+ reply.io_error()
+ }
+ }
+
+ async fn statfs<'o>(&self, (_, reply, _): Op<'o, Statfs>) -> Done<'o> {
+ let total_blocks = self.superblock.s_blocks_count as u64;
+ let free_blocks = self.superblock.s_free_blocks_count as u64;
+ let available_blocks = free_blocks - self.superblock.s_r_blocks_count as u64;
+ let total_inodes = self.superblock.s_inodes_count as u64;
+ let free_inodes = self.superblock.s_free_inodes_count as u64;
+
+ reply.info(
+ FsInfo::default()
+ .blocks(
+ self.block_size() as u32,
+ total_blocks,
+ free_blocks,
+ available_blocks,
+ )
+ .inodes(total_inodes, free_inodes)
+ .filenames(255),
+ )
+ }
+}
+
+#[async_trait]
+impl blown_fuse::fs::Inode for Inode {
+ type Fuse = Ext2;
+
+ fn ino(self: &Farc) -> Ino {
+ match self.ino {
+ Ino::ROOT => EXT2_ROOT,
+ EXT2_ROOT => Ino::ROOT,
+ ino => ino,
+ }
+ }
+
+ fn inode_type(self: &Farc) -> Type {
+ let inode_type = self.i_mode >> 12;
+ match inode_type {
+ 0x01 => Type::Fifo,
+ 0x02 => Type::CharacterDevice,
+ 0x04 => Type::Directory,
+ 0x06 => Type::BlockDevice,
+ 0x08 => Type::File,
+ 0x0A => Type::Symlink,
+ 0x0C => Type::Socket,
+
+ _ => {
+ log::error!("Inode {} has invalid type {:x}", self.ino, inode_type);
+ Type::File
+ }
+ }
+ }
+
+ fn attrs(self: &Farc) -> (Attrs, TimeToLive) {
+ let (access, modify, change) = {
+ let time = |seconds: u32| (UNIX_EPOCH + Duration::from_secs(seconds.into())).into();
+ (time(self.i_atime), time(self.i_mtime), time(self.i_ctime))
+ };
+
+ let attrs = Attrs::default()
+ .size((self.i_dir_acl as u64) << 32 | self.i_size as u64)
+ .owner(
+ Uid::from_raw(self.i_uid.into()),
+ Gid::from_raw(self.i_gid.into()),
+ )
+ .mode(Mode::from_bits_truncate(self.i_mode.into()))
+ .blocks(self.i_blocks.into(), 512)
+ .times(access, modify, change)
+ .links(self.i_links_count.into());
+
+ (attrs, TimeToLive::MAX)
+ }
+
+ async fn lookup<'o>(self: Farc, (request, reply, session): Op<'o, Lookup>) -> Done<'o> {
+ let fs = session.fs();
+ let name = request.name();
+
+ //TODO: Indexed directories
+ let lookup = async move {
+ let stream = fs.directory_stream(self, 0);
+ tokio::pin!(stream);
+
+ loop {
+ match stream.try_next().await? {
+ Some(entry) if entry.name == name => break Ok(Some(entry.inode)),
+ Some(_) => continue,
+ None => break Ok(None),
+ }
+ }
+ };
+
+ let (reply, result) = reply.interruptible(lookup).await?;
+ let (reply, inode) = reply.fallible(result)?;
+
+ if let Some(inode) = inode {
+ reply.found(&inode, TimeToLive::MAX)
+ } else {
+ reply.not_found(TimeToLive::MAX)
+ }
+ }
+
+ async fn readlink<'o>(self: Farc, (_, reply, session): Op<'o, Readlink>) -> Done<'o> {
+ if Inode::inode_type(&self) != Type::Symlink {
+ return reply.invalid_argument();
+ }
+
+ let size = self.i_size as usize;
+ if size < size_of::<[u32; 15]>() {
+ return reply.target(OsStr::from_bytes(&cast_slice(&self.i_block)[..size]));
+ }
+
+ let fs = session.fs();
+ let segments = async {
+ /* This is unlikely to ever spill, and is guaranteed not to
+ * do so for valid symlinks on any fs where block_size >= 4096.
+ */
+ let mut segments = SmallVec::<[&OsStr; 1]>::new();
+ let (mut size, mut offset) = (size, 0);
+
+ while size > 0 {
+ let segment = fs.seek_contiguous(&self, offset)?;
+ let segment = &segment[..segment.len().min(size)];
+
+ segments.push(OsStr::from_bytes(segment));
+
+ size -= segment.len();
+ offset += segment.len() as u64;
+ }
+
+ Ok(segments)
+ };
+
+ let (reply, segments) = reply.fallible(segments.await)?;
+ reply.gather_target(&segments)
+ }
+
+ async fn readdir<'o>(self: Farc, (request, reply, session): Op<'o, Readdir>) -> Done<'o> {
+ let stream = session.fs().directory_stream(self, request.offset());
+ reply.try_stream(stream).await?
+ }
+}
+
+fn early_error<T, E>(_: ()) -> Result<T, E>
+where
+ nix::Error: Into<E>,
+{
+ Err(nix::Error::Sys(Errno::EINVAL).into())
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let matches = App::new("ext2")
+ .about("read-only ext2 FUSE driver")
+ .arg(Arg::from_usage("[mount_options] -o <options>... 'See fuse(8)'").number_of_values(1))
+ .arg(Arg::from_usage("<image> 'Filesystem image file'"))
+ .arg(Arg::from_usage("<mountpoint> 'Filesystem mountpoint'"))
+ .get_matches();
+
+ env_logger::builder()
+ .filter_level(log::LevelFilter::Info)
+ .init();
+
+ let (image, session) = {
+ let (image, mountpoint) = {
+ let required_path = |key| Path::new(matches.value_of(key).unwrap());
+ (required_path("image"), required_path("mountpoint"))
+ };
+
+ let canonical = image.canonicalize();
+ let canonical = canonical.as_ref().map(PathBuf::as_path).unwrap_or(image);
+
+ let mut options = Options::default();
+ options
+ .fs_name(canonical)
+ .read_only()
+ .extend(matches.values_of_os("mount_options").into_iter().flatten());
+
+ (image, mount_sync(mountpoint, &options)?)
+ };
+
+ let file = File::open(image)?;
+ let backing = unsafe {
+ let length = file.metadata().unwrap().len() as usize;
+
+ let base = mmap(
+ std::ptr::null_mut(),
+ length,
+ ProtFlags::PROT_READ,
+ MapFlags::MAP_PRIVATE,
+ file.as_raw_fd(),
+ 0,
+ );
+
+ std::slice::from_raw_parts(base.unwrap() as *const u8, length)
+ };
+
+ let superblock = if backing.len() >= 1024 + size_of::<Superblock>() {
+ Some(&backing[1024..1024 + size_of::<Superblock>()])
+ } else {
+ None
+ };
+
+ let superblock = superblock.and_then(|superblock| try_from_bytes(superblock).ok());
+ let superblock: &'static Superblock = match superblock {
+ Some(superblock) => superblock,
+ None => return early_error(log::error!("Bad superblock")),
+ };
+
+ if superblock.s_magic != 0xef53 {
+ return early_error(log::error!("Bad magic"));
+ }
+
+ let (major, minor) = (superblock.s_rev_level, superblock.s_minor_rev_level);
+ if (major, minor) != (1, 0) {
+ return early_error(log::error!("Unsupported revision: {}.{}", major, minor));
+ }
+
+ let fs = Ext2 {
+ backing,
+ superblock,
+ };
+
+ Ok(Runtime::new()?.block_on(async { session.start(fs).await?.main_loop().await })?)
+}