diff --git a/.gitignore b/.gitignore index e3d7486..3555095 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ **/*.zip !**/.vscode/tasks.json +sw/deployer/.idea/ +.idea/ diff --git a/sw/deployer/Cargo.lock b/sw/deployer/Cargo.lock index 574ae63..8d7cf4f 100644 --- a/sw/deployer/Cargo.lock +++ b/sw/deployer/Cargo.lock @@ -501,6 +501,33 @@ dependencies = [ "spin", ] +[[package]] +name = "fuse_mt" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e098b8dc4cd32e9ba31d9c8cdfef11271d8191233c64c2a671432ff19d354948" +dependencies = [ + "fuser", + "libc", + "log", + "threadpool", +] + +[[package]] +name = "fuser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b" +dependencies = [ + "libc", + "log", + "memchr", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -559,6 +586,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -745,9 +778,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libflate" @@ -1010,6 +1043,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1026,6 +1069,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "page_size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "panic-message" version = "0.3.0" @@ -1304,11 +1357,14 @@ dependencies = [ "crc32fast", "ctrlc", "encoding_rs", + "fuse_mt", "hex", "image", "include-flate", + "libc", "libftdi1-sys", "libusb1-sys", + "log", "md5", "panic-message", "rand", @@ -1485,6 +1541,15 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tiff" version = "0.9.1" @@ -1825,6 +1890,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/sw/deployer/Cargo.toml b/sw/deployer/Cargo.toml index dd63148..1c7e180 100644 --- a/sw/deployer/Cargo.toml +++ b/sw/deployer/Cargo.toml @@ -30,6 +30,11 @@ rust-ini = "0.18.0" serial2 = "0.2.26" serialport = "4.4.0" +[target.'cfg(unix)'.dependencies] +fuse_mt = "0.6.1" +libc = "0.2.161" +log = "0.4.22" + [profile.release] lto = true strip = true diff --git a/sw/deployer/src/main.rs b/sw/deployer/src/main.rs index 7ed78e1..5ffdaf1 100644 --- a/sw/deployer/src/main.rs +++ b/sw/deployer/src/main.rs @@ -272,6 +272,14 @@ enum SDCommands { /// Format the SD card #[command(name = "mkfs")] Format, + + /// Mount the SD card + #[command(name = "fuse")] + #[cfg(unix)] + Fuse { + /// Path to the directory + mount_path: PathBuf, + }, } #[derive(Subcommand)] @@ -990,6 +998,12 @@ fn handle_sd_command(connection: Connection, command: &SDCommands) -> Result<(), } log_wait(format!("Formatting the SD card"), || ff.mkfs())?; } + #[cfg(unix)] + SDCommands::Fuse { mount_path } => { + let fuse_args = [std::ffi::OsStr::new("-o"), std::ffi::OsStr::new("fsname=passthrufs,allow_other,auto_unmount")]; + + fuse_mt::mount(fuse_mt::FuseMT::new(ff, 1), mount_path, &fuse_args[..])?; + } } Ok(()) diff --git a/sw/deployer/src/sc64/ff.rs b/sw/deployer/src/sc64/ff.rs index 855bddf..fb02e09 100644 --- a/sw/deployer/src/sc64/ff.rs +++ b/sw/deployer/src/sc64/ff.rs @@ -1,5 +1,24 @@ use super::{SdCardResult, SC64, SD_CARD_SECTOR_SIZE}; use chrono::{Datelike, Timelike}; +#[cfg(unix)] +use fuse_mt::{ + CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, + ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultStatfs, Statfs +}; + +#[cfg(unix)] +use std::{ + cell::RefCell, + collections::HashMap, + io::{Read, Seek, SeekFrom}, + sync::{Arc, Mutex}, + time::{Duration, SystemTime} +}; +#[cfg(unix)] +use log::{debug, error}; +#[cfg(unix)] +use chrono::{Utc}; + mod fatfs { #![allow(non_camel_case_types)] #![allow(non_snake_case)] @@ -138,8 +157,104 @@ fn uninstall_driver() -> Result<(), Error> { Ok(()) } +#[cfg(unix)] +struct HandleMap { + mutex: Mutex, + counter: RefCell, + map: RefCell>>>, +} + +#[cfg(unix)] +impl HandleMap { + pub fn new(start_counter: u64) -> Self { + HandleMap { + mutex: Mutex::new(0), + counter: RefCell::new(start_counter), + map: RefCell::new(HashMap::new()), + } + } + + pub fn insert(&self, item: T) -> u64 { + let _lock = self.mutex.lock(); + let mut counter = self.counter.borrow_mut(); + *counter += 1; + self.map.borrow_mut().insert(*counter, Arc::new(Mutex::new(item))); + *counter + } + + pub fn get(&self, handle: u64) -> Result>, libc::c_int> { + let _lock = self.mutex.lock(); + let map = self.map.borrow(); + map.get(&handle) + .map(|item| Arc::clone(item)) + .ok_or(libc::EEXIST) + } + + pub fn release(&self, handle: u64) { + let _lock = self.mutex.lock(); + self.map.borrow_mut().retain(|&k, _| k != handle); + } +} + +#[cfg(unix)] +pub struct FatFsHelper { + dirs: HandleMap, + files: HandleMap, + stat_cache: StatCache, +} + +#[cfg(unix)] +pub struct StatCache { + mutex: Mutex, + cache: RefCell>, +} + +#[cfg(unix)] +impl StatCache { + pub fn new() -> Self { + StatCache { + mutex: Mutex::new(0), + cache: RefCell::new(HashMap::new()), + } + } + + /// Inserts an entry into the cache, returning the previous entry if it existed. + pub fn insert(&self, path: &std::path::Path, entry: T) -> Option { + let _lock = self.mutex.lock(); + let mut cache = self.cache.borrow_mut(); + cache.insert(path.to_path_buf(), entry) + } + + /// Removes an entry from the cache, if it exists. + pub fn remove(&self, path: &std::path::PathBuf) { + let _lock = self.mutex.lock(); + let mut cache = self.cache.borrow_mut(); + cache.remove(path); + } + + /// Retrieves an entry from the cache, returning None if it doesn't exist. + pub fn get(&self, path: &std::path::Path) -> Option { + let _lock = self.mutex.lock(); + let cache = self.cache.borrow(); + cache.get(path).cloned() // Clone the entry if it exists + } +} + +#[cfg(unix)] +impl FatFsHelper { + pub fn new() -> Self { + FatFsHelper { + dirs: HandleMap::new(0), + files: HandleMap::new(u64::MAX / 2), + stat_cache: StatCache::new(), + } + } +} + pub struct FatFs { fs: Box, + #[cfg(unix)] + helper: FatFsHelper, } impl FatFs { @@ -147,6 +262,8 @@ impl FatFs { install_driver(driver)?; let mut ff = Self { fs: Box::new(unsafe { std::mem::zeroed() }), + #[cfg(unix)] + helper: FatFsHelper::new(), }; ff.mount(false)?; Ok(ff) @@ -167,7 +284,7 @@ impl FatFs { } } - pub fn open>(&mut self, path: P) -> Result { + pub fn open>(&self, path: P) -> Result { File::open( path, fatfs::FA_OPEN_EXISTING | fatfs::FA_READ | fatfs::FA_WRITE, @@ -181,7 +298,7 @@ impl FatFs { ) } - pub fn stat>(&mut self, path: P) -> Result { + pub fn stat>(&self, path: P) -> Result { let mut fno = unsafe { std::mem::zeroed() }; match unsafe { fatfs::f_stat(fatfs::path(path)?.as_ptr(), &mut fno) } { fatfs::FRESULT_FR_OK => Ok(fno.into()), @@ -189,7 +306,7 @@ impl FatFs { } } - pub fn opendir>(&mut self, path: P) -> Result { + pub fn opendir>(&self, path: P) -> Result { Directory::open(path) } @@ -244,6 +361,260 @@ impl FatFs { } } + +// TODO, see https://github.com/wfraser/fuse-mt/issues/29 +#[cfg(unix)] +unsafe impl Send for FatFs {} +#[cfg(unix)] +unsafe impl Sync for FatFs {} +#[cfg(unix)] +const TTL: Duration = Duration::from_secs(u64::MAX); // TTL can be high because currently it's RO anyway + +#[cfg(unix)] +fn stat_to_fuse(stat: &Entry) -> FileAttr { + let time = |secs: i64, nanos: i64| + SystemTime::UNIX_EPOCH + Duration::new(secs as u64, nanos as u32); + + FileAttr { + size: match stat.info { + EntryInfo::Directory => { 0 } + EntryInfo::File { size } => { size } + }, + blocks: match stat.info { + EntryInfo::Directory => { 0 } + EntryInfo::File { size } => { (size / 4096) + 1 } // https://github.com/wfraser/fuse-mt/blob/ee9d91d9003e5aa72877ca39b614ff7d84149f02/src/fusemt.rs#L49 + }, + atime: time(stat.datetime.and_utc().timestamp(), 0), + mtime: time(stat.datetime.and_utc().timestamp(), 0), + ctime: time(stat.datetime.and_utc().timestamp(), 0), + crtime: SystemTime::UNIX_EPOCH, + kind: match stat.info { + EntryInfo::Directory => { FileType::Directory } + EntryInfo::File { .. } => { FileType::RegularFile } + }, + perm: 0o755, // TODO? + nlink: match stat.info { + EntryInfo::Directory => { 2 } + EntryInfo::File { .. } => { 1 } + }, + // TODO ?!?! + uid: 501, + gid: 20, + rdev: 0, + flags: 0, + } +} + + +#[cfg(unix)] +impl FilesystemMT for FatFs { + fn init(&self, _req: RequestInfo) -> ResultEmpty { + debug!("init"); + Ok(()) + } + + fn destroy(&self) { + debug!("destroy"); + } + + fn getattr(&self, _req: RequestInfo, path: &std::path::Path, _fh: Option) -> ResultEntry { + debug!("getattr: {:?}", path); + + if let Some(entry) = self.helper.stat_cache.get(path) { + return Ok((TTL, stat_to_fuse(&entry))); + } + + debug!("getattr: {:?}, no cache", path); + + + if path.as_os_str() == "/" { + let entry = Entry { + name: "name".to_string(), + datetime: Utc::now().naive_utc(), + info: EntryInfo::Directory, + }; + + self.helper.stat_cache.insert(path, entry.clone()); + return Ok((TTL, stat_to_fuse(&entry))); + } + + match self.stat(path) { + Ok(entry) => { + self.helper.stat_cache.insert(path, entry.clone()); + Ok((TTL, stat_to_fuse(&entry))) + } + Err(e) => Err(e.into()) + } + } + + + fn open(&self, _req: RequestInfo, path: &std::path::Path, flags: u32) -> ResultOpen { + debug!("open: {:?} flags={:#x}", path, flags); + + match FatFs::open(self, path) { + Ok(fh) => { + Ok((self.helper.files.insert(fh), 0)) + } + Err(e) => { + error!("open({:?}) failed: {}", path, libc::EEXIST); + Err(e.into()) + } + } + } + + fn read(&self, _req: RequestInfo, path: &std::path::Path, fh: u64, offset: u64, size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { + debug!("read attempt: {:?} {:#x} @ {:#x}", path, size, offset); + + let file_arc = match self.helper.files.get(fh) { + Ok(dir) => { dir } + Err(_) => { return callback(Err(libc::EEXIST)); } + }; + + let mut file = file_arc.lock().unwrap(); + + if let Err(e) = file.seek(SeekFrom::Start(offset)) { + error!("seek({:?}, {}) failed: {}", path, offset, e); + return callback(Err(e.raw_os_error().unwrap())); + } + + let mut data = vec![0u8; size as usize]; + match file.read(&mut data) { + Ok(read_size) => { + data.resize(read_size, 0); + callback(Ok(&data)) + } + Err(_) => { + error!("read: {:?} {:#x} @ {:#x} failed.", path, size, offset); + callback(Err(libc::EFAULT)) + } + } + } + + fn release(&self, _req: RequestInfo, path: &std::path::Path, fh: u64, _flags: u32, _lock_owner: u64, _flush: bool) -> ResultEmpty { + debug!("release: {:?}", path); + self.helper.files.release(fh); + Ok(()) + } + + fn opendir(&self, _req: RequestInfo, path: &std::path::Path, _flags: u32) -> ResultOpen { + debug!("opendir: {:?} (flags = {:#o})", path, _flags); + + match FatFs::opendir(self, path) { + Ok(fh) => { + Ok((self.helper.dirs.insert(fh), 0)) + } + Err(e) => { + let new_e = e.into(); + error!("opendir({:?}): failed {}", path, new_e); + Err(new_e) + } + } + } + + fn readdir(&self, _req: RequestInfo, path: &std::path::Path, fh: u64) -> ResultReaddir { + debug!("readdir: {:?}", path); + let mut entries: Vec = vec![]; + + if fh == 0 { + error!("readdir: missing fh"); + return Err(libc::EINVAL); + } + + let dir_arc = match self.helper.dirs.get(fh) { + Ok(dir) => { dir } + Err(_) => { return Err(libc::EEXIST); } + }; + + let mut dir = dir_arc.lock().unwrap(); + + loop { + match dir.read() { + Ok(Some(entry)) => { + let mut full_path = path.to_path_buf(); + full_path.push(&entry.name); + + if self.helper.stat_cache.get(full_path.as_path()).is_none() { + self.helper.stat_cache.insert(full_path.as_path(), entry.clone()); + } + + let name = entry.name; + + let filetype = match entry.info { + EntryInfo::Directory => { FileType::Directory } + EntryInfo::File { .. } => { FileType::RegularFile } + }; + + entries.push(DirectoryEntry { + name: std::ffi::OsString::from(name), + kind: filetype, + }) + } + Ok(None) => { break; } + Err(e) => { + let new_val = e.into(); + error!("readdir failed: {:?}: {}", path, new_val); + return Err(new_val); + } + } + } + + Ok(entries) + } + + fn releasedir(&self, _req: RequestInfo, path: &std::path::Path, fh: u64, _flags: u32) -> ResultEmpty { + debug!("releasedir: {:?}", path); + self.helper.dirs.release(fh); + Ok(()) + } + + fn statfs(&self, _req: RequestInfo, path: &std::path::Path) -> ResultStatfs { + debug!("statfs: {:?}", path); + // TODO which values to actually put here? + Ok(Statfs { + blocks: 10000, + bfree: 1000, + bavail: 1000, + files: 5000, + ffree: 5000, + bsize: 4096, + namelen: 255, + frsize: 4096, + }) + } +} + +#[cfg(unix)] +impl From for libc::c_int { + fn from(value: fatfs::Error) -> Self { + // TODO almost all of these values are arbitrary set to EINVAL, correct mapping is needed + match value { + Error::DiskErr => { libc::EINVAL } + Error::IntErr => { libc::EINVAL } + Error::NotReady => { libc::EINVAL } + Error::NoFile => { libc::ENOENT } + Error::NoPath => { libc::ENOENT } + Error::InvalidName => { libc::ENOENT } + Error::Denied => { libc::EINVAL } + Error::Exist => { libc::EEXIST } + Error::InvalidObject => { libc::EFAULT } + Error::WriteProtected => { libc::EACCES } + Error::InvalidDrive => { libc::EINVAL } + Error::NotEnabled => { libc::EINVAL } + Error::NoFilesystem => { libc::EINVAL } + Error::MkfsAborted => { libc::EINVAL } + Error::Timeout => { libc::EINVAL } + Error::Locked => { libc::EINVAL } + Error::NotEnoughCore => { libc::EINVAL } + Error::TooManyOpenFiles => { libc::EINVAL } + Error::InvalidParameter => { libc::EINVAL } + Error::DriverInstalled => { libc::EINVAL } + Error::DriverNotInstalled => { libc::EINVAL } + Error::Unknown => { libc::EINVAL } + } + } +} + + impl Drop for FatFs { fn drop(&mut self) { self.unmount().ok(); @@ -439,7 +810,7 @@ unsafe extern "C" fn get_fattime() -> fatfs::DWORD { | (second << 1) } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct Entry { pub info: EntryInfo, pub name: String, @@ -521,6 +892,7 @@ impl From for Entry { } } +#[derive(Clone)] pub struct Directory { dir: fatfs::DIR, }