//! Driving the `pimsync` binary. //! //! pimsync handles the CalDAV and WebCal legs, mirroring each remote into a local //! vdir. It is invoked one-shot: its own daemon would race the reconciler over the //! same files, so scheduling belongs to calcalist. use std::fmt; use std::io; use std::process::Command; use thiserror::Error; pub const BINARY: &str = "pimsync"; /// The `0.5.x` series this build generates configuration for. pimsync is /// pre-1.0 and its config syntax may change between minor releases, so the /// range is deliberately narrow. pub const SUPPORTED_MAJOR: u64 = 0; pub const SUPPORTED_MINOR: u64 = 5; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Version { pub major: u64, pub minor: u64, pub patch: u64, } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}.{}.{}", self.major, self.minor, self.patch) } } impl Version { pub fn is_supported(&self) -> bool { self.major == SUPPORTED_MAJOR && self.minor == SUPPORTED_MINOR } /// Parses the `pimsync 0.5.11` line printed by `pimsync version`. fn parse(output: &str) -> Option { let field = output.split_whitespace().nth(1)?; let mut parts = field.split('.'); Some(Version { major: parts.next()?.parse().ok()?, minor: parts.next()?.parse().ok()?, patch: parts.next().unwrap_or("0").parse().ok()?, }) } } #[derive(Debug, Error)] pub enum PimsyncError { #[error("`{BINARY}` was not found on PATH; install it (devbox provides {BINARY}@0.5.11)")] NotFound, #[error("could not run `{BINARY}`: {0}")] Spawn(#[source] io::Error), #[error("`{BINARY} version` failed: {0}")] Failed(String), #[error("could not read a version out of `{0}`")] Unparseable(String), } /// Asks the installed pimsync for its version. pub fn probe() -> Result { let output = Command::new(BINARY) .arg("version") .output() .map_err(|error| { if error.kind() == io::ErrorKind::NotFound { PimsyncError::NotFound } else { PimsyncError::Spawn(error) } })?; if !output.status.success() { return Err(PimsyncError::Failed( String::from_utf8_lossy(&output.stderr).trim().to_string(), )); } let text = String::from_utf8_lossy(&output.stdout); let line = text.lines().next().unwrap_or_default().trim(); Version::parse(line).ok_or_else(|| PimsyncError::Unparseable(line.to_string())) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_the_version_line() { assert_eq!( Version::parse("pimsync 0.5.11"), Some(Version { major: 0, minor: 5, patch: 11 }) ); } #[test] fn tolerates_a_two_component_version() { assert_eq!( Version::parse("pimsync 1.2"), Some(Version { major: 1, minor: 2, patch: 0 }) ); } #[test] fn rejects_unparseable_output() { assert_eq!(Version::parse("pimsync"), None); assert_eq!(Version::parse("pimsync unreleased"), None); } #[test] fn only_the_supported_series_is_accepted() { let supported = Version { major: 0, minor: 5, patch: 0, }; let next_series = Version { major: 0, minor: 6, patch: 0, }; assert!(supported.is_supported()); assert!(!next_series.is_supported()); } }