From b4e49c761a20da1f112eba975b1a96f0be8aeadd Mon Sep 17 00:00:00 2001 From: vasilito Date: Wed, 8 Jul 2026 19:51:36 +0300 Subject: [PATCH] Redox scheme:// VFS backend (target_os gate for cross-compile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added src/vfs/redox_scheme.rs — VFS backend that maps scheme:name/path to Redox kernel scheme operations via redox_syscall. Implements: - read_dir: opens scheme dir, reads newline-separated entry list, stats each child for metadata - stat/exists/is_dir/is_file: via open(O_STAT) + fstat - open_read: via open(O_RDONLY) + read, buffers file content Gated behind #[cfg(target_os = redox)] — entire module compiles out on Linux. Registered in vfs/mod.rs for_path() dispatch under "scheme". Includes path parsing tests for scheme:file:/home/user paths. This is the last remaining MC parity item (MC supports fish://, ftp://, sftp:// on Linux — TLC now supports scheme:// on Redox). --- local/recipes/tui/tlc/README.md | 4 +- local/recipes/tui/tlc/source/src/vfs/mod.rs | 9 +- .../tui/tlc/source/src/vfs/redox_scheme.rs | 209 ++++++++++++++++++ 3 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 local/recipes/tui/tlc/source/src/vfs/redox_scheme.rs diff --git a/local/recipes/tui/tlc/README.md b/local/recipes/tui/tlc/README.md index fcd3dfc0df..c808f99f83 100644 --- a/local/recipes/tui/tlc/README.md +++ b/local/recipes/tui/tlc/README.md @@ -155,8 +155,8 @@ dialog. Selection persists to `~/.config/tlc/config.toml`. | 4 keymap + dispatcher | ✅ | | 5 editor polish | ✅ | | 6 filemanager + viewer extras | ✅ | -| 7 VFS (local + remote backends) | ✅ SFTP full read-write, FTP, tar/cpio/zip/extfs read-only | -| 8 archives + skin + i18n | ✅ browse+extract (tar/cpio/zip), compress (Alt-Shift-C), 8 skins, i18n | +| 7 VFS (local + remote backends) | ✅ SFTP full read-write, FTP, scheme:// (Redox), tar/cpio/zip/extfs | +| 8 archives + skin + i18n | ✅ browse+extract+compress (Alt-Shift-C), 8 skins, i18n | | 13 skins + runtime selection | ✅ 8 built-in skins, Alt-S dialog, config persistence | | 14a critical features | ✅ menu bar (F9), select/unselect group, quick cd, Ctrl-O sub-shell, Alt-Enter cmdline | | 14b bug fixes | ✅ viewer keys, editor cursor sync, overwrite dialog, Ctrl-O sub-shell | diff --git a/local/recipes/tui/tlc/source/src/vfs/mod.rs b/local/recipes/tui/tlc/source/src/vfs/mod.rs index 70690680a7..21b32f828a 100644 --- a/local/recipes/tui/tlc/source/src/vfs/mod.rs +++ b/local/recipes/tui/tlc/source/src/vfs/mod.rs @@ -2,8 +2,9 @@ //! //! VFS paths in TLC have the form `:///`. //! [`for_path`] dispatches the parsed [`VfsPath`] to the right backend -//! (`local`, `sftp`, `ftp`, `tar`, `cpio`, `zip`, `extfs`); backends -//! gated on Cargo features are only registered when that feature is on. +//! (`local`, `sftp`, `ftp`, `tar`, `cpio`, `zip`, `extfs`, `scheme`); +//! backends gated on Cargo features or target OS are only registered +//! when that gate is active. pub mod extfs; #[cfg(feature = "ftp")] @@ -11,6 +12,8 @@ pub mod ftp; pub mod known_hosts; pub mod local; pub mod path; +#[cfg(target_os = "redox")] +pub mod redox_scheme; #[cfg(feature = "sftp")] pub mod sftp; pub mod traits; @@ -101,6 +104,8 @@ pub fn for_path(p: &VfsPath) -> Result>> { Ok(Some(Box::new(extfs::ExtfsVfs::open_from_archive(archive)))) } "sftp" | "ftp" => Ok(None), + #[cfg(target_os = "redox")] + "scheme" => Ok(Some(Box::new(redox_scheme::RedoxSchemeVfs::new("file")))), _ => Ok(None), } } diff --git a/local/recipes/tui/tlc/source/src/vfs/redox_scheme.rs b/local/recipes/tui/tlc/source/src/vfs/redox_scheme.rs new file mode 100644 index 0000000000..e183159d1e --- /dev/null +++ b/local/recipes/tui/tlc/source/src/vfs/redox_scheme.rs @@ -0,0 +1,209 @@ +//! Redox scheme VFS backend. +//! +//! Exposes Redox kernel schemes (`scheme:name/path`) through the [`Vfs`] +//! trait. On Redox, every I/O resource is a scheme — filesystem (`file:`), +//! disks (`disk:`), network (`tcp:`, `udp:`), display (`display:`), etc. +//! This backend translates scheme paths into [`redox_syscall`] operations +//! so that TLC can browse and operate on any scheme that supports +//! directory listing and file reads. +//! +//! Only available when `target_os = "redox"`. On all other platforms the +//! entire module is compiled out — scheme paths are a Redox kernel +//! concept with no Linux/Unix equivalent. +//! +//! The backend is read-mostly: `read_dir`, `stat`, `exists`, `is_dir`, +//! `is_file`, `open_read` are implemented. Mutating operations +//! (`mkdir`, `remove`, `rename`, `open_write`) inherit the +//! [`VfsError::Unsupported`] default — most hardware schemes do not +//! support mutation through the file abstraction. + +#![cfg(target_os = "redox")] + +use std::io::{self, Read}; +use std::path::PathBuf; + +use redox_syscall::flag::{O_RDONLY, O_DIRECTORY, O_STAT}; +use redox_syscall::{self, Stat as RedoxStat}; + +use crate::fs::{FileType, Permissions, Stat}; +use crate::vfs::local::Entry; +use crate::vfs::path::VfsPath; +use crate::vfs::traits::{Vfs, VfsError}; + +/// A VFS backend that maps `scheme:name/path` to Redox kernel scheme +/// operations. +pub struct RedoxSchemeVfs { + /// The scheme prefix (e.g. `"file"`, `"disk"`). + scheme: String, +} + +impl RedoxSchemeVfs { + /// Create a new scheme VFS for the given scheme name. + #[must_use] + pub fn new(scheme: impl Into) -> Self { + Self { scheme: scheme.into() } + } + + /// Convert a [`VfsPath`] to an absolute scheme path string. + fn scheme_path(p: &VfsPath) -> Result { + let s = p.as_str(); + if let Some(rest) = s.strip_prefix("scheme:") { + let colon = rest.find(':').ok_or_else(|| { + VfsError::Other("scheme path missing colon after scheme name".into()) + })?; + let (name, path) = rest.split_at(colon); + let path = &path[1..]; // skip the colon + Ok(format!("{name}:{path}")) + } else { + Err(VfsError::Other("not a scheme path".into())) + } + } + + /// Map a redox `Stat` to TLC's [`Stat`]. + fn map_stat(rs: &RedoxStat) -> Stat { + let file_type = if rs.st_mode & redox_syscall::flag::MODE_DIR != 0 { + FileType::Directory + } else if rs.st_mode & redox_syscall::flag::MODE_SYMLINK != 0 { + FileType::Symlink + } else { + FileType::Regular + }; + Stat { + file_type, + permissions: Permissions { + mode: (rs.st_mode & 0o7777) as u16, + }, + size: rs.st_size as u64, + mtime: rs.st_mtime as i64, + atime: rs.st_atime as i64, + ctime: rs.st_ctime as i64, + uid: rs.st_uid, + gid: rs.st_gid, + nlink: rs.st_nlink as u64, + dev: rs.st_dev as u64, + ino: rs.st_ino, + blksize: rs.st_blksize as u64, + blocks: rs.st_blocks as u64, + } + } +} + +impl Vfs for RedoxSchemeVfs { + fn kind(&self) -> &'static str { + "scheme" + } + + fn read_dir(&self, p: &VfsPath, show_hidden: bool) -> Result, VfsError> { + let path = Self::scheme_path(p)?; + let path_c = std::ffi::CString::new(path.clone()) + .map_err(|e| VfsError::Other(format!("scheme path: {e}")))?; + let fd = redox_syscall::open(&path_c, O_RDONLY | O_DIRECTORY | O_STAT, 0) + .map_err(|e| VfsError::Other(format!("scheme open dir: {e}")))?; + let mut entries = Vec::new(); + let mut buf = vec![0u8; 4096]; + loop { + let n = redox_syscall::read(fd, &mut buf) + .map_err(|e| VfsError::Other(format!("scheme read dir: {e}")))?; + if n == 0 { + break; + } + // Scheme directory entries are newline-separated. + let chunk = String::from_utf8_lossy(&buf[..n]); + for line in chunk.lines() { + let name = line.trim().to_string(); + if name.is_empty() { + continue; + } + if !show_hidden && name.starts_with('.') { + continue; + } + let child_path = format!("{path}/{name}"); + let child_c = std::ffi::CString::new(&child_path) + .map_err(|_| VfsError::Other("invalid child path".into()))?; + let mut rs = RedoxStat::default(); + let stat_fd = redox_syscall::open(&child_c, O_RDONLY | O_STAT, 0) + .map_err(|e| VfsError::Other(format!("scheme stat child: {e}")))?; + let stat_result = redox_syscall::fstat(stat_fd, &mut rs); + let _ = redox_syscall::close(stat_fd); + let stat = match stat_result { + Ok(_) => Self::map_stat(&rs), + Err(_) => Stat::default(), + }; + entries.push(Entry { + name, + size: stat.size, + modified: stat.mtime, + is_dir: stat.file_type == FileType::Directory, + is_symlink: stat.file_type == FileType::Symlink, + permissions: stat.permissions, + }); + } + } + let _ = redox_syscall::close(fd); + entries.sort_by(|a, b| { + b.is_dir + .cmp(&a.is_dir) + .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) + }); + Ok(entries) + } + + fn stat(&self, p: &VfsPath) -> Result { + let path = Self::scheme_path(p)?; + let path_c = std::ffi::CString::new(path) + .map_err(|e| VfsError::Other(format!("scheme path: {e}")))?; + let fd = redox_syscall::open(&path_c, O_RDONLY | O_STAT, 0) + .map_err(|e| VfsError::Other(format!("scheme stat: {e}")))?; + let mut rs = RedoxStat::default(); + redox_syscall::fstat(fd, &mut rs) + .map_err(|e| VfsError::Other(format!("scheme fstat: {e}")))?; + let _ = redox_syscall::close(fd); + Ok(Self::map_stat(&rs)) + } + + fn exists(&self, p: &VfsPath) -> bool { + self.stat(p).is_ok() + } + + fn is_dir(&self, p: &VfsPath) -> Result { + self.stat(p).map(|s| s.file_type == FileType::Directory) + } + + fn is_file(&self, p: &VfsPath) -> Result { + self.stat(p).map(|s| s.file_type == FileType::Regular) + } + + fn open_read(&self, p: &VfsPath) -> Result, VfsError> { + let path = Self::scheme_path(p)?; + let path_c = std::ffi::CString::new(path.clone()) + .map_err(|e| VfsError::Other(format!("scheme path: {e}")))?; + let fd = redox_syscall::open(&path_c, O_RDONLY, 0) + .map_err(|e| VfsError::Other(format!("scheme open read: {e}")))?; + let mut buf = vec![0u8; fd as usize]; + let n = redox_syscall::read(fd, &mut buf) + .map_err(|e| VfsError::Other(format!("scheme read: {e}")))?; + buf.truncate(n); + let _ = redox_syscall::close(fd); + Ok(Box::new(io::Cursor::new(buf))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verify path parsing rejects non-scheme paths. + #[test] + fn scheme_path_rejects_local() { + let vfs_path = VfsPath::parse("local:///home/user").unwrap(); + assert!(RedoxSchemeVfs::scheme_path(&vfs_path).is_err()); + } + + /// Verify scheme path parsing for `scheme:file:/home`. + #[test] + fn scheme_path_parses_file_scheme() { + let vfs_path = VfsPath::parse("scheme:file:/home/user").unwrap(); + let result = RedoxSchemeVfs::scheme_path(&vfs_path).unwrap(); + assert_eq!(result, "file:/home/user"); + } +}