pub mod backend;
pub mod memory;
pub mod native;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
pub use backend::{AccessMode, FileBackend, LockMode, OpenFile, OpenMode};
pub const MIN_FILE_NUMBER: i16 = 1;
pub const MAX_FILE_NUMBER: i16 = 511;
pub const MIN_RECORD_NUMBER: i32 = 0;
pub const MAX_RECORD_NUMBER: i32 = 32767;
static BACKEND: OnceLock<Mutex<Box<dyn FileBackend>>> = OnceLock::new();
static ROOT: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
thread_local! {
static OPEN_FILES: RefCell<HashMap<i16, OpenFile>> = RefCell::new(HashMap::new());
}
fn backend() -> &'static Mutex<Box<dyn FileBackend>> {
BACKEND.get_or_init(|| Mutex::new(default_backend()))
}
fn root() -> &'static Mutex<Option<PathBuf>> {
ROOT.get_or_init(|| Mutex::new(None))
}
fn default_backend() -> Box<dyn FileBackend> {
if cfg!(target_arch = "wasm32") {
Box::new(memory::MemoryBackend::new())
} else {
Box::new(native::NativeBackend::new())
}
}
pub fn set_backend(new_backend: Box<dyn FileBackend>) {
let mut backend_guard = backend().lock().unwrap_or_else(|e| e.into_inner());
*backend_guard = new_backend;
}
pub fn reset_backend() {
set_backend(default_backend());
}
pub fn set_root(path: impl Into<PathBuf>) {
let mut root_guard = root().lock().unwrap_or_else(|e| e.into_inner());
*root_guard = Some(path.into());
}
pub fn get_root() -> PathBuf {
let root_guard = root().lock().unwrap_or_else(|e| e.into_inner());
root_guard
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")))
}
pub fn resolve_path(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
let root = get_root();
root.join(path)
}
}
pub fn open_file(
path: &Path,
mode: OpenMode,
access: AccessMode,
lock: LockMode,
record_length: i32,
file_number: i16,
) -> io::Result<()> {
OPEN_FILES.with(|files| {
if files.borrow().contains_key(&file_number) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("File number {} is already in use", file_number),
));
}
Ok(())
})?;
let resolved_path = resolve_path(path);
let mut file = backend().lock().unwrap_or_else(|e| e.into_inner()).open(
&resolved_path,
mode,
access,
lock,
record_length,
)?;
file.number = file_number;
OPEN_FILES.with(|files| {
files.borrow_mut().insert(file_number, file);
});
Ok(())
}
pub fn close_file(file_number: i16) -> io::Result<()> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
if let Some(file) = files.remove(&file_number) {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.close(&file)?;
}
Ok(())
})
}
pub fn close_all_files() -> io::Result<()> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
let numbers: Vec<i16> = files.keys().copied().collect();
for number in numbers {
if let Some(file) = files.remove(&number) {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.close(&file)?;
}
}
Ok(())
})
}
pub fn read_file(file_number: i16, buf: &mut [u8]) -> io::Result<usize> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
let file = files
.get_mut(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.read(file, buf)
})
}
pub fn write_file(file_number: i16, buf: &[u8]) -> io::Result<usize> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
let file = files
.get_mut(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.write(file, buf)
})
}
pub fn seek_file(file_number: i16, position: i64) -> io::Result<i64> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
let file = files
.get_mut(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
let pos = position - 1;
if pos < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid file position",
));
}
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.seek(file, pos)
})
}
pub fn position_file(file_number: i16) -> io::Result<i64> {
OPEN_FILES.with(|files| {
let files = files.borrow();
let file = files
.get(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
Ok(file.position + 1)
})
}
pub fn lof_file(file_number: i16) -> io::Result<i64> {
OPEN_FILES.with(|files| {
let files = files.borrow();
let file = files
.get(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.lof(file)
})
}
pub fn file_len(path: &Path) -> io::Result<i64> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.file_len(&resolved_path)
}
pub fn file_exists(path: &Path) -> bool {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.file_exists(&resolved_path)
}
pub fn is_file_open(file_number: i16) -> bool {
OPEN_FILES.with(|files| files.borrow().contains_key(&file_number))
}
pub fn free_file(range: i16) -> i16 {
let (min, max) = if range == 1 { (256, 511) } else { (1, 255) };
OPEN_FILES.with(|files| {
let files = files.borrow();
for num in min..=max {
if !files.contains_key(&num) {
return num;
}
}
0
})
}
pub fn get_file(file_number: i16) -> Option<OpenFile> {
OPEN_FILES.with(|files| files.borrow().get(&file_number).cloned())
}
pub fn get_open_files() -> Vec<(i16, OpenFile)> {
OPEN_FILES.with(|files| {
files
.borrow()
.iter()
.map(|(&num, file)| (num, file.clone()))
.collect()
})
}
pub fn read_file_to_vec(file_number: i16) -> io::Result<Vec<u8>> {
OPEN_FILES.with(|files| {
let files = files.borrow();
let file = files
.get(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
let path = Path::new(&file.path);
let resolved = resolve_path(path);
let mut backend = backend().lock().unwrap_or_else(|e| e.into_inner());
let len = backend.file_len(&resolved)?;
let mut buf = vec![0u8; len as usize];
let mut file_clone = file.clone();
file_clone.position = 0;
let bytes_read = backend.read(&mut file_clone, &mut buf)?;
buf.truncate(bytes_read);
Ok(buf)
})
}
pub fn list_memory_files() -> io::Result<Vec<memory::VirtualFile>> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_any()
.downcast_ref::<memory::MemoryBackend>()
.map(|memory| memory.files().values().cloned().collect())
.ok_or_else(|| io::Error::other("Backend is not a memory backend"))
}
pub fn write_memory_file(path: &str, content: &[u8]) -> io::Result<()> {
let resolved_path = resolve_path(Path::new(path));
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_any_mut()
.downcast_mut::<memory::MemoryBackend>()
.map(|memory| memory.insert_file(&resolved_path.to_string_lossy(), content.to_vec()))
.ok_or_else(|| io::Error::other("Backend is not a memory backend"))
}
pub fn with_file_mut<T>(file_number: i16, f: impl FnOnce(&mut OpenFile) -> T) -> io::Result<T> {
OPEN_FILES.with(|files| {
let mut files = files.borrow_mut();
let file = files
.get_mut(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
Ok(f(file))
})
}
pub fn reset() {
let _ = close_all_files();
reset_backend();
}
pub fn reset_with_root(path: impl Into<PathBuf>) {
let _ = close_all_files();
set_root(path);
}
pub fn copy_file(src: &Path, dst: &Path) -> io::Result<()> {
let resolved_src = resolve_path(src);
let resolved_dst = resolve_path(dst);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.copy_file(&resolved_src, &resolved_dst)
}
pub fn rename_file(old_path: &Path, new_path: &Path) -> io::Result<()> {
let resolved_old = resolve_path(old_path);
let resolved_new = resolve_path(new_path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.rename_file(&resolved_old, &resolved_new)
}
pub fn remove_file(path: &Path) -> io::Result<()> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove_file(&resolved_path)
}
pub fn create_dir(path: &Path) -> io::Result<()> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.create_dir(&resolved_path)
}
pub fn remove_dir(path: &Path) -> io::Result<()> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove_dir(&resolved_path)
}
pub fn get_attrs(path: &Path) -> io::Result<i16> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get_attrs(&resolved_path)
}
pub fn set_attrs(path: &Path, attrs: i16) -> io::Result<()> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.set_attrs(&resolved_path, attrs)
}
pub fn file_datetime(path: &Path) -> io::Result<std::time::SystemTime> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.file_datetime(&resolved_path)
}
pub fn current_dir() -> io::Result<PathBuf> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.current_dir()
}
pub fn set_current_dir(path: &Path) -> io::Result<()> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.set_current_dir(path)
}
pub fn drives() -> Vec<char> {
backend().lock().unwrap_or_else(|e| e.into_inner()).drives()
}
pub fn current_dir_for_drive(drive: char) -> io::Result<PathBuf> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.current_dir_for_drive(drive)
}
pub fn set_current_drive(drive: char) -> io::Result<()> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.set_current_drive(drive)
}
pub fn lock_file(file_number: i16, record_range: Option<(i32, i32)>) -> io::Result<()> {
OPEN_FILES.with(|files| {
let files = files.borrow();
let file = files
.get(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
let path = Path::new(&file.path);
let resolved = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.lock_file(&resolved, record_range)
})
}
pub fn unlock_file(file_number: i16, record_range: Option<(i32, i32)>) -> io::Result<()> {
OPEN_FILES.with(|files| {
let files = files.borrow();
let file = files
.get(&file_number)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
let path = Path::new(&file.path);
let resolved = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.unlock_file(&resolved, record_range)
})
}
pub fn get_print_column(file_number: i16) -> usize {
OPEN_FILES.with(|files| {
files
.borrow()
.get(&file_number)
.map(|f| f.print_column)
.unwrap_or(1)
})
}
pub fn set_print_column(file_number: i16, column: usize) {
OPEN_FILES.with(|files| {
if let Some(file) = files.borrow_mut().get_mut(&file_number) {
file.print_column = column;
}
});
}
pub fn advance_print_column(file_number: i16, count: usize) {
OPEN_FILES.with(|files| {
if let Some(file) = files.borrow_mut().get_mut(&file_number) {
file.print_column += count;
}
});
}
pub fn reset_print_column(file_number: i16) {
set_print_column(file_number, 1);
}
pub const DEFAULT_ZONE_WIDTH: usize = 14;
pub fn zone_width() -> usize {
DEFAULT_ZONE_WIDTH
}
pub fn file_dir(path: &Path, pattern: &str, attributes: i16) -> io::Result<Vec<String>> {
let resolved_path = resolve_path(path);
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.file_dir(&resolved_path, pattern, attributes)
}
pub fn matches_wildcard(file_name: &str, pattern: &str) -> bool {
if !pattern.contains('*') && !pattern.contains('?') {
return file_name.to_lowercase().contains(&pattern.to_lowercase());
}
if pattern == "*" {
return true;
}
file_name.to_lowercase().contains(&pattern.to_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn free_file_returns_lowest_available() {
let _guard = crate::state::test_support::lock_test();
let _ = close_all_files();
assert_eq!(free_file(0), 1);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
1,
)
.unwrap();
assert_eq!(free_file(0), 2);
let path = dir.path().join("test2.txt");
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
2,
)
.unwrap();
assert_eq!(free_file(0), 3);
close_file(1).unwrap();
assert_eq!(free_file(0), 1);
let _ = close_all_files();
}
#[test]
fn free_file_high_range() {
let _guard = crate::state::test_support::lock_test();
let _ = close_all_files();
assert_eq!(free_file(1), 256);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
256,
)
.unwrap();
assert_eq!(free_file(1), 257);
let _ = close_all_files();
}
#[test]
fn open_and_close_file() {
let _guard = crate::state::test_support::lock_test();
let _ = close_all_files();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
1,
)
.unwrap();
assert!(is_file_open(1));
close_file(1).unwrap();
assert!(!is_file_open(1));
let _ = close_all_files();
}
#[test]
fn write_and_read_file() {
let _guard = crate::state::test_support::lock_test();
let _ = close_all_files();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
1,
)
.unwrap();
write_file(1, b"Hello, World!").unwrap();
open_file(
&path,
OpenMode::Input,
AccessMode::Read,
LockMode::Shared,
0,
2,
)
.unwrap();
let mut buf = [0u8; 13];
let bytes_read = read_file(2, &mut buf).unwrap();
assert_eq!(bytes_read, 13);
assert_eq!(&buf, b"Hello, World!");
let _ = close_all_files();
}
#[test]
fn file_exists_check() {
let _guard = crate::state::test_support::lock_test();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
assert!(!file_exists(&path));
open_file(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
1,
)
.unwrap();
close_file(1).unwrap();
assert!(file_exists(&path));
let _ = close_all_files();
}
}