use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use super::backend::{AccessMode, FileBackend, LockMode, OpenFile, OpenMode};
type LockRange = (i32, i32);
pub struct NativeBackend {
files: HashMap<String, File>,
current_dir: PathBuf,
current_drive: char,
locks: HashMap<String, Vec<Option<LockRange>>>,
}
impl NativeBackend {
pub fn new() -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
Self {
files: HashMap::new(),
current_dir: cwd,
current_drive: 'C',
locks: HashMap::new(),
}
}
}
impl Default for NativeBackend {
fn default() -> Self {
Self::new()
}
}
impl FileBackend for NativeBackend {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn open(
&mut self,
path: &Path,
mode: OpenMode,
access: AccessMode,
lock: LockMode,
record_length: i32,
) -> io::Result<OpenFile> {
if mode == OpenMode::Output || mode == OpenMode::Append {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
}
let file = match mode {
OpenMode::Input => OpenOptions::new()
.read(true)
.write(false)
.create(false)
.truncate(false)
.open(path)?,
OpenMode::Output => OpenOptions::new()
.read(false)
.write(true)
.create(true)
.truncate(true)
.open(path)?,
OpenMode::Append => OpenOptions::new()
.read(false)
.create(true)
.append(true)
.open(path)?,
OpenMode::Random => OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?,
OpenMode::Binary => OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?,
};
let initial_position = if mode == OpenMode::Append {
file.metadata()?.len() as i64
} else {
0
};
let path_str = path.to_string_lossy().to_string();
self.files.insert(path_str.clone(), file);
Ok(OpenFile {
number: 0, path: path_str,
mode,
access,
lock,
record_length,
position: initial_position,
width: 0, print_column: 1, })
}
fn close(&mut self, file: &OpenFile) -> io::Result<()> {
self.files.remove(&file.path);
Ok(())
}
fn read(&mut self, file: &mut OpenFile, buf: &mut [u8]) -> io::Result<usize> {
let f = self
.files
.get_mut(&file.path)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
f.seek(SeekFrom::Start(file.position as u64))?;
let bytes_read = f.read(buf)?;
file.position += bytes_read as i64;
Ok(bytes_read)
}
fn write(&mut self, file: &mut OpenFile, buf: &[u8]) -> io::Result<usize> {
let f = self
.files
.get_mut(&file.path)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
f.seek(SeekFrom::Start(file.position as u64))?;
let bytes_written = f.write(buf)?;
file.position += bytes_written as i64;
Ok(bytes_written)
}
fn seek(&mut self, file: &mut OpenFile, position: i64) -> io::Result<i64> {
let _ = self
.files
.get(&file.path)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "File not open"))?;
file.position = position;
Ok(file.position)
}
fn file_len(&mut self, path: &Path) -> io::Result<i64> {
let metadata = std::fs::metadata(path)?;
Ok(metadata.len() as i64)
}
fn file_exists(&mut self, path: &Path) -> bool {
path.exists()
}
fn file_dir(&mut self, path: &Path, pattern: &str, attributes: i16) -> io::Result<Vec<String>> {
let mut results = Vec::new();
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let file_name = entry
.path()
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if file_name == "." || file_name == ".." {
continue;
}
let matches_pattern = crate::state::file::matches_wildcard(&file_name, pattern);
let metadata = std::fs::metadata(entry.path());
let mut file_attrs: i16 = 0;
if let Ok(meta) = metadata {
if meta.is_dir() {
file_attrs |= 16; }
if meta.is_file() {
file_attrs |= 32; }
}
let attr_match = if attributes == 0 || attributes == file_attrs {
true
} else {
(attributes & file_attrs) == attributes
};
if matches_pattern && attr_match {
results.push(file_name);
}
}
}
Ok(results)
}
fn position(&self, file: &OpenFile) -> i64 {
file.position + 1
}
fn lof(&mut self, file: &OpenFile) -> io::Result<i64> {
let path = Path::new(&file.path);
let metadata = std::fs::metadata(path)?;
Ok(metadata.len() as i64)
}
fn copy_file(&mut self, src: &Path, dst: &Path) -> io::Result<()> {
std::fs::copy(src, dst)?;
Ok(())
}
fn rename_file(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> {
std::fs::rename(old_path, new_path)
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
std::fs::remove_file(path)
}
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
std::fs::create_dir(path)
}
fn remove_dir(&mut self, path: &Path) -> io::Result<()> {
std::fs::remove_dir(path)
}
fn get_attrs(&mut self, path: &Path) -> io::Result<i16> {
let metadata = std::fs::metadata(path)?;
let perms = metadata.permissions();
let mut attrs: i16 = 0;
if perms.readonly() {
attrs |= 1; }
if metadata.is_dir() {
attrs |= 16; }
if metadata.is_file() {
attrs |= 32; }
Ok(attrs)
}
fn set_attrs(&mut self, path: &Path, attrs: i16) -> io::Result<()> {
let metadata = std::fs::metadata(path)?;
let mut perms = metadata.permissions();
perms.set_readonly((attrs & 1) != 0);
std::fs::set_permissions(path, perms)
}
fn file_datetime(&mut self, path: &Path) -> io::Result<std::time::SystemTime> {
let metadata = std::fs::metadata(path)?;
metadata.modified()
}
fn current_dir(&mut self) -> io::Result<PathBuf> {
Ok(self.current_dir.clone())
}
fn set_current_dir(&mut self, path: &Path) -> io::Result<()> {
let target = if path.is_relative() {
self.current_dir.join(path)
} else {
path.to_path_buf()
};
if !target.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
));
}
self.current_dir = target;
Ok(())
}
fn drives(&self) -> Vec<char> {
vec!['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
}
fn current_dir_for_drive(&mut self, drive: char) -> io::Result<PathBuf> {
if drive == self.current_drive {
Ok(self.current_dir.clone())
} else {
Ok(PathBuf::from(format!("{drive}:\\")))
}
}
fn set_current_drive(&mut self, drive: char) -> io::Result<()> {
self.current_drive = drive.to_ascii_uppercase();
Ok(())
}
fn lock_file(&mut self, path: &Path, record_range: Option<(i32, i32)>) -> io::Result<()> {
let path_str = path.to_string_lossy().to_string();
let existing = self.locks.entry(path_str).or_default();
for existing_lock in existing.iter() {
match (existing_lock, &record_range) {
(None, _) | (_, None) => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"File already locked",
));
}
(Some((e_start, e_end)), Some((n_start, n_end))) => {
if n_start <= e_end && n_end >= e_start {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"File already locked",
));
}
}
}
}
existing.push(record_range);
Ok(())
}
fn unlock_file(&mut self, path: &Path, record_range: Option<(i32, i32)>) -> io::Result<()> {
let path_str = path.to_string_lossy().to_string();
let existing = self
.locks
.get_mut(&path_str)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not locked"))?;
let pos = existing
.iter()
.position(|lock| match (lock, &record_range) {
(None, None) => true,
(Some((a, b)), Some((c, d))) => a == c && b == d,
_ => false,
});
match pos {
Some(i) => {
existing.remove(i);
if existing.is_empty() {
self.locks.remove(&path_str);
}
Ok(())
}
None => Err(io::Error::new(io::ErrorKind::NotFound, "File not locked")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_and_close_file() {
let mut backend = NativeBackend::new();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
let mut file = backend
.open(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
)
.unwrap();
file.number = 1;
backend.close(&file).unwrap();
}
#[test]
fn write_and_read_file() {
let mut backend = NativeBackend::new();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
let mut file = backend
.open(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
)
.unwrap();
file.number = 1;
backend.write(&mut file, b"Hello, World!").unwrap();
backend.close(&file).unwrap();
let mut file = backend
.open(
&path,
OpenMode::Input,
AccessMode::Read,
LockMode::Shared,
0,
)
.unwrap();
file.number = 1;
let mut buf = [0u8; 13];
let bytes_read = backend.read(&mut file, &mut buf).unwrap();
assert_eq!(bytes_read, 13);
assert_eq!(&buf, b"Hello, World!");
backend.close(&file).unwrap();
}
#[test]
fn file_exists_check() {
let mut backend = NativeBackend::new();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
assert!(!backend.file_exists(&path));
let file = backend
.open(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
)
.unwrap();
backend.close(&file).unwrap();
assert!(backend.file_exists(&path));
}
#[test]
fn file_len_returns_correct_size() {
let mut backend = NativeBackend::new();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
let mut file = backend
.open(
&path,
OpenMode::Output,
AccessMode::Write,
LockMode::Shared,
0,
)
.unwrap();
file.number = 1;
backend.write(&mut file, b"12345").unwrap();
let len = backend.file_len(&path).unwrap();
assert_eq!(len, 5);
}
}