use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use lazy_static::lazy_static;
use spin::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct NfsFileHandle(pub u64);
#[derive(Debug, Clone)]
pub struct CopyFileRangeRequest {
pub src_fh: NfsFileHandle,
pub src_offset: u64,
pub dst_fh: NfsFileHandle,
pub dst_offset: u64,
pub size: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NfsOp {
Read,
Write,
Create,
Remove,
Getattr,
Setattr,
Readdir,
Clone,
CopyFileRange,
Allocate,
Deallocate,
}
impl NfsOp {
pub fn name(&self) -> &'static str {
match self {
NfsOp::Read => "READ",
NfsOp::Write => "WRITE",
NfsOp::Create => "CREATE",
NfsOp::Remove => "REMOVE",
NfsOp::Getattr => "GETATTR",
NfsOp::Setattr => "SETATTR",
NfsOp::Readdir => "READDIR",
NfsOp::Clone => "CLONE",
NfsOp::CopyFileRange => "COPY_FILE_RANGE",
NfsOp::Allocate => "ALLOCATE",
NfsOp::Deallocate => "DEALLOCATE",
}
}
}
#[derive(Debug, Clone)]
pub struct NfsAttrs {
pub fh: NfsFileHandle,
pub file_type: FileType,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub atime: u64,
pub mtime: u64,
pub ctime: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Regular,
Directory,
Symlink,
}
#[derive(Debug, Clone)]
pub struct NfsClient {
pub id: u64,
pub address: String,
pub connected: bool,
pub last_activity: u64,
pub open_files: Vec<NfsFileHandle>,
}
impl NfsClient {
pub fn new(id: u64, address: String, timestamp: u64) -> Self {
Self {
id,
address,
connected: true,
last_activity: timestamp,
open_files: Vec::new(),
}
}
pub fn activity(&mut self, timestamp: u64) {
self.last_activity = timestamp;
}
pub fn open_file(&mut self, fh: NfsFileHandle) {
if !self.open_files.contains(&fh) {
self.open_files.push(fh);
}
}
pub fn close_file(&mut self, fh: NfsFileHandle) {
self.open_files.retain(|&f| f != fh);
}
}
#[derive(Debug, Clone)]
pub struct NfsExport {
pub path: String,
pub dataset_id: u64,
pub read_only: bool,
pub allowed_clients: Vec<String>,
}
impl NfsExport {
pub fn new(path: String, dataset_id: u64, read_only: bool) -> Self {
Self {
path,
dataset_id,
read_only,
allowed_clients: Vec::new(),
}
}
pub fn is_client_allowed(&self, client_addr: &str) -> bool {
if self.allowed_clients.is_empty() {
return true; }
self.allowed_clients.iter().any(|addr| addr == client_addr)
}
}
#[derive(Debug, Clone, Default)]
pub struct NfsStats {
pub total_ops: u64,
pub reads: u64,
pub writes: u64,
pub clones: u64,
pub copy_file_ranges: u64,
pub bytes_read: u64,
pub bytes_written: u64,
pub active_clients: u64,
}
lazy_static! {
static ref NFS_SERVER: Mutex<NfsServer> = Mutex::new(NfsServer::new());
}
pub struct NfsServer {
exports: Vec<NfsExport>,
clients: BTreeMap<u64, NfsClient>,
file_handles: BTreeMap<NfsFileHandle, (u64, u64)>,
next_fh: u64,
next_client_id: u64,
stats: NfsStats,
}
impl Default for NfsServer {
fn default() -> Self {
Self::new()
}
}
impl NfsServer {
pub fn new() -> Self {
Self {
exports: Vec::new(),
clients: BTreeMap::new(),
file_handles: BTreeMap::new(),
next_fh: 1,
next_client_id: 1,
stats: NfsStats::default(),
}
}
pub fn add_export(&mut self, export: NfsExport) {
crate::lcpfs_println!(
"[ NFS ] Exported {} (dataset: {}, ro: {})",
export.path,
export.dataset_id,
export.read_only
);
self.exports.push(export);
}
pub fn connect_client(&mut self, address: String, timestamp: u64) -> Result<u64, &'static str> {
let client_id = self.next_client_id;
self.next_client_id += 1;
let client = NfsClient::new(client_id, address.clone(), timestamp);
crate::lcpfs_println!("[ NFS ] Client {} connected from {}", client_id, address);
self.clients.insert(client_id, client);
self.stats.active_clients += 1;
Ok(client_id)
}
pub fn allocate_fh(&mut self, dataset_id: u64, offset: u64) -> NfsFileHandle {
let fh = NfsFileHandle(self.next_fh);
self.next_fh += 1;
self.file_handles.insert(fh, (dataset_id, offset));
fh
}
pub fn read(
&mut self,
client_id: u64,
fh: NfsFileHandle,
offset: u64,
size: u64,
timestamp: u64,
) -> Result<u64, &'static str> {
let client = self.clients.get_mut(&client_id).ok_or("Client not found")?;
client.activity(timestamp);
self.file_handles.get(&fh).ok_or("Invalid file handle")?;
self.stats.total_ops += 1;
self.stats.reads += 1;
self.stats.bytes_read += size;
Ok(size)
}
pub fn write(
&mut self,
client_id: u64,
fh: NfsFileHandle,
offset: u64,
size: u64,
timestamp: u64,
) -> Result<u64, &'static str> {
let client = self.clients.get_mut(&client_id).ok_or("Client not found")?;
client.activity(timestamp);
let (dataset_id, _) = self.file_handles.get(&fh).ok_or("Invalid file handle")?;
let export = self.exports.iter().find(|e| e.dataset_id == *dataset_id);
if let Some(exp) = export {
if exp.read_only {
return Err("Export is read-only");
}
}
self.stats.total_ops += 1;
self.stats.writes += 1;
self.stats.bytes_written += size;
Ok(size)
}
pub fn clone(
&mut self,
client_id: u64,
src_fh: NfsFileHandle,
dst_fh: NfsFileHandle,
timestamp: u64,
) -> Result<(), &'static str> {
let client = self.clients.get_mut(&client_id).ok_or("Client not found")?;
client.activity(timestamp);
self.file_handles
.get(&src_fh)
.ok_or("Invalid source file handle")?;
self.file_handles
.get(&dst_fh)
.ok_or("Invalid destination file handle")?;
self.stats.total_ops += 1;
self.stats.clones += 1;
crate::lcpfs_println!(
"[ NFS ] Clone: {:?} -> {:?} (client: {})",
src_fh,
dst_fh,
client_id
);
Ok(())
}
pub fn copy_file_range(
&mut self,
client_id: u64,
request: CopyFileRangeRequest,
timestamp: u64,
) -> Result<u64, &'static str> {
let client = self.clients.get_mut(&client_id).ok_or("Client not found")?;
client.activity(timestamp);
self.file_handles
.get(&request.src_fh)
.ok_or("Invalid source file handle")?;
self.file_handles
.get(&request.dst_fh)
.ok_or("Invalid destination file handle")?;
self.stats.total_ops += 1;
self.stats.copy_file_ranges += 1;
Ok(request.size)
}
pub fn getattr(
&mut self,
client_id: u64,
fh: NfsFileHandle,
timestamp: u64,
) -> Result<NfsAttrs, &'static str> {
let client = self.clients.get_mut(&client_id).ok_or("Client not found")?;
client.activity(timestamp);
self.file_handles.get(&fh).ok_or("Invalid file handle")?;
self.stats.total_ops += 1;
Ok(NfsAttrs {
fh,
file_type: FileType::Regular,
mode: 0o644,
uid: 1000,
gid: 1000,
size: 4096,
atime: timestamp,
mtime: timestamp,
ctime: timestamp,
})
}
pub fn disconnect_client(&mut self, client_id: u64) {
if self.clients.remove(&client_id).is_some() {
self.stats.active_clients = self.stats.active_clients.saturating_sub(1);
crate::lcpfs_println!("[ NFS ] Client {} disconnected", client_id);
}
}
pub fn stats(&self) -> NfsStats {
self.stats.clone()
}
pub fn export_count(&self) -> usize {
self.exports.len()
}
}
pub struct Nfs;
impl Nfs {
pub fn add_export(export: NfsExport) {
let mut server = NFS_SERVER.lock();
server.add_export(export);
}
pub fn connect_client(address: String, timestamp: u64) -> Result<u64, &'static str> {
let mut server = NFS_SERVER.lock();
server.connect_client(address, timestamp)
}
pub fn read(
client_id: u64,
fh: NfsFileHandle,
offset: u64,
size: u64,
timestamp: u64,
) -> Result<u64, &'static str> {
let mut server = NFS_SERVER.lock();
server.read(client_id, fh, offset, size, timestamp)
}
pub fn write(
client_id: u64,
fh: NfsFileHandle,
offset: u64,
size: u64,
timestamp: u64,
) -> Result<u64, &'static str> {
let mut server = NFS_SERVER.lock();
server.write(client_id, fh, offset, size, timestamp)
}
pub fn clone(
client_id: u64,
src_fh: NfsFileHandle,
dst_fh: NfsFileHandle,
timestamp: u64,
) -> Result<(), &'static str> {
let mut server = NFS_SERVER.lock();
server.clone(client_id, src_fh, dst_fh, timestamp)
}
pub fn stats() -> NfsStats {
let server = NFS_SERVER.lock();
server.stats()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nfs_op_names() {
assert_eq!(NfsOp::Read.name(), "READ");
assert_eq!(NfsOp::Clone.name(), "CLONE");
assert_eq!(NfsOp::CopyFileRange.name(), "COPY_FILE_RANGE");
}
#[test]
fn test_file_handle() {
let fh1 = NfsFileHandle(1);
let fh2 = NfsFileHandle(2);
assert_ne!(fh1, fh2);
assert!(fh1 < fh2);
}
#[test]
fn test_export_creation() {
let export = NfsExport::new("/tank/data".into(), 100, false);
assert_eq!(export.path, "/tank/data");
assert_eq!(export.dataset_id, 100);
assert!(!export.read_only);
}
#[test]
fn test_client_allowed() {
let mut export = NfsExport::new("/tank".into(), 100, false);
assert!(export.is_client_allowed("192.168.1.100"));
export.allowed_clients.push("192.168.1.100".into());
assert!(export.is_client_allowed("192.168.1.100"));
assert!(!export.is_client_allowed("192.168.1.101"));
}
#[test]
fn test_client_connection() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
assert_eq!(client_id, 1);
assert_eq!(server.stats.active_clients, 1);
}
#[test]
fn test_file_handle_allocation() {
let mut server = NfsServer::new();
let fh1 = server.allocate_fh(100, 0);
let fh2 = server.allocate_fh(100, 4096);
assert_eq!(fh1, NfsFileHandle(1));
assert_eq!(fh2, NfsFileHandle(2));
}
#[test]
fn test_read_operation() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let fh = server.allocate_fh(100, 0);
let size = server
.read(client_id, fh, 0, 4096, 1100)
.expect("test: operation should succeed");
assert_eq!(size, 4096);
assert_eq!(server.stats.reads, 1);
assert_eq!(server.stats.bytes_read, 4096);
}
#[test]
fn test_write_operation() {
let mut server = NfsServer::new();
let export = NfsExport::new("/tank".into(), 100, false);
server.add_export(export);
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let fh = server.allocate_fh(100, 0);
let size = server
.write(client_id, fh, 0, 8192, 1100)
.expect("test: operation should succeed");
assert_eq!(size, 8192);
assert_eq!(server.stats.writes, 1);
assert_eq!(server.stats.bytes_written, 8192);
}
#[test]
fn test_read_only_export() {
let mut server = NfsServer::new();
let export = NfsExport::new("/tank".into(), 100, true);
server.add_export(export);
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let fh = server.allocate_fh(100, 0);
let result = server.write(client_id, fh, 0, 4096, 1100);
assert!(result.is_err());
}
#[test]
fn test_clone_operation() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let src_fh = server.allocate_fh(100, 0);
let dst_fh = server.allocate_fh(100, 4096);
let result = server.clone(client_id, src_fh, dst_fh, 1100);
assert!(result.is_ok());
assert_eq!(server.stats.clones, 1);
}
#[test]
fn test_copy_file_range() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let src_fh = server.allocate_fh(100, 0);
let dst_fh = server.allocate_fh(100, 8192);
let request = CopyFileRangeRequest {
src_fh,
src_offset: 0,
dst_fh,
dst_offset: 0,
size: 4096,
};
let size = server
.copy_file_range(client_id, request, 1100)
.expect("test: operation should succeed");
assert_eq!(size, 4096);
assert_eq!(server.stats.copy_file_ranges, 1);
}
#[test]
fn test_getattr() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let fh = server.allocate_fh(100, 0);
let attrs = server
.getattr(client_id, fh, 1100)
.expect("test: operation should succeed");
assert_eq!(attrs.fh, fh);
assert_eq!(attrs.file_type, FileType::Regular);
}
#[test]
fn test_client_activity() {
let mut client = NfsClient::new(1, "192.168.1.100:2049".into(), 1000);
assert_eq!(client.last_activity, 1000);
client.activity(2000);
assert_eq!(client.last_activity, 2000);
}
#[test]
fn test_client_open_files() {
let mut client = NfsClient::new(1, "192.168.1.100:2049".into(), 1000);
let fh1 = NfsFileHandle(1);
let fh2 = NfsFileHandle(2);
client.open_file(fh1);
client.open_file(fh2);
assert_eq!(client.open_files.len(), 2);
client.close_file(fh1);
assert_eq!(client.open_files.len(), 1);
}
#[test]
fn test_client_disconnect() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
assert_eq!(server.stats.active_clients, 1);
server.disconnect_client(client_id);
assert_eq!(server.stats.active_clients, 0);
}
#[test]
fn test_invalid_file_handle() {
let mut server = NfsServer::new();
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let invalid_fh = NfsFileHandle(999);
let result = server.read(client_id, invalid_fh, 0, 4096, 1100);
assert!(result.is_err());
}
#[test]
fn test_statistics() {
let mut server = NfsServer::new();
let export = NfsExport::new("/tank".into(), 100, false);
server.add_export(export);
let client_id = server
.connect_client("192.168.1.100:2049".into(), 1000)
.expect("test: operation should succeed");
let fh = server.allocate_fh(100, 0);
server
.read(client_id, fh, 0, 1024, 1100)
.expect("test: operation should succeed");
server
.write(client_id, fh, 0, 2048, 1200)
.expect("test: operation should succeed");
let stats = server.stats();
assert_eq!(stats.total_ops, 2);
assert_eq!(stats.reads, 1);
assert_eq!(stats.writes, 1);
assert_eq!(stats.bytes_read, 1024);
assert_eq!(stats.bytes_written, 2048);
}
}