use std::io::{self, ErrorKind, Read as _};
use std::path::Path;
use rustix::fs::OFlags;
use crate::project_lock::{open_state_file, replace_state_file};
pub const CURRENT: u32 = 1;
pub const VERSION_FILE: &str = "version";
const READ_LIMIT: u64 = 32;
pub fn read(project_dir: &Path) -> io::Result<Option<u32>> {
let flags = OFlags::RDONLY | OFlags::NONBLOCK;
let file = match open_state_file(project_dir, VERSION_FILE, flags, false) {
Ok(file) => file,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
let mut text = String::new();
file.take(READ_LIMIT).read_to_string(&mut text)?;
text.trim().parse().map(Some).map_err(|_| {
io::Error::new(
ErrorKind::InvalidData,
format!(
"{} does not hold a format number",
project_dir
.join(crate::project_lock::STATE_DIR)
.join(VERSION_FILE)
.display()
),
)
})
}
pub fn write_current(project_dir: &Path) -> io::Result<()> {
replace_state_file(project_dir, VERSION_FILE, format!("{CURRENT}\n").as_bytes())
}
#[must_use]
pub fn predates_versions(project_dir: &Path) -> bool {
project_dir.join(crate::config::CONFIG_FILE).is_file() && matches!(read(project_dir), Ok(None))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project_lock::STATE_DIR;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn a_written_version_reads_back() -> TestResult {
let dir = tempfile::tempdir()?;
assert_eq!(read(dir.path())?, None);
write_current(dir.path())?;
assert_eq!(read(dir.path())?, Some(CURRENT));
assert_eq!(
std::fs::read_to_string(dir.path().join(STATE_DIR).join(VERSION_FILE))?,
"1\n"
);
Ok(())
}
#[test]
fn a_version_that_is_not_a_number_is_an_error() -> TestResult {
let dir = tempfile::tempdir()?;
std::fs::create_dir(dir.path().join(STATE_DIR))?;
std::fs::write(dir.path().join(STATE_DIR).join(VERSION_FILE), "one\n")?;
match read(dir.path()) {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidData),
other => return Err(format!("expected an error, got {other:?}").into()),
}
Ok(())
}
#[cfg(target_os = "linux")]
#[test]
fn a_fifo_version_is_refused_without_blocking() -> TestResult {
let dir = tempfile::tempdir()?;
std::fs::create_dir(dir.path().join(STATE_DIR))?;
rustix::fs::mkfifoat(
rustix::fs::CWD,
dir.path().join(STATE_DIR).join(VERSION_FILE),
rustix::fs::Mode::from_raw_mode(0o600),
)?;
match read(dir.path()) {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
other => return Err(format!("expected an error, got {other:?}").into()),
}
std::fs::write(dir.path().join(crate::config::CONFIG_FILE), "")?;
assert!(!predates_versions(dir.path()));
Ok(())
}
#[test]
fn only_a_project_without_a_version_predates_versions() -> TestResult {
let dir = tempfile::tempdir()?;
assert!(!predates_versions(dir.path()), "not a project");
std::fs::write(dir.path().join(crate::config::CONFIG_FILE), "")?;
assert!(predates_versions(dir.path()));
write_current(dir.path())?;
assert!(!predates_versions(dir.path()));
Ok(())
}
}