Redox scheme:// VFS backend (target_os gate for cross-compile)

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).
This commit is contained in:
2026-07-08 19:51:36 +03:00
parent 5980b019df
commit b4e49c761a
3 changed files with 218 additions and 4 deletions
+2 -2
View File
@@ -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 |
+7 -2
View File
@@ -2,8 +2,9 @@
//!
//! VFS paths in TLC have the form `<scheme>://<authority>/<path>`.
//! [`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<Option<Box<dyn Vfs>>> {
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),
}
}
@@ -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<String>) -> Self {
Self { scheme: scheme.into() }
}
/// Convert a [`VfsPath`] to an absolute scheme path string.
fn scheme_path(p: &VfsPath) -> Result<String, VfsError> {
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<Vec<Entry>, 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<Stat, VfsError> {
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<bool, VfsError> {
self.stat(p).map(|s| s.file_type == FileType::Directory)
}
fn is_file(&self, p: &VfsPath) -> Result<bool, VfsError> {
self.stat(p).map(|s| s.file_type == FileType::Regular)
}
fn open_read(&self, p: &VfsPath) -> Result<Box<dyn Read + Send>, 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");
}
}