use super::transport::{DirEntryInfo, StatInfo, Transport, TransportError};
use super::LockState;
use crate::lock::{LockError, ReadLock, TemporaryWriteLockResult, 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 len(&mut self) -> Result<u64, TransportError> {
let file = self.current_file()?;
Ok(file.metadata().map_err(TransportError::from)?.len())
}
fn read_at(&mut self, offset: u64, len: usize) -> Result<Vec<u8>, TransportError> {
let file = self.current_file()?;
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; len];
let mut filled = 0;
while filled < len {
let n = file.read(&mut buf[filled..])?;
if n == 0 {
buf.truncate(filled);
return Ok(buf);
}
filled += n;
}
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 upgrade_to_write_lock(&mut self) -> Result<bool, TransportError> {
match self.lock.take() {
None => Err(TransportError::NotLocked),
Some(LockHandle::Write(wl)) => {
self.lock = Some(LockHandle::Write(wl));
Err(TransportError::AlreadyLocked)
}
Some(LockHandle::Read(rl)) => match rl.temporary_write_lock().map_err(map_lock_err)? {
TemporaryWriteLockResult::Succeeded(wl) => {
self.lock = Some(LockHandle::Write(wl));
Ok(true)
}
TemporaryWriteLockResult::Failed(rl) => {
self.lock = Some(LockHandle::Read(rl));
Ok(false)
}
},
}
}
fn downgrade_to_read_lock(&mut self) -> Result<(), TransportError> {
match self.lock.take() {
None => Err(TransportError::NotLocked),
Some(LockHandle::Read(rl)) => {
self.lock = Some(LockHandle::Read(rl));
Ok(())
}
Some(LockHandle::Write(wl)) => {
let rl = wl.restore_read_lock().map_err(map_lock_err)?;
self.lock = Some(LockHandle::Read(rl));
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> {
super::transport::lstat_path(abspath)
}
fn read_link(&self, abspath: &[u8]) -> Result<Vec<u8>, TransportError> {
super::transport::read_link_path(abspath)
}
fn is_tree_reference_dir(&self, abspath: &[u8]) -> Result<bool, TransportError> {
super::transport::is_tree_reference_dir_path(abspath)
}
fn list_dir(&self, abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError> {
super::transport::list_dir_path(abspath)
}
}
#[cfg(test)]
mod tests {
use super::*;
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 read_at_returns_requested_window() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "0123456789abcdef").unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_read().unwrap();
assert_eq!(t.read_at(0, 4).unwrap(), b"0123");
assert_eq!(t.read_at(4, 4).unwrap(), b"4567");
assert_eq!(t.read_at(10, 6).unwrap(), b"abcdef");
t.unlock().unwrap();
}
#[test]
fn read_at_short_read_at_eof() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "0123").unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_read().unwrap();
assert_eq!(t.read_at(2, 100).unwrap(), b"23");
assert_eq!(t.read_at(100, 4).unwrap(), b"");
t.unlock().unwrap();
}
#[test]
fn read_at_requires_lock() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "x").unwrap();
let mut t = FileTransport::new(tmp.path());
assert!(matches!(t.read_at(0, 1), Err(TransportError::NotLocked)));
}
#[test]
fn len_reports_file_size() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "0123456789").unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_read().unwrap();
assert_eq!(t.len().unwrap(), 10);
t.unlock().unwrap();
}
#[test]
fn upgrade_to_write_then_downgrade_roundtrip() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "before").unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_read().unwrap();
assert_eq!(t.lock_state(), Some(LockState::Read));
assert!(t.upgrade_to_write_lock().unwrap());
assert_eq!(t.lock_state(), Some(LockState::Write));
t.write_all(b"after").unwrap();
t.downgrade_to_read_lock().unwrap();
assert_eq!(t.lock_state(), Some(LockState::Read));
assert_eq!(t.read_all().unwrap(), b"after");
t.unlock().unwrap();
}
#[test]
fn upgrade_to_write_lock_when_not_locked_errors() {
let tmp = NamedTempFile::new().unwrap();
let mut t = FileTransport::new(tmp.path());
assert!(matches!(
t.upgrade_to_write_lock(),
Err(TransportError::NotLocked)
));
}
#[test]
fn upgrade_to_write_lock_when_already_write_errors() {
let tmp = NamedTempFile::new().unwrap();
let mut t = FileTransport::new(tmp.path());
t.lock_write().unwrap();
assert!(matches!(
t.upgrade_to_write_lock(),
Err(TransportError::AlreadyLocked)
));
t.unlock().unwrap();
}
#[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 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();
}
}