format_stability.rs
raw
//! Guards against changes that would make previously written data unreadable.
use cipherbox::{CipherBox, Error};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
/// Password for the fixture. It contains a precomposed "é" (U+00E9), so reading
/// it back with the decomposed form also pins password normalization.
const FIXTURE_PASSWORD: &str = "fixture p\u{e9}ssword";
/// A value exercising each part of the serde data model that the fixture
/// pins down.
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize)]
struct Fixture {
text: String,
bytes: Vec<u8>,
small: u8,
large: u64,
negative: i64,
flag: bool,
missing: Option<String>,
present: Option<u32>,
modes: Vec<Mode>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize)]
enum Mode {
Plain,
Tagged(u32),
Named { depth: u8 },
}
fn fixture() -> Fixture {
Fixture {
text: String::from("The quick brown fox jumps over the lazy dog"),
bytes: vec![0, 1, 2, 254, 255],
small: 7,
large: u64::MAX,
negative: -42,
flag: true,
missing: None,
present: Some(1_000_000),
modes: vec![Mode::Plain, Mode::Tagged(9), Mode::Named { depth: 3 }],
}
}
/// `tests/data/v1.cb` was written by format version 1 and must remain readable
/// by every future release. Never regenerate it: if this test fails, the
/// change that broke it has made existing user data unreadable. A new format
/// version gets a new fixture alongside this one.
#[test]
fn format_v1_fixture_remains_readable() {
let file = include_bytes!("data/v1.cb");
let restored: CipherBox<Fixture> = CipherBox::read(FIXTURE_PASSWORD, &file[..]).unwrap();
assert_eq!(*restored, fixture());
let decomposed = FIXTURE_PASSWORD.replace('\u{e9}', "e\u{301}");
let restored: CipherBox<Fixture> = CipherBox::read(&decomposed, &file[..]).unwrap();
assert_eq!(*restored, fixture());
}
#[derive(Serialize, Deserialize, Zeroize)]
struct SettingsV1 {
username: String,
token: String,
retired: u32,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize)]
struct SettingsV2 {
username: String,
token: String,
#[serde(default)]
server: Option<String>,
}
/// Applications evolve the types they store. Adding a defaulted field or
/// removing a field must not make existing data unreadable.
#[test]
fn stored_types_can_evolve() {
let mut file = Vec::new();
CipherBox::new(SettingsV1 {
username: String::from("alice"),
token: String::from("hunter2"),
retired: 5,
})
.write("password", &mut file)
.unwrap();
let restored: CipherBox<SettingsV2> = CipherBox::read("password", file.as_slice()).unwrap();
assert_eq!(
*restored,
SettingsV2 {
username: String::from("alice"),
token: String::from("hunter2"),
server: None,
}
);
}
#[test]
fn mismatched_type_is_a_deserialization_error() {
let mut file = Vec::new();
CipherBox::new(String::from("not a number"))
.write("password", &mut file)
.unwrap();
assert!(matches!(
CipherBox::<u64>::read("password", file.as_slice()),
Err(Error::Deserialization(_))
));
}