Comments help non-Rust users understand what each function, struct, and module does. Covers the core service (18 source files) and all four example projects (can-sync, canfs, filemanager, paste). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
325 lines
11 KiB
Rust
325 lines
11 KiB
Rust
use std::os::raw::c_void;
|
|
use std::sync::Arc;
|
|
|
|
use parking_lot::{Mutex, RwLock};
|
|
use tracing::{debug, warn};
|
|
use widestring::U16CStr;
|
|
|
|
use winfsp::filesystem::{
|
|
DirBuffer, DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, OpenFileInfo,
|
|
VolumeInfo, WideNameInfo,
|
|
};
|
|
use winfsp::FspError;
|
|
|
|
use crate::api::{AssetMeta, CanClient};
|
|
use crate::tree::{NodeId, NodeKind, VirtualTree};
|
|
use crate::util;
|
|
|
|
// NTSTATUS constants (raw i32 values to avoid windows crate version conflicts)
|
|
const STATUS_OBJECT_NAME_NOT_FOUND: i32 = 0xC0000034_u32 as i32;
|
|
const STATUS_NOT_A_DIRECTORY: i32 = 0xC0000103_u32 as i32;
|
|
const STATUS_UNEXPECTED_NETWORK_ERROR: i32 = 0xC00000C4_u32 as i32;
|
|
const STATUS_INVALID_DEVICE_REQUEST: i32 = 0xC0000010_u32 as i32;
|
|
|
|
// File attribute constants
|
|
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
|
|
const FILE_ATTRIBUTE_READONLY: u32 = 0x01;
|
|
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20;
|
|
|
|
// Wrap a raw NTSTATUS error code into WinFSP's error type.
|
|
fn ntstatus(code: i32) -> FspError {
|
|
FspError::NTSTATUS(code)
|
|
}
|
|
|
|
/// Shared cache state: asset list + virtual tree.
|
|
pub struct CacheState {
|
|
pub assets: Vec<AssetMeta>,
|
|
pub tree: VirtualTree,
|
|
}
|
|
|
|
/// The WinFSP filesystem context for CAN service.
|
|
pub struct CanFs {
|
|
pub cache: Arc<RwLock<CacheState>>,
|
|
pub client: Arc<CanClient>,
|
|
}
|
|
|
|
/// Per-open-handle context.
|
|
pub struct CanFileContext {
|
|
node_id: NodeId,
|
|
/// Lazily fetched file bytes.
|
|
content: Mutex<Option<Vec<u8>>>,
|
|
/// Directory enumeration buffer.
|
|
dir_buffer: DirBuffer,
|
|
}
|
|
|
|
impl FileSystemContext for CanFs {
|
|
type FileContext = CanFileContext;
|
|
|
|
/// Called by Windows to check if a file/folder exists and get its basic attributes before opening it.
|
|
fn get_security_by_name(
|
|
&self,
|
|
file_name: &U16CStr,
|
|
_security_descriptor: Option<&mut [c_void]>,
|
|
_resolve_reparse_points: impl FnOnce(&U16CStr) -> Option<FileSecurity>,
|
|
) -> winfsp::Result<FileSecurity> {
|
|
let path = util::normalize_path(file_name);
|
|
debug!("get_security_by_name: {}", path);
|
|
|
|
let cache = self.cache.read();
|
|
let node_id = cache
|
|
.tree
|
|
.lookup(&path)
|
|
.ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?;
|
|
let node = cache.tree.get(node_id);
|
|
|
|
let attributes = if node.is_directory() {
|
|
FILE_ATTRIBUTE_DIRECTORY
|
|
} else {
|
|
FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE
|
|
};
|
|
|
|
Ok(FileSecurity {
|
|
reparse: false,
|
|
sz_security_descriptor: 0,
|
|
attributes,
|
|
})
|
|
}
|
|
|
|
/// Called when a file or directory is opened; returns a context handle and fills in size/timestamps.
|
|
fn open(
|
|
&self,
|
|
file_name: &U16CStr,
|
|
_create_options: u32,
|
|
_granted_access: u32,
|
|
file_info: &mut OpenFileInfo,
|
|
) -> winfsp::Result<Self::FileContext> {
|
|
let path = util::normalize_path(file_name);
|
|
debug!("open: {}", path);
|
|
|
|
let cache = self.cache.read();
|
|
let node_id = cache
|
|
.tree
|
|
.lookup(&path)
|
|
.ok_or(ntstatus(STATUS_OBJECT_NAME_NOT_FOUND))?;
|
|
let node = cache.tree.get(node_id);
|
|
|
|
let fi = file_info.as_mut();
|
|
if node.is_directory() {
|
|
fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
fi.file_size = 0;
|
|
fi.allocation_size = 0;
|
|
} else {
|
|
fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
|
|
if let NodeKind::File { asset_index } = &node.kind {
|
|
let sz = cache.assets[*asset_index].size as u64;
|
|
fi.file_size = sz;
|
|
fi.allocation_size = sz;
|
|
} else {
|
|
fi.file_size = 0;
|
|
fi.allocation_size = 0;
|
|
}
|
|
}
|
|
|
|
if let NodeKind::File { asset_index } = &node.kind {
|
|
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
|
|
fi.creation_time = ts;
|
|
fi.last_access_time = ts;
|
|
fi.last_write_time = ts;
|
|
fi.change_time = ts;
|
|
} else {
|
|
fi.creation_time = 0;
|
|
fi.last_access_time = 0;
|
|
fi.last_write_time = 0;
|
|
fi.change_time = 0;
|
|
}
|
|
|
|
fi.index_number = 0;
|
|
fi.hard_links = 0;
|
|
fi.ea_size = 0;
|
|
fi.reparse_tag = 0;
|
|
|
|
Ok(CanFileContext {
|
|
node_id,
|
|
content: Mutex::new(None),
|
|
dir_buffer: DirBuffer::new(),
|
|
})
|
|
}
|
|
|
|
/// Called when a handle is closed; nothing to clean up since content is dropped automatically.
|
|
fn close(&self, _context: Self::FileContext) {}
|
|
|
|
/// Returns up-to-date size and attribute info for an already-opened file or directory.
|
|
fn get_file_info(
|
|
&self,
|
|
context: &Self::FileContext,
|
|
file_info: &mut FileInfo,
|
|
) -> winfsp::Result<()> {
|
|
let cache = self.cache.read();
|
|
let node = cache.tree.get(context.node_id);
|
|
|
|
if node.is_directory() {
|
|
file_info.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
file_info.file_size = 0;
|
|
file_info.allocation_size = 0;
|
|
file_info.creation_time = 0;
|
|
file_info.last_access_time = 0;
|
|
file_info.last_write_time = 0;
|
|
file_info.change_time = 0;
|
|
} else {
|
|
file_info.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
|
|
|
|
// Use actual downloaded size if available, otherwise metadata size
|
|
let content = context.content.lock();
|
|
if let Some(ref bytes) = *content {
|
|
let sz = bytes.len() as u64;
|
|
file_info.file_size = sz;
|
|
file_info.allocation_size = sz;
|
|
} else if let NodeKind::File { asset_index } = &node.kind {
|
|
let sz = cache.assets[*asset_index].size as u64;
|
|
file_info.file_size = sz;
|
|
file_info.allocation_size = sz;
|
|
} else {
|
|
file_info.file_size = 0;
|
|
file_info.allocation_size = 0;
|
|
}
|
|
|
|
if let NodeKind::File { asset_index } = &node.kind {
|
|
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
|
|
file_info.creation_time = ts;
|
|
file_info.last_access_time = ts;
|
|
file_info.last_write_time = ts;
|
|
file_info.change_time = ts;
|
|
}
|
|
}
|
|
|
|
file_info.index_number = 0;
|
|
file_info.hard_links = 0;
|
|
file_info.ea_size = 0;
|
|
file_info.reparse_tag = 0;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reads file bytes at the given offset; downloads the asset from the CAN service on first access.
|
|
fn read(
|
|
&self,
|
|
context: &Self::FileContext,
|
|
buffer: &mut [u8],
|
|
offset: u64,
|
|
) -> winfsp::Result<u32> {
|
|
let mut content = context.content.lock();
|
|
|
|
if content.is_none() {
|
|
let cache = self.cache.read();
|
|
let node = cache.tree.get(context.node_id);
|
|
if let NodeKind::File { asset_index } = &node.kind {
|
|
let hash = &cache.assets[*asset_index].hash;
|
|
debug!("fetching bytes for {}", hash);
|
|
match self.client.fetch_bytes(hash) {
|
|
Ok(bytes) => {
|
|
*content = Some(bytes);
|
|
}
|
|
Err(e) => {
|
|
warn!("failed to fetch asset: {}", e);
|
|
return Err(ntstatus(STATUS_UNEXPECTED_NETWORK_ERROR));
|
|
}
|
|
}
|
|
} else {
|
|
return Err(ntstatus(STATUS_INVALID_DEVICE_REQUEST));
|
|
}
|
|
}
|
|
|
|
let bytes = content.as_ref().unwrap();
|
|
let offset = offset as usize;
|
|
if offset >= bytes.len() {
|
|
return Ok(0);
|
|
}
|
|
let end = (offset + buffer.len()).min(bytes.len());
|
|
let count = end - offset;
|
|
buffer[..count].copy_from_slice(&bytes[offset..end]);
|
|
Ok(count as u32)
|
|
}
|
|
|
|
/// Lists the contents of a directory, including "." and ".." entries, for Windows Explorer and dir commands.
|
|
fn read_directory(
|
|
&self,
|
|
context: &Self::FileContext,
|
|
_pattern: Option<&U16CStr>,
|
|
marker: DirMarker,
|
|
buffer: &mut [u8],
|
|
) -> winfsp::Result<u32> {
|
|
let cache = self.cache.read();
|
|
let node = cache.tree.get(context.node_id);
|
|
|
|
if !node.is_directory() {
|
|
return Err(ntstatus(STATUS_NOT_A_DIRECTORY));
|
|
}
|
|
|
|
if let Ok(dir_buffer_lock) = context.dir_buffer.acquire(marker.is_none(), None) {
|
|
// "." entry
|
|
{
|
|
let mut di: DirInfo = DirInfo::new();
|
|
let _ = di.set_name(std::ffi::OsStr::new("."));
|
|
di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
let _ = dir_buffer_lock.write(&mut di);
|
|
}
|
|
// ".." entry
|
|
{
|
|
let mut di: DirInfo = DirInfo::new();
|
|
let _ = di.set_name(std::ffi::OsStr::new(".."));
|
|
di.file_info_mut().file_attributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
let _ = dir_buffer_lock.write(&mut di);
|
|
}
|
|
|
|
for &child_id in &node.children {
|
|
let child = cache.tree.get(child_id);
|
|
let mut di: DirInfo = DirInfo::new();
|
|
|
|
if di.set_name(std::ffi::OsStr::new(&child.name)).is_err() {
|
|
continue;
|
|
}
|
|
|
|
let fi = di.file_info_mut();
|
|
if child.is_directory() {
|
|
fi.file_attributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
fi.file_size = 0;
|
|
fi.allocation_size = 0;
|
|
} else {
|
|
fi.file_attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_ARCHIVE;
|
|
if let NodeKind::File { asset_index } = &child.kind {
|
|
let sz = cache.assets[*asset_index].size as u64;
|
|
fi.file_size = sz;
|
|
fi.allocation_size = sz;
|
|
} else {
|
|
fi.file_size = 0;
|
|
fi.allocation_size = 0;
|
|
}
|
|
}
|
|
|
|
if let NodeKind::File { asset_index } = &child.kind {
|
|
let ts = util::epoch_ms_to_filetime(cache.assets[*asset_index].timestamp);
|
|
fi.creation_time = ts;
|
|
fi.last_access_time = ts;
|
|
fi.last_write_time = ts;
|
|
fi.change_time = ts;
|
|
}
|
|
|
|
fi.index_number = 0;
|
|
fi.hard_links = 0;
|
|
fi.ea_size = 0;
|
|
fi.reparse_tag = 0;
|
|
|
|
let _ = dir_buffer_lock.write(&mut di);
|
|
}
|
|
}
|
|
|
|
Ok(context.dir_buffer.read(marker, buffer))
|
|
}
|
|
|
|
/// Reports the virtual drive's total and free space (shows as a 1 GB read-only volume).
|
|
fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> {
|
|
out_volume_info.total_size = 1024 * 1024 * 1024; // 1 GB
|
|
out_volume_info.free_size = 0;
|
|
Ok(())
|
|
}
|
|
}
|