use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::fs;
use std::path::{Component, Path, PathBuf};
use crate::error::{Error, Result};
use super::{DirEntry, FileType, Vfs};
#[derive(Debug, Clone)]
pub struct StdFs {
root: PathBuf,
}
impl StdFs {
pub fn new(root: impl Into<PathBuf>) -> Self {
StdFs { root: root.into() }
}
fn resolve(&self, path: &str) -> Result<PathBuf> {
let p = Path::new(path);
for comp in p.components() {
match comp {
Component::Normal(_) | Component::CurDir => {}
Component::ParentDir => {
return Err(Error::Io(alloc::format!(
"vfs: path escapes the root via `..`: {path:?}"
)));
}
Component::RootDir | Component::Prefix(_) => {
return Err(Error::Io(alloc::format!(
"vfs: absolute path rejected: {path:?}"
)));
}
}
}
Ok(self.root.join(p))
}
}
impl Vfs for StdFs {
fn read(&self, path: &str) -> Result<Vec<u8>> {
Ok(fs::read(self.resolve(path)?)?)
}
fn write(&self, path: &str, data: &[u8]) -> Result<()> {
let full = self.resolve(path)?;
if let Some(parent) = full.parent() {
fs::create_dir_all(parent)?;
}
let tmp = tmp_sibling(&full);
fs::write(&tmp, data)?;
match fs::rename(&tmp, &full) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e.into())
}
}
}
fn exists(&self, path: &str) -> bool {
match self.resolve(path) {
Ok(full) => full.exists(),
Err(_) => false,
}
}
fn metadata(&self, path: &str) -> Result<FileType> {
let md = fs::symlink_metadata(self.resolve(path)?)?;
let ft = md.file_type();
Ok(if ft.is_dir() {
FileType::Dir
} else if ft.is_symlink() {
FileType::Symlink
} else {
FileType::File
})
}
fn create_dir_all(&self, path: &str) -> Result<()> {
Ok(fs::create_dir_all(self.resolve(path)?)?)
}
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
let mut out = Vec::new();
for entry in fs::read_dir(self.resolve(path)?)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
let ft = entry.file_type()?;
let file_type = if ft.is_dir() {
FileType::Dir
} else if ft.is_symlink() {
FileType::Symlink
} else {
FileType::File
};
out.push(DirEntry { name, file_type });
}
Ok(out)
}
fn remove_file(&self, path: &str) -> Result<()> {
Ok(fs::remove_file(self.resolve(path)?)?)
}
fn rename(&self, from: &str, to: &str) -> Result<()> {
Ok(fs::rename(self.resolve(from)?, self.resolve(to)?)?)
}
fn create_new(&self, path: &str, data: &[u8]) -> Result<()> {
use std::io::Write;
let full = self.resolve(path)?;
if let Some(parent) = full.parent() {
fs::create_dir_all(parent)?;
}
let mut f = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&full)?;
f.write_all(data)?;
Ok(())
}
fn open_relative(&self, path: &str) -> Result<Self> {
let p = Path::new(path);
let root = if p.is_absolute() {
p.to_path_buf()
} else {
self.root.join(p)
};
Ok(StdFs::new(root))
}
}
fn tmp_sibling(full: &Path) -> PathBuf {
let leaf = full
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "tmp".to_string());
let suffix = leaf.as_ptr() as usize;
let mut s = String::with_capacity(leaf.len() + 24);
s.push('.');
s.push_str(&leaf);
s.push('.');
s.push_str(&suffix.to_string());
s.push_str(".tmp");
full.with_file_name(s)
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_root(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(alloc::format!("puregit-vfs-{}-{tag}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn rejects_parent_dir_and_absolute() {
let root = temp_root("reject");
let fs = StdFs::new(&root);
assert!(fs.read("../secret").is_err());
assert!(fs.write("../secret", b"x").is_err());
assert!(fs.create_dir_all("a/../..").is_err());
assert!(!fs.exists("../secret"));
assert!(fs.read("/etc/passwd").is_err());
assert!(fs.write("/tmp/escape", b"x").is_err());
assert!(!fs.exists("/etc/passwd"));
let _ = fs::remove_dir_all(&root);
}
#[test]
fn create_new_is_exclusive() {
let root = temp_root("createnew");
let fs = StdFs::new(&root);
fs.create_new("refs/heads/x.lock", b"first\n").unwrap();
assert!(fs.create_new("refs/heads/x.lock", b"second\n").is_err());
assert_eq!(fs.read("refs/heads/x.lock").unwrap(), b"first\n");
let _ = fs::remove_dir_all(&root);
}
#[test]
fn allows_legit_relative_paths() {
let root = temp_root("legit");
let fs = StdFs::new(&root);
fs.write("config", b"[core]\n").unwrap();
assert_eq!(fs.read("config").unwrap(), b"[core]\n");
fs.write("refs/heads/main", b"deadbeef\n").unwrap();
assert!(fs.exists("refs/heads/main"));
fs.create_dir_all("objects/pack").unwrap();
assert!(fs.exists("objects/pack"));
fs.write("./HEAD", b"ref: refs/heads/main\n").unwrap();
assert!(fs.exists("HEAD"));
let _ = fs::remove_dir_all(&root);
}
}