blob: a86d66208466254056787988f2246fcec71251c4 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{anyhow, Context as _},
fuchsia_merkle::Hash,
fuchsia_syslog::fx_log_err,
serde::{Deserialize, Serialize},
std::{
fs::{self, File},
io,
path::PathBuf,
},
};
pub trait LastUpdateStorage {
fn load(&self) -> Option<Hash>;
fn store(&self, update: &Hash);
}
pub struct LastUpdateStorageFile {
pub data_dir: PathBuf,
}
const LAST_UPDATE_FILENAME: &str = "last_update.json";
const LAST_UPDATE_FILENAME_PART: &str = "last_update.json.part";
impl LastUpdateStorage for LastUpdateStorageFile {
fn load(&self) -> Option<Hash> {
self.load_impl().unwrap_or_else(|e| {
fx_log_err!("error loading last_update.json: {:#}", anyhow!(e));
None
})
}
fn store(&self, update: &Hash) {
self.store_impl(update).unwrap_or_else(|e| {
fx_log_err!("error storing last_update info, continuing anyway; {:#}", anyhow!(e));
})
}
}
impl LastUpdateStorageFile {
fn load_impl(&self) -> Result<Option<Hash>, anyhow::Error> {
let open_result = File::open(self.data_dir.join(LAST_UPDATE_FILENAME));
if let Err(e) = &open_result {
if e.kind() == io::ErrorKind::NotFound {
return Ok(None);
}
}
let file = open_result.context("opening last update file")?;
let value: StorageFormat =
serde_json::from_reader(file).context("reading last update file")?;
let StorageFormat::Version1(v1) = value;
let hash =
v1.update_package_merkle.parse::<Hash>().context("parsing update_package_merkle")?;
Ok(Some(hash))
}
fn store_impl(&self, update: &Hash) -> Result<(), anyhow::Error> {
let wrapped =
StorageFormat::Version1(StorageFormatV1 { update_package_merkle: update.to_string() });
let part_filename = self.data_dir.join(LAST_UPDATE_FILENAME_PART);
{
let file = File::create(&part_filename).context("opening last update file")?;
serde_json::to_writer(&file, &wrapped).context("writing last update file")?;
file.sync_all().context("sync last update file")?;
}
fs::rename(part_filename, self.data_dir.join(LAST_UPDATE_FILENAME))
.context("swap last update file into place")?;
Ok(())
}
}
#[derive(Deserialize, Serialize)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
enum StorageFormat {
#[serde(rename = "1")]
Version1(StorageFormatV1),
}
#[derive(Deserialize, Serialize)]
struct StorageFormatV1 {
update_package_merkle: String,
}
#[cfg(test)]
mod tests {
use {super::*, lazy_static::lazy_static, serde_json::json, std::fs, tempfile::TempDir};
lazy_static! {
static ref EXAMPLE_UPDATE_HASH: Hash = [0x22; 32].into();
}
#[test]
fn test_load_works() {
let tempdir = TempDir::new().expect("create tempdir");
fs::write(
tempdir.path().join("last_update.json"),
r#"{"version":"1","content":{"update_package_merkle":"2222222222222222222222222222222222222222222222222222222222222222"}}"#,
)
.expect("write last_update.json");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
assert_eq!(storage.load(), Some(*EXAMPLE_UPDATE_HASH));
}
#[test]
fn test_load_without_file_returns_none() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
assert_eq!(storage.load(), None);
}
#[test]
fn test_load_impl_without_file_returns_none() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
// We don't want it to print a log if the file isn't found, so it should return Ok(None) instead of an error.
assert_eq!(storage.load_impl().map_err(|e| e.to_string()), Ok(None));
}
#[test]
fn test_load_with_invalid_file_returns_none() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
fs::write(
tempdir.path().join("last_update.json"),
r#""2222222222222222222222222222222222222222222222222222222222222222""#,
)
.expect("write last_update.json");
assert_eq!(storage.load(), None);
}
#[test]
fn test_store_works() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
storage.store(&*EXAMPLE_UPDATE_HASH);
let data = fs::read(tempdir.path().join("last_update.json")).expect("read file");
let parsed: serde_json::Value = serde_json::from_slice(&data).expect("parse json");
assert_eq!(
parsed,
json!({
"version": "1",
"content": {
"update_package_merkle": "2222222222222222222222222222222222222222222222222222222222222222"
}
})
)
}
#[test]
fn test_store_roundtrip() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
storage.store(&*EXAMPLE_UPDATE_HASH);
assert_eq!(storage.load().expect("a stored value"), *EXAMPLE_UPDATE_HASH);
}
#[test]
fn test_store_overwrite() {
let tempdir = TempDir::new().expect("create tempdir");
let storage = LastUpdateStorageFile { data_dir: tempdir.path().into() };
storage.store(&[0x33; 32].into());
storage.store(&*EXAMPLE_UPDATE_HASH);
assert_eq!(storage.load().expect("a stored value"), *EXAMPLE_UPDATE_HASH);
}
}