Added macos client

This commit is contained in:
Ultradesu
2026-03-11 00:05:13 +00:00
parent b7bbaa2d33
commit 57c9a3f456
5 changed files with 692 additions and 24 deletions

159
Cargo.lock generated
View File

@@ -91,7 +91,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -103,7 +103,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -136,7 +136,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -147,7 +147,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -266,12 +266,27 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bytestream"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f720842a717d6afaf69fee2dc69b771edc165f12cc3eb1b0e8eeef53a86454"
dependencies = [
"byteorder",
]
[[package]]
name = "cc"
version = "1.2.56"
@@ -327,7 +342,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -460,7 +475,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -518,6 +533,17 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -606,6 +632,24 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "furumi-mount-macos"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"ctrlc",
"furumi-client-core",
"furumi-common",
"libc",
"nfsserve",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "furumi-server"
version = "0.1.0"
@@ -708,7 +752,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1032,6 +1076,18 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libredox"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
"bitflags",
"libc",
"plain",
"redox_syscall 0.7.3",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -1129,6 +1185,26 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "nfsserve"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d73615e054238e6bf5e554407b5b23e82fc63616db459057c51b794799eda6fb"
dependencies = [
"anyhow",
"async-trait",
"byteorder",
"bytestream",
"filetime",
"futures",
"num-derive",
"num-traits",
"smallvec",
"tokio",
"tracing",
"tracing-attributes",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -1188,6 +1264,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -1282,7 +1369,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.18",
"smallvec",
"windows-link",
]
@@ -1330,7 +1417,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1351,6 +1438,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -1379,7 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1456,7 +1549,7 @@ dependencies = [
"prost",
"prost-types",
"regex",
"syn",
"syn 2.0.117",
"tempfile",
]
@@ -1470,7 +1563,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1585,6 +1678,15 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -1804,7 +1906,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1933,6 +2035,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -1958,7 +2071,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2006,7 +2119,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2017,7 +2130,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2085,7 +2198,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2166,7 +2279,7 @@ dependencies = [
"prost-build",
"prost-types",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2237,7 +2350,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -2391,7 +2504,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -2587,7 +2700,7 @@ dependencies = [
"heck",
"indexmap 2.13.0",
"prettyplease",
"syn",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -2603,7 +2716,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -2689,7 +2802,7 @@ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]

View File

@@ -3,6 +3,12 @@ members = [
"furumi-common",
"furumi-server",
"furumi-client-core",
"furumi-mount-linux"
"furumi-mount-linux",
"furumi-mount-macos"
]
default-members = [
"furumi-common",
"furumi-server",
"furumi-client-core",
]
resolver = "2"

View File

@@ -0,0 +1,18 @@
[package]
name = "furumi-mount-macos"
version = "0.1.0"
edition = "2024"
[dependencies]
furumi-common = { path = "../furumi-common" }
furumi-client-core = { path = "../furumi-client-core" }
anyhow = "1.0.102"
nfsserve = "0.10.2"
async-trait = "0.1.89"
clap = { version = "4.5.60", features = ["derive", "env"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tokio = { version = "1.50.0", features = ["full"] }
tokio-stream = "0.1.18"
ctrlc = "3.5.2"
libc = "0.2.183"

View File

@@ -0,0 +1,125 @@
pub mod nfs;
use clap::Parser;
use furumi_client_core::FurumiClient;
use nfsserve::tcp::{NFSTcp, NFSTcpListener};
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tracing::info;
#[derive(Parser, Debug)]
#[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + NFS (macOS)")]
struct Args {
/// Server address (IP:PORT, will use https:// by default unless --no-tls is specified)
#[arg(short, long, env = "FURUMI_SERVER", default_value = "0.0.0.0:50051")]
server: String,
/// Authentication Bearer token (leave empty if auth is disabled on server)
#[arg(short, long, env = "FURUMI_TOKEN", default_value = "")]
token: String,
/// Mount point directory
#[arg(short, long, env = "FURUMI_MOUNT")]
mount: PathBuf,
/// Disable TLS encryption
#[arg(long, default_value_t = false)]
no_tls: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let args = Args::parse();
if !args.mount.exists() || !args.mount.is_dir() {
eprintln!(
"Error: Mount point {:?} does not exist or is not a directory",
args.mount
);
std::process::exit(1);
}
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let full_addr = if args.no_tls {
if args.server.starts_with("http://") || args.server.starts_with("https://") {
args.server.clone()
} else {
format!("http://{}", args.server)
}
} else if args.server.starts_with("http://") || args.server.starts_with("https://") {
args.server.clone()
} else {
format!("https://{}", args.server)
};
let client = rt.block_on(async { FurumiClient::connect(&full_addr, &args.token).await })?;
let furumi_nfs = nfs::FurumiNfs::new(client);
let mount_point = args.mount.clone();
let mount_point_umount = args.mount.clone();
// Set up signal handler for graceful shutdown
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
eprintln!("\nReceived signal, unmounting...");
r.store(false, Ordering::SeqCst);
})?;
rt.block_on(async move {
// Bind NFS server to a random available port on localhost
let listener = NFSTcpListener::bind("127.0.0.1:0", furumi_nfs)
.await
.expect("Failed to bind NFS listener");
let port = listener.get_listen_port();
info!("NFS server listening on 127.0.0.1:{}", port);
// Start handling NFS connections BEFORE mounting —
// mount_nfs needs to talk to the server during mount.
let handle = tokio::spawn(async move {
if let Err(e) = listener.handle_forever().await {
eprintln!("NFS server error: {}", e);
}
});
// Mount via macOS native mount_nfs
let mount_path = mount_point.to_string_lossy().to_string();
let opts = format!(
"rdonly,locallocks,noresvport,nfsvers=3,tcp,rsize=1048576,port={},mountport={}",
port, port
);
let status = Command::new("mount_nfs")
.args(["-o", &opts, "127.0.0.1:/", &mount_path])
.status()
.expect("Failed to execute mount_nfs");
if !status.success() {
handle.abort();
eprintln!("Error: mount_nfs failed with status {}", status);
std::process::exit(1);
}
println!("Mounted Furumi-ng to {:?}", mount_path);
// Wait for shutdown signal
while running.load(Ordering::SeqCst) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
// Unmount
let _ = Command::new("umount").arg(mount_point_umount.to_string_lossy().as_ref()).status();
handle.abort();
println!("Unmounted successfully.");
});
Ok(())
}

View File

@@ -0,0 +1,406 @@
use async_trait::async_trait;
use furumi_client_core::FurumiClient;
use nfsserve::nfs::{
fattr3, fileid3, filename3, ftype3, mode3, nfspath3, nfsstat3, nfstime3, sattr3, specdata3,
};
use nfsserve::vfs::{DirEntry, NFSFileSystem, ReadDirResult, VFSCapabilities};
use std::collections::HashMap;
use std::sync::Mutex;
use tokio_stream::StreamExt;
use tracing::{debug, error};
// ── ID Mapper ──────────────────────────────────────────────────
/// Maps NFS file IDs (u64) to remote virtual filesystem paths and vice versa.
/// ID 1 is permanently assigned to the root directory ("/").
/// ID 0 is reserved by NFS and never used.
struct IdMapper {
id_to_path: HashMap<fileid3, String>,
path_to_id: HashMap<String, fileid3>,
next_id: fileid3,
}
impl IdMapper {
fn new() -> Self {
let mut id_to_path = HashMap::new();
let mut path_to_id = HashMap::new();
// Root is always ID 1 (0 is reserved in NFS)
id_to_path.insert(1, "/".to_string());
path_to_id.insert("/".to_string(), 1);
Self {
id_to_path,
path_to_id,
next_id: 2,
}
}
fn get_or_insert(&mut self, path: &str) -> fileid3 {
if let Some(&id) = self.path_to_id.get(path) {
return id;
}
let id = self.next_id;
self.next_id += 1;
self.id_to_path.insert(id, path.to_string());
self.path_to_id.insert(path.to_string(), id);
id
}
fn get_path(&self, id: fileid3) -> Option<&str> {
self.id_to_path.get(&id).map(|s| s.as_str())
}
}
// ── FurumiNfs ──────────────────────────────────────────────────
pub struct FurumiNfs {
client: FurumiClient,
mapper: Mutex<IdMapper>,
}
impl FurumiNfs {
pub fn new(client: FurumiClient) -> Self {
Self {
client,
mapper: Mutex::new(IdMapper::new()),
}
}
/// Build the full remote path from a parent directory ID and a child filename.
fn resolve_child_path(&self, dirid: fileid3, name: &filename3) -> Result<String, nfsstat3> {
let mapper = self.mapper.lock().unwrap();
let parent = mapper.get_path(dirid).ok_or(nfsstat3::NFS3ERR_STALE)?;
let name_str = String::from_utf8_lossy(&name.0);
let path = if parent == "/" {
format!("/{}", name_str)
} else {
format!("{}/{}", parent, name_str)
};
Ok(path)
}
/// Convert a gRPC AttrResponse into NFS fattr3, assigning/looking up the file ID.
fn make_fattr(
&self,
path: &str,
attr: &furumi_common::proto::AttrResponse,
) -> fattr3 {
let mut mapper = self.mapper.lock().unwrap();
let id = mapper.get_or_insert(path);
let is_dir = attr.mode & (libc::S_IFDIR as u32) != 0;
let ftype = if is_dir {
ftype3::NF3DIR
} else {
ftype3::NF3REG
};
let mtime = nfstime3 {
seconds: attr.mtime as u32,
nseconds: 0,
};
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
fattr3 {
ftype,
mode: (attr.mode & 0o777) as mode3,
nlink: if is_dir { 2 } else { 1 },
uid,
gid,
size: attr.size,
used: attr.size,
rdev: specdata3::default(),
fsid: 0,
fileid: id,
atime: mtime,
mtime,
ctime: mtime,
}
}
}
#[async_trait]
impl NFSFileSystem for FurumiNfs {
fn capabilities(&self) -> VFSCapabilities {
VFSCapabilities::ReadOnly
}
fn root_dir(&self) -> fileid3 {
1
}
async fn lookup(&self, dirid: fileid3, filename: &filename3) -> Result<fileid3, nfsstat3> {
let name_str = String::from_utf8_lossy(&filename.0);
debug!("lookup: dirid={} name={}", dirid, name_str);
let path = self.resolve_child_path(dirid, filename)?;
let attr = self
.client
.get_attr(&path)
.await
.map_err(|_| nfsstat3::NFS3ERR_NOENT)?;
let mut mapper = self.mapper.lock().unwrap();
let id = mapper.get_or_insert(&path);
// Sanity: verify the attr makes sense (file exists)
let _ = attr;
debug!("lookup: {} -> id={}", path, id);
Ok(id)
}
async fn getattr(&self, id: fileid3) -> Result<fattr3, nfsstat3> {
debug!("getattr: id={}", id);
let path = {
let mapper = self.mapper.lock().unwrap();
mapper
.get_path(id)
.ok_or(nfsstat3::NFS3ERR_STALE)?
.to_string()
};
let attr = self
.client
.get_attr(&path)
.await
.map_err(|_| nfsstat3::NFS3ERR_IO)?;
Ok(self.make_fattr(&path, &attr))
}
async fn setattr(&self, _id: fileid3, _setattr: sattr3) -> Result<fattr3, nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn read(
&self,
id: fileid3,
offset: u64,
count: u32,
) -> Result<(Vec<u8>, bool), nfsstat3> {
debug!("read: id={} offset={} count={}", id, offset, count);
let path = {
let mapper = self.mapper.lock().unwrap();
mapper
.get_path(id)
.ok_or(nfsstat3::NFS3ERR_STALE)?
.to_string()
};
let mut stream = self
.client
.read_file(&path, offset, count, count)
.await
.map_err(|e| {
error!("read_file error: {:?}", e);
nfsstat3::NFS3ERR_IO
})?;
let mut data = Vec::new();
while let Some(chunk_res) = stream.next().await {
match chunk_res {
Ok(chunk) => data.extend_from_slice(&chunk.data),
Err(e) => {
error!("read stream error: {:?}", e);
return Err(nfsstat3::NFS3ERR_IO);
}
}
}
// EOF if we got less data than requested
let eof = (data.len() as u32) < count;
Ok((data, eof))
}
async fn write(&self, _id: fileid3, _offset: u64, _data: &[u8]) -> Result<fattr3, nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn create(
&self,
_dirid: fileid3,
_filename: &filename3,
_attr: sattr3,
) -> Result<(fileid3, fattr3), nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn create_exclusive(
&self,
_dirid: fileid3,
_filename: &filename3,
) -> Result<fileid3, nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn mkdir(
&self,
_dirid: fileid3,
_dirname: &filename3,
) -> Result<(fileid3, fattr3), nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn remove(&self, _dirid: fileid3, _filename: &filename3) -> Result<(), nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn rename(
&self,
_from_dirid: fileid3,
_from_filename: &filename3,
_to_dirid: fileid3,
_to_filename: &filename3,
) -> Result<(), nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn readdir(
&self,
dirid: fileid3,
start_after: fileid3,
max_entries: usize,
) -> Result<ReadDirResult, nfsstat3> {
debug!(
"readdir: dirid={} start_after={} max={}",
dirid, start_after, max_entries
);
let path = {
let mapper = self.mapper.lock().unwrap();
mapper
.get_path(dirid)
.ok_or(nfsstat3::NFS3ERR_STALE)?
.to_string()
};
let entries = self.client.read_dir(&path).await.map_err(|e| {
error!("read_dir error: {:?}", e);
nfsstat3::NFS3ERR_IO
})?;
// Build full list with IDs and attributes
let mut nfs_entries: Vec<DirEntry> = Vec::new();
for entry in &entries {
let child_path = if path == "/" {
format!("/{}", entry.name)
} else {
format!("{}/{}", path, entry.name)
};
let is_dir = entry.r#type == 4; // DT_DIR
let child_id = {
let mut mapper = self.mapper.lock().unwrap();
mapper.get_or_insert(&child_path)
};
// Try to get real attributes, fall back to synthetic ones
let attr = match self.client.get_attr(&child_path).await {
Ok(a) => self.make_fattr(&child_path, &a),
Err(_) => {
let mut mapper = self.mapper.lock().unwrap();
let id = mapper.get_or_insert(&child_path);
let ftype = if is_dir {
ftype3::NF3DIR
} else {
ftype3::NF3REG
};
fattr3 {
ftype,
mode: if is_dir { 0o755 } else { 0o644 },
nlink: if is_dir { 2 } else { 1 },
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
size: 0,
used: 0,
rdev: specdata3::default(),
fsid: 0,
fileid: id,
atime: nfstime3::default(),
mtime: nfstime3::default(),
ctime: nfstime3::default(),
}
}
};
nfs_entries.push(DirEntry {
fileid: child_id,
name: entry.name.as_bytes().into(),
attr,
});
}
// Handle pagination: skip entries up to and including start_after
let filtered: Vec<DirEntry> = if start_after > 0 {
let skip_pos = nfs_entries
.iter()
.position(|e| e.fileid == start_after)
.map(|p| p + 1)
.unwrap_or(0);
nfs_entries.into_iter().skip(skip_pos).collect()
} else {
nfs_entries
};
let end = filtered.len() <= max_entries;
let entries: Vec<DirEntry> = filtered.into_iter().take(max_entries).collect();
Ok(ReadDirResult { entries, end })
}
async fn symlink(
&self,
_dirid: fileid3,
_linkname: &filename3,
_symlink: &nfspath3,
_attr: &sattr3,
) -> Result<(fileid3, fattr3), nfsstat3> {
Err(nfsstat3::NFS3ERR_ROFS)
}
async fn readlink(&self, _id: fileid3) -> Result<nfspath3, nfsstat3> {
Err(nfsstat3::NFS3ERR_NOTSUPP)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_mapper_root() {
let mapper = IdMapper::new();
assert_eq!(mapper.get_path(1).unwrap(), "/");
}
#[test]
fn test_id_mapper_assignment() {
let mut mapper = IdMapper::new();
let id1 = mapper.get_or_insert("/hello.txt");
assert_eq!(id1, 2);
let id2 = mapper.get_or_insert("/dir/world.mkv");
assert_eq!(id2, 3);
assert_eq!(mapper.get_path(2).unwrap(), "/hello.txt");
assert_eq!(mapper.get_path(3).unwrap(), "/dir/world.mkv");
// Same path returns same ID
assert_eq!(mapper.get_or_insert("/hello.txt"), 2);
}
#[test]
fn test_id_mapper_non_existent() {
let mapper = IdMapper::new();
assert!(mapper.get_path(999).is_none());
}
}