use super::transport::{DirEntryInfo, StatInfo, Transport, TransportError};
use super::LockState;
use crate::lock::{LockError, ReadLock, WriteLock};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
enum LockHandle {
Read(ReadLock),
Write(WriteLock),
}
pub struct FileTransport {
path: PathBuf,
lock: Option<LockHandle>,
}
impl FileTransport {
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self {
path: path.into(),
lock: None,
}
}
pub fn path(&self) -> &Path {
&self.path
}
fn current_file(&mut self) -> Result<&mut std::fs::File, TransportError> {
match &mut self.lock {
Some(LockHandle::Read(rl)) => rl.file_mut().ok_or(TransportError::NotLocked),
Some(LockHandle::Write(wl)) => wl.file_mut().ok_or(TransportError::NotLocked),
None => Err(TransportError::NotLocked),
}
}
}
fn map_lock_err(err: LockError) -> TransportError {
match err {
LockError::Contention(p) => {
TransportError::LockContention(p.to_string_lossy().into_owned())
}
LockError::NotHeld(_) => TransportError::NotLocked,
LockError::Io(e) => TransportError::from(e),
}
}
impl Transport for FileTransport {
fn exists(&self) -> Result<bool, TransportError> {
Ok(self.path.exists())
}
fn lock_read(&mut self) -> Result<(), TransportError> {
if self.lock.is_some() {
return Err(TransportError::AlreadyLocked);
}
let lock = ReadLock::new(&self.path).map_err(map_lock_err)?;
self.lock = Some(LockHandle::Read(lock));
Ok(())
}
fn lock_write(&mut self) -> Result<(), TransportError> {
if self.lock.is_some() {
return Err(TransportError::AlreadyLocked);
}
let lock = WriteLock::new(&self.path).map_err(map_lock_err)?;
self.lock = Some(LockHandle::Write(lock));
Ok(())
}
fn unlock(&mut self) -> Result<(), TransportError> {
match self.lock.take() {
None => Err(TransportError::NotLocked),
Some(LockHandle::Read(mut rl)) => rl.unlock().map_err(map_lock_err),
Some(LockHandle::Write(mut wl)) => wl.unlock().map_err(map_lock_err),
}
}
fn lock_state(&self) -> Option<LockState> {
match &self.lock {
None => None,
Some(LockHandle::Read(_)) => Some(LockState::Read),
Some(LockHandle::Write(_)) => Some(LockState::Write),
}
}
fn read_all(&mut self) -> Result<Vec<u8>, TransportError> {
let file = self.current_file()?;
file.seek(SeekFrom::Start(0))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
Ok(buf)
}
fn write_all(&mut self, bytes: &[u8]) -> Result<(), TransportError> {
match &self.lock {
Some(LockHandle::Write(_)) => {}
Some(LockHandle::Read(_)) => {
return Err(TransportError::Other(
"write_all requires a write lock".to_string(),
))
}
None => return Err(TransportError::NotLocked),
}
let file = self.current_file()?;
file.seek(SeekFrom::Start(0))?;
file.write_all(bytes)?;
let len = bytes.len() as u64;
file.set_len(len)?;
file.flush()?;
Ok(())
}
fn fdatasync(&mut self) -> Result<(), TransportError> {
let file = self.current_file()?;
file.sync_data().map_err(TransportError::from)?;
Ok(())
}
fn lstat(&self, abspath: &[u8]) -> Result<StatInfo, TransportError> {
let path = bytes_to_path(abspath)?;
let metadata = std::fs::symlink_metadata(&path).map_err(TransportError::from)?;
stat_info_from_metadata(&metadata)
}
fn read_link(&self, abspath: &[u8]) -> Result<Vec<u8>, TransportError> {
let path = bytes_to_path(abspath)?;
let target = std::fs::read_link(&path).map_err(TransportError::from)?;
Ok(path_to_bytes(&target))
}
fn is_tree_reference_dir(&self, abspath: &[u8]) -> Result<bool, TransportError> {
let path = bytes_to_path(abspath)?;
let bzr_dir = path.join(".bzr");
Ok(bzr_dir.is_dir())
}
fn list_dir(&self, abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError> {
let path = bytes_to_path(abspath)?;
let entries = std::fs::read_dir(&path).map_err(TransportError::from)?;
let mut out = Vec::new();
for entry in entries {
let entry = entry.map_err(TransportError::from)?;
let name = entry.file_name();
let basename = path_to_bytes(Path::new(&name));
let metadata = entry.metadata().map_err(TransportError::from)?;
let stat = stat_info_from_metadata(&metadata)?;
let kind = osutils_kind_from_stat(&stat);
let abs = entry.path();
out.push(DirEntryInfo {
basename,
kind,
stat,
abspath: path_to_bytes(&abs),
});
}
Ok(out)
}
}
#[cfg(unix)]
fn bytes_to_path(b: &[u8]) -> Result<PathBuf, TransportError> {
use std::os::unix::ffi::OsStrExt;
Ok(PathBuf::from(std::ffi::OsStr::from_bytes(b)))
}
#[cfg(not(unix))]
fn bytes_to_path(b: &[u8]) -> Result<PathBuf, TransportError> {
String::from_utf8(b.to_vec())
.map(PathBuf::from)
.map_err(|e| TransportError::Other(e.to_string()))
}
#[cfg(unix)]
fn path_to_bytes(p: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt;
p.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
fn path_to_bytes(p: &Path) -> Vec<u8> {
p.to_string_lossy().into_owned().into_bytes()
}
fn stat_info_from_metadata(m: &std::fs::Metadata) -> Result<StatInfo, TransportError> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
Ok(StatInfo {
mode: m.mode(),
size: m.size(),
mtime: m.mtime(),
ctime: m.ctime(),
dev: m.dev(),
ino: m.ino(),
})
}
#[cfg(not(unix))]
{
let _ = m;
Err(TransportError::Other(
"lstat unsupported on this platform".to_string(),
))
}
}
fn osutils_kind_from_stat(stat: &StatInfo) -> Option<crate::osutils::Kind> {
if stat.is_file() {
Some(crate::osutils::Kind::File)
} else if stat.is_dir() {
Some(crate::osutils::Kind::Directory)
} else if stat.is_symlink() {
Some(crate::osutils::Kind::Symlink)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use tempfile::NamedTempFile;
#[test]
fn read_write_roundtrip() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "hello").unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_read().unwrap();
let data = t.read_all().unwrap();
assert_eq!(data, b"hello");
t.unlock().unwrap();
t.lock_write().unwrap();
t.write_all(b"world!").unwrap();
t.unlock().unwrap();
let mut t2 = FileTransport::new(tmp.path());
t2.lock_read().unwrap();
let data = t2.read_all().unwrap();
assert_eq!(data, b"world!");
t2.unlock().unwrap();
}
#[test]
fn write_lock_creates_missing_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing");
let mut t = FileTransport::new(&path);
t.lock_write().unwrap();
t.write_all(b"created").unwrap();
t.unlock().unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"created");
}
#[test]
fn read_all_requires_lock() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "x").unwrap();
let mut t = FileTransport::new(tmp.path());
assert!(matches!(t.read_all(), Err(TransportError::NotLocked)));
}
#[test]
fn dirstate_initialize_then_load_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("dirstate");
let mut transport = FileTransport::new(&path);
let provider: Box<dyn crate::dirstate::SHA1Provider + Send + Sync> =
Box::new(crate::dirstate::DefaultSHA1Provider::new());
let mut state =
crate::dirstate::DirState::initialize(&mut transport, path.clone(), provider).unwrap();
assert_eq!(state.parents, Vec::<Vec<u8>>::new());
assert_eq!(state.dirblocks.len(), 2);
assert_eq!(state.dirblocks[0].entries.len(), 1);
assert_eq!(
state.dirblocks[0].entries[0].key.file_id,
crate::inventory::ROOT_ID
);
transport.unlock().unwrap();
let mut t2 = FileTransport::new(&path);
t2.lock_read().unwrap();
let data = t2.read_all().unwrap();
assert!(data.starts_with(b"#bazaar dirstate flat format 3\n"));
t2.unlock().unwrap();
}
}