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(_))
    ));
}