135 lines
3.6 KiB
Rust
135 lines
3.6 KiB
Rust
|
|
//! 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<Self> {
|
||
|
|
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<Version, PimsyncError> {
|
||
|
|
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());
|
||
|
|
}
|
||
|
|
}
|