use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use super::FileSystem;
#[derive(Clone, Default)]
pub struct InMemoryFileSystem {
files: Arc<RwLock<HashMap<PathBuf, String>>>,
binary_files: Arc<RwLock<HashMap<PathBuf, Vec<u8>>>>,
directories: Arc<RwLock<HashSet<PathBuf>>>,
}
impl InMemoryFileSystem {
pub fn new() -> Self {
Self {
files: Arc::new(RwLock::new(HashMap::new())),
binary_files: Arc::new(RwLock::new(HashMap::new())),
directories: Arc::new(RwLock::new(HashSet::new())),
}
}
pub fn with_files(entries: Vec<(PathBuf, String)>) -> Self {
let fs = Self::new();
{
let mut files = fs.files.write().unwrap();
let mut dirs = fs.directories.write().unwrap();
for (path, content) in entries {
let mut current = path.as_path();
while let Some(parent) = current.parent() {
if !parent.as_os_str().is_empty() {
dirs.insert(parent.to_path_buf());
}
current = parent;
}
files.insert(path, content);
}
}
fs
}
pub fn load_from_entries(entries: Vec<(String, String)>) -> Self {
let entries: Vec<(PathBuf, String)> = entries
.into_iter()
.map(|(path, content)| (PathBuf::from(path), content))
.collect();
Self::with_files(entries)
}
pub fn export_entries(&self) -> Vec<(String, String)> {
let files = self.files.read().unwrap();
files
.iter()
.map(|(path, content)| (path.to_string_lossy().to_string(), content.clone()))
.collect()
}
pub fn export_binary_entries(&self) -> Vec<(String, Vec<u8>)> {
let binary_files = self.binary_files.read().unwrap();
binary_files
.iter()
.map(|(path, content)| (path.to_string_lossy().to_string(), content.clone()))
.collect()
}
pub fn load_binary_entries(&self, entries: Vec<(String, Vec<u8>)>) {
let mut binary_files = self.binary_files.write().unwrap();
let mut dirs = self.directories.write().unwrap();
for (path_str, content) in entries {
let path = PathBuf::from(&path_str);
let mut current = path.as_path();
while let Some(parent) = current.parent() {
if !parent.as_os_str().is_empty() {
dirs.insert(parent.to_path_buf());
}
current = parent;
}
binary_files.insert(path, content);
}
}
pub fn list_all_files(&self) -> Vec<PathBuf> {
let files = self.files.read().unwrap();
files.keys().cloned().collect()
}
pub fn clear(&self) {
let mut files = self.files.write().unwrap();
let mut dirs = self.directories.write().unwrap();
files.clear();
dirs.clear();
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
use std::path::Component;
match component {
Component::CurDir => {} Component::ParentDir => {
if !components.is_empty() {
components.pop();
}
}
c => components.push(c),
}
}
components.iter().collect()
}
}
impl FileSystem for InMemoryFileSystem {
fn read_to_string(&self, path: &Path) -> Result<String> {
let normalized = Self::normalize_path(path);
let files = self.files.read().unwrap();
files
.get(&normalized)
.cloned()
.ok_or_else(|| Error::new(ErrorKind::NotFound, format!("File not found: {:?}", path)))
}
fn write_file(&self, path: &Path, content: &str) -> Result<()> {
let normalized = Self::normalize_path(path);
if let Some(parent) = normalized.parent() {
self.create_dir_all(parent)?;
}
let mut files = self.files.write().unwrap();
files.insert(normalized, content.to_string());
Ok(())
}
fn create_new(&self, path: &Path, content: &str) -> Result<()> {
let normalized = Self::normalize_path(path);
{
let files = self.files.read().unwrap();
if files.contains_key(&normalized) {
return Err(Error::new(
ErrorKind::AlreadyExists,
format!("File already exists: {:?}", path),
));
}
}
if let Some(parent) = normalized.parent() {
self.create_dir_all(parent)?;
}
let mut files = self.files.write().unwrap();
files.insert(normalized, content.to_string());
Ok(())
}
fn delete_file(&self, path: &Path) -> Result<()> {
let normalized = Self::normalize_path(path);
{
let mut files = self.files.write().unwrap();
if files.remove(&normalized).is_some() {
return Ok(());
}
}
{
let mut binary_files = self.binary_files.write().unwrap();
if binary_files.remove(&normalized).is_some() {
return Ok(());
}
}
Err(Error::new(
ErrorKind::NotFound,
format!("File not found: {:?}", path),
))
}
fn list_md_files(&self, dir: &Path) -> Result<Vec<PathBuf>> {
let normalized = Self::normalize_path(dir);
let files = self.files.read().unwrap();
let mut result = Vec::new();
for path in files.keys() {
if let Some(parent) = path.parent()
&& parent == normalized
&& path.extension().is_some_and(|ext| ext == "md")
{
result.push(path.clone());
}
}
Ok(result)
}
fn exists(&self, path: &Path) -> bool {
let normalized = Self::normalize_path(path);
let files = self.files.read().unwrap();
let binary_files = self.binary_files.read().unwrap();
let dirs = self.directories.read().unwrap();
files.contains_key(&normalized)
|| binary_files.contains_key(&normalized)
|| dirs.contains(&normalized)
}
fn create_dir_all(&self, path: &Path) -> Result<()> {
let normalized = Self::normalize_path(path);
let mut dirs = self.directories.write().unwrap();
let mut current = normalized.as_path();
loop {
if !current.as_os_str().is_empty() {
dirs.insert(current.to_path_buf());
}
match current.parent() {
Some(parent) if !parent.as_os_str().is_empty() => {
current = parent;
}
_ => break,
}
}
Ok(())
}
fn is_dir(&self, path: &Path) -> bool {
let normalized = Self::normalize_path(path);
let dirs = self.directories.read().unwrap();
dirs.contains(&normalized)
}
fn move_file(&self, from: &Path, to: &Path) -> Result<()> {
let from_norm = Self::normalize_path(from);
let to_norm = Self::normalize_path(to);
if from_norm == to_norm {
return Ok(());
}
let is_dir = self.is_dir(&from_norm);
if is_dir {
let files_to_move: Vec<(PathBuf, String)>;
{
let files = self.files.read().unwrap();
files_to_move = files
.iter()
.filter(|(path, _)| path.starts_with(&from_norm))
.map(|(path, content)| (path.clone(), content.clone()))
.collect();
}
if files_to_move.is_empty() && !self.is_dir(&from_norm) {
return Err(Error::new(
ErrorKind::NotFound,
format!("Source directory not found or empty: {:?}", from),
));
}
{
let files = self.files.read().unwrap();
let dirs = self.directories.read().unwrap();
if files.contains_key(&to_norm) || dirs.contains(&to_norm) {
return Err(Error::new(
ErrorKind::AlreadyExists,
format!("Destination already exists: {:?}", to),
));
}
}
{
let mut files = self.files.write().unwrap();
for (old_path, content) in files_to_move {
files.remove(&old_path);
let relative = old_path.strip_prefix(&from_norm).unwrap();
let new_path = to_norm.join(relative);
files.insert(new_path, content);
}
}
{
let mut dirs = self.directories.write().unwrap();
let old_dirs: Vec<PathBuf> = dirs
.iter()
.filter(|d| d.starts_with(&from_norm) || **d == from_norm)
.cloned()
.collect();
for old_dir in old_dirs {
dirs.remove(&old_dir);
if old_dir == from_norm {
dirs.insert(to_norm.clone());
} else if let Ok(relative) = old_dir.strip_prefix(&from_norm) {
dirs.insert(to_norm.join(relative));
}
}
let mut current = to_norm.as_path();
loop {
match current.parent() {
Some(parent) if !parent.as_os_str().is_empty() => {
dirs.insert(parent.to_path_buf());
current = parent;
}
_ => break,
}
}
}
Ok(())
} else {
{
let files = self.files.read().unwrap();
if !files.contains_key(&from_norm) {
return Err(Error::new(
ErrorKind::NotFound,
format!("Source file not found: {:?}", from),
));
}
if files.contains_key(&to_norm) {
return Err(Error::new(
ErrorKind::AlreadyExists,
format!("Destination already exists: {:?}", to),
));
}
}
if let Some(parent) = to_norm.parent() {
self.create_dir_all(parent)?;
}
let mut files = self.files.write().unwrap();
let content = files.remove(&from_norm).ok_or_else(|| {
Error::new(
ErrorKind::NotFound,
format!("Source file not found: {:?}", from),
)
})?;
files.insert(to_norm, content);
Ok(())
}
}
fn read_binary(&self, path: &Path) -> Result<Vec<u8>> {
let normalized = Self::normalize_path(path);
{
let binary_files = self.binary_files.read().unwrap();
if let Some(data) = binary_files.get(&normalized) {
return Ok(data.clone());
}
}
let files = self.files.read().unwrap();
files
.get(&normalized)
.map(|s| s.as_bytes().to_vec())
.ok_or_else(|| Error::new(ErrorKind::NotFound, format!("File not found: {:?}", path)))
}
fn write_binary(&self, path: &Path, content: &[u8]) -> Result<()> {
let normalized = Self::normalize_path(path);
if let Some(parent) = normalized.parent() {
self.create_dir_all(parent)?;
}
let mut binary_files = self.binary_files.write().unwrap();
binary_files.insert(normalized, content.to_vec());
Ok(())
}
fn list_files(&self, dir: &Path) -> Result<Vec<PathBuf>> {
let normalized = Self::normalize_path(dir);
let files = self.files.read().unwrap();
let binary_files = self.binary_files.read().unwrap();
let dirs = self.directories.read().unwrap();
let mut result = Vec::new();
for path in files.keys() {
if let Some(parent) = path.parent()
&& parent == normalized
{
result.push(path.clone());
}
}
for path in binary_files.keys() {
if let Some(parent) = path.parent()
&& parent == normalized
{
result.push(path.clone());
}
}
for path in dirs.iter() {
if let Some(parent) = path.parent()
&& parent == normalized
&& path != &normalized
{
result.push(path.clone());
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_in_memory_fs_basic_operations() {
let fs = InMemoryFileSystem::new();
fs.write_file(Path::new("test.md"), "Hello, World!")
.unwrap();
assert_eq!(
fs.read_to_string(Path::new("test.md")).unwrap(),
"Hello, World!"
);
assert!(fs.exists(Path::new("test.md")));
assert!(!fs.exists(Path::new("nonexistent.md")));
fs.delete_file(Path::new("test.md")).unwrap();
assert!(!fs.exists(Path::new("test.md")));
}
#[test]
fn test_in_memory_fs_create_new() {
let fs = InMemoryFileSystem::new();
fs.create_new(Path::new("new.md"), "Content").unwrap();
assert_eq!(fs.read_to_string(Path::new("new.md")).unwrap(), "Content");
let result = fs.create_new(Path::new("new.md"), "Other content");
assert!(result.is_err());
}
#[test]
fn test_in_memory_fs_directories() {
let fs = InMemoryFileSystem::new();
fs.write_file(Path::new("a/b/c/file.md"), "Content")
.unwrap();
assert!(fs.is_dir(Path::new("a")));
assert!(fs.is_dir(Path::new("a/b")));
assert!(fs.is_dir(Path::new("a/b/c")));
assert!(fs.exists(Path::new("a/b/c/file.md")));
}
#[test]
fn test_in_memory_fs_list_md_files() {
let fs = InMemoryFileSystem::new();
fs.write_file(Path::new("dir/file1.md"), "Content 1")
.unwrap();
fs.write_file(Path::new("dir/file2.md"), "Content 2")
.unwrap();
fs.write_file(Path::new("dir/file.txt"), "Not markdown")
.unwrap();
fs.write_file(Path::new("dir/subdir/file3.md"), "Content 3")
.unwrap();
let md_files = fs.list_md_files(Path::new("dir")).unwrap();
assert_eq!(md_files.len(), 2);
assert!(md_files.contains(&PathBuf::from("dir/file1.md")));
assert!(md_files.contains(&PathBuf::from("dir/file2.md")));
}
#[test]
fn test_in_memory_fs_export_import() {
let fs = InMemoryFileSystem::new();
fs.write_file(Path::new("file1.md"), "Content 1").unwrap();
fs.write_file(Path::new("dir/file2.md"), "Content 2")
.unwrap();
let entries = fs.export_entries();
assert_eq!(entries.len(), 2);
let fs2 = InMemoryFileSystem::load_from_entries(entries);
assert_eq!(
fs2.read_to_string(Path::new("file1.md")).unwrap(),
"Content 1"
);
assert_eq!(
fs2.read_to_string(Path::new("dir/file2.md")).unwrap(),
"Content 2"
);
}
#[test]
fn test_in_memory_fs_path_normalization() {
let fs = InMemoryFileSystem::new();
fs.write_file(Path::new("dir/file.md"), "Content").unwrap();
assert!(fs.exists(Path::new("dir/file.md")));
assert!(fs.exists(Path::new("dir/./file.md")));
assert!(fs.exists(Path::new("dir/subdir/../file.md")));
}
}