use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use atomicwrites::{AllowOverwrite, AtomicFile};
use serde_json;
use errors::*;
use secret::pki::{CaChain, X509};
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
pub struct Cache {
pub certificates: HashMap<String, X509>,
pub ca_certificate_chain: Option<CaChain>,
}
impl Cache {
pub fn load(path: &Path) -> Result<Self> {
if let Ok(file) = File::open(path) {
serde_json::from_reader(file).chain_err(|| "Unable to parse cache on disk")
} else {
Ok(Default::default())
}
}
pub fn update<F: Fn(&mut Self)>(path: &Path, updater: F) -> Result<()> {
debug!("Updating cache on disk");
AtomicFile::new(path, AllowOverwrite)
.write(|f| {
let cache_string = {
let mut cache = Cache::load(path).unwrap_or_default();
updater(&mut cache);
serde_json::to_string(&cache).expect("Impossible to fail to serialize cache")
};
f.write_all(cache_string.as_bytes())
.chain_err(|| "Failed to write cache")
})
.chain_err(|| "Failed to update contents of on-disk cache")
.log(|m| debug!("Successfully updated cache on disk: {:?}", m))
}
}
#[cfg(test)]
mod tests {
extern crate tempfile;
use std::time::Duration;
use self::tempfile::NamedTempFile;
use secret::pki::X509;
use secret::Secret;
use Cache;
fn new_certificate<S: Into<String>>(common_name: S) -> X509 {
X509 {
common_name: common_name.into(),
certificate: "certificate".to_string(),
issuing_ca: "issuing_ca".to_string(),
ca_chain: None,
private_key: "private_key".to_string(),
private_key_type: "private_key_type".to_string(),
serial_number: "serial_number".to_string(),
replace_after: Duration::from_secs(100),
lifetime: Duration::from_secs(200),
}
}
#[test]
fn insert() {
let cache_file = NamedTempFile::new().unwrap();
let certificate = new_certificate("foo");
Cache::update(cache_file.path(), |cache| certificate.update_cache(cache)).unwrap();
assert!(
!Cache::load(cache_file.path())
.unwrap()
.certificates
.contains_key("bar"),
"Unexpected key 'bar' in cache"
);
let new_certificate = new_certificate("bar");
Cache::update(cache_file.path(), |cache| {
new_certificate.update_cache(cache)
}).unwrap();
assert!(
Cache::load(cache_file.path())
.unwrap()
.certificates
.contains_key("foo"),
"'foo' not in cache"
);
assert!(
Cache::load(cache_file.path())
.unwrap()
.certificates
.contains_key("bar"),
"'bar' not in cache"
);
}
#[test]
fn read_write_symmetry() {
let cache_file = NamedTempFile::new().unwrap();
let certificate = new_certificate("foo");
Cache::update(cache_file.path(), |cache| certificate.update_cache(cache)).unwrap();
assert_eq!(
Cache::load(cache_file.path()).unwrap().certificates["foo"],
certificate,
"Certificate after read/write cycle does not match original"
);
}
#[test]
fn idempotency() {
let cache_file = NamedTempFile::new().unwrap();
let certificate = new_certificate("foo");
Cache::update(cache_file.path(), |cache| certificate.update_cache(cache)).unwrap();
let old_cache = Cache::load(cache_file.path()).unwrap();
assert_eq!(
old_cache.certificates["foo"], certificate,
"Failed to write expected certificate"
);
Cache::update(cache_file.path(), |cache| certificate.update_cache(cache)).unwrap();
assert_eq!(
Cache::load(cache_file.path()).unwrap(),
old_cache,
"Cache update not idempotent"
);
}
}