#[cfg(test)]
mod tests {
use armour_core::GetType;
use crate::armour::TypedMigration;
use crate::{
CollectionMeta, DbError, FormatAction, FormatMigration, FormatMigrationPolicy,
MigrateAction,
};
use super::{MigrationPlan, TransformBuffers, build_format_only_plan, build_plan};
#[derive(GetType)]
struct V;
impl CollectionMeta for V {
type SelfId = [u8; 8];
const NAME: &'static str = "format_plan_v";
const VERSION: u16 = 4;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[STEP_1, STEP_2, STEP_3];
}
fn write_v2(_key: &[u8], old: &[u8], out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
out.extend_from_slice(if old == b"v1" { b"v2" } else { old });
Ok(FormatAction::Write)
}
fn write_v3(_key: &[u8], old: &[u8], out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
out.extend_from_slice(if old == b"v2" { b"v3" } else { old });
Ok(FormatAction::Write)
}
fn write_v4(_key: &[u8], old: &[u8], out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
out.extend_from_slice(if old == b"v3" { b"v4" } else { old });
Ok(FormatAction::Write)
}
const STEP_1: FormatMigration = FormatMigration::raw(1, write_v2);
const STEP_2: FormatMigration = FormatMigration::raw(2, write_v3);
const STEP_3: FormatMigration = FormatMigration::raw(3, write_v4);
const CHECKED_STEP_7: FormatMigration =
FormatMigration::raw_checked(7, &u8::TYPE, &u16::TYPE, write_v2);
const CHECKED_STEP_8: FormatMigration =
FormatMigration::raw_checked(8, &u16::TYPE, &u32::TYPE, write_v3);
fn keep_output(_key: &[u8], _old: &[u8], out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
out.extend_from_slice(b"ignored");
Ok(FormatAction::Keep)
}
fn drop_output(_key: &[u8], _old: &[u8], out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
out.extend_from_slice(b"ignored");
Ok(FormatAction::Drop)
}
const KEEP_OUTPUT: FormatMigration = FormatMigration::raw(1, keep_output);
const DROP_OUTPUT: FormatMigration = FormatMigration::raw(1, drop_output);
fn fails_step(_key: &[u8], _old: &[u8], _out: &mut Vec<u8>) -> crate::DbResult<FormatAction> {
Err(DbError::Client("step failed"))
}
const FAILS_STEP: FormatMigration = FormatMigration::raw(1, fails_step);
fn keep_value(_key: &[u8; 8], _value: &V) -> MigrateAction<V> {
MigrateAction::Keep
}
#[test]
fn format_plan_rejects_duplicate_format_steps() {
struct Duplicate;
impl GetType for Duplicate {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for Duplicate {
type SelfId = [u8; 8];
const NAME: &'static str = "duplicate_format";
const VERSION: u16 = 2;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[
FormatMigration::raw(1, write_v2),
FormatMigration::raw(1, write_v2),
];
}
assert!(matches!(
build_plan::<[u8; 8], Duplicate>(Duplicate::NAME, Some((1, 0)), &[]),
Err(DbError::DuplicateMigrationStep { version: 1, .. })
));
}
#[test]
fn format_plan_rejects_duplicate_value_steps() {
let steps: &[TypedMigration<[u8; 8], V>] = &[(1, keep_value), (1, keep_value)];
assert!(matches!(
build_plan::<[u8; 8], V>(V::NAME, Some((1, V::TYPE.h())), steps),
Err(DbError::DuplicateMigrationStep { version: 1, .. })
));
}
#[test]
fn format_plan_rejects_cross_kind_duplicate() {
let steps: &[TypedMigration<[u8; 8], V>] = &[(1, keep_value)];
assert!(matches!(
build_plan::<[u8; 8], V>(V::NAME, Some((1, V::TYPE.h())), steps),
Err(DbError::DuplicateMigrationStep { version: 1, .. })
));
}
#[test]
fn format_plan_rejects_format_after_value() {
struct BadOrder;
impl GetType for BadOrder {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for BadOrder {
type SelfId = [u8; 8];
const NAME: &'static str = "format_after_value";
const VERSION: u16 = 3;
const FORMAT_MIGRATIONS: &'static [FormatMigration] =
&[FormatMigration::raw(2, write_v3)];
}
let steps: &[TypedMigration<[u8; 8], BadOrder>] = &[(1, |_k, _v| MigrateAction::Keep)];
assert!(matches!(
build_plan(BadOrder::NAME, Some((1, BadOrder::TYPE.h())), steps),
Err(DbError::FormatStepAfterValueStep { version: 2, .. })
));
}
#[test]
fn format_plan_reports_missing_step() {
assert!(matches!(
build_plan::<[u8; 8], V>(V::NAME, Some((0, V::TYPE.h())), &[]),
Err(DbError::SchemaMismatch {
kind: crate::SchemaMismatchKind::MissingStep { from: 0 },
..
})
));
}
#[test]
fn format_plan_rejects_old_or_broken_type_chain() {
struct Checked;
impl GetType for Checked {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for Checked {
type SelfId = [u8; 8];
const NAME: &'static str = "checked_format";
const VERSION: u16 = 2;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[FormatMigration::raw_checked(
1,
&u8::TYPE,
&u16::TYPE,
write_v2,
)];
}
assert!(matches!(
build_plan::<[u8; 8], Checked>(Checked::NAME, Some((1, u16::TYPE.h())), &[]),
Err(DbError::FormatTypMismatch { from: 1, .. })
));
}
#[test]
fn format_only_plan_allows_value_suffix_and_warns() {
let plan = build_format_only_plan::<V>(V::NAME, Some((1, V::TYPE.h())))
.unwrap()
.unwrap();
assert_eq!(plan.format_to_version, 4);
assert!(
plan.warnings
.iter()
.any(|warning| warning.contains("raw type check skipped"))
);
assert!(
plan.warnings
.iter()
.any(|warning| { warning == "raw step at version 1 has no replay fingerprint" })
);
}
#[test]
fn format_only_plan_warns_about_unvalidated_value_suffix() {
struct CheckedPrefix;
impl GetType for CheckedPrefix {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for CheckedPrefix {
type SelfId = [u8; 8];
const NAME: &'static str = "checked_format_prefix";
const VERSION: u16 = 3;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[FormatMigration::raw_checked(
1,
&V::TYPE,
&V::TYPE,
write_v2,
)];
}
let plan =
build_format_only_plan::<CheckedPrefix>(CheckedPrefix::NAME, Some((1, V::TYPE.h())))
.unwrap()
.unwrap();
assert!(
plan.warnings
.iter()
.any(|warning| warning.contains("value suffix")),
"format-only plans must disclose the unvalidated value suffix"
);
}
#[test]
fn format_only_plan_completed_prefix_returns_none() {
struct CheckedPrefix;
impl GetType for CheckedPrefix {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for CheckedPrefix {
type SelfId = [u8; 8];
const NAME: &'static str = "completed_format_prefix";
const VERSION: u16 = 3;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[FormatMigration::raw_checked(
1,
&V::TYPE,
&V::TYPE,
write_v2,
)];
}
assert!(
build_format_only_plan::<CheckedPrefix>(CheckedPrefix::NAME, Some((2, V::TYPE.h())))
.unwrap()
.is_none()
);
}
#[test]
fn format_only_plan_value_only_returns_none() {
struct ValueOnly;
impl GetType for ValueOnly {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for ValueOnly {
type SelfId = [u8; 8];
const NAME: &'static str = "value_only_format_prefix";
const VERSION: u16 = 2;
}
assert!(
build_format_only_plan::<ValueOnly>(ValueOnly::NAME, Some((1, V::TYPE.h())))
.unwrap()
.is_none()
);
}
#[test]
fn format_only_plan_rejects_a_later_format_step_after_a_value_gap() {
struct NonContiguousFormat;
impl GetType for NonContiguousFormat {
const TYPE: armour_core::Typ = V::TYPE;
}
impl CollectionMeta for NonContiguousFormat {
type SelfId = [u8; 8];
const NAME: &'static str = "noncontiguous_format_prefix";
const VERSION: u16 = 4;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[
FormatMigration::raw_checked(1, &V::TYPE, &V::TYPE, write_v2),
FormatMigration::raw_checked(3, &V::TYPE, &V::TYPE, write_v4),
];
}
assert!(matches!(
build_format_only_plan::<NonContiguousFormat>(
NonContiguousFormat::NAME,
Some((1, V::TYPE.h()))
),
Err(DbError::FormatStepAfterValueStep {
collection: NonContiguousFormat::NAME,
version: 3,
})
));
}
#[test]
fn format_only_plan_rejects_checked_prefix_with_wrong_final_type() {
struct WrongFinal;
impl GetType for WrongFinal {
const TYPE: armour_core::Typ = u64::TYPE;
}
impl CollectionMeta for WrongFinal {
type SelfId = [u8; 8];
const NAME: &'static str = "wrong_final_format";
const VERSION: u16 = 2;
const FORMAT_MIGRATIONS: &'static [FormatMigration] = &[FormatMigration::raw_checked(
1,
&u32::TYPE,
&u16::TYPE,
write_v2,
)];
}
assert!(matches!(
build_format_only_plan::<WrongFinal>(WrongFinal::NAME, Some((1, u32::TYPE.h()))),
Err(DbError::FormatTypMismatch { .. })
));
}
#[test]
fn first_open_builds_no_plan() {
assert!(
build_plan::<[u8; 8], V>(V::NAME, None, &[])
.unwrap()
.is_none()
);
}
#[test]
fn same_version_keeps_typ_hash_guard() {
assert!(matches!(
build_plan::<[u8; 8], V>(V::NAME, Some((V::VERSION, 0)), &[]),
Err(DbError::SchemaMismatch {
kind: crate::SchemaMismatchKind::TypHash { .. },
..
})
));
}
#[test]
fn format_transform_clears_out_and_ping_pongs_three_steps() {
let plan = MigrationPlan {
collection: V::NAME,
stored_version: 1,
target_version: 4,
format_to_version: 4,
format_steps: vec![&STEP_1, &STEP_2, &STEP_3],
accepted_fingerprints: vec![],
warnings: vec![],
};
let mut buffers = TransformBuffers::default();
buffers.a.extend_from_slice(b"stale-a");
buffers.b.extend_from_slice(b"stale-b");
let outcome = plan
.transform_from_version(1, b"key", b"v1", &mut buffers)
.unwrap();
assert_eq!(outcome.as_bytes(), Some(&b"v4"[..]));
assert!(buffers.a.capacity() >= 2);
assert!(buffers.b.capacity() >= 2);
}
#[test]
fn format_transform_logged_uses_the_recorded_suffix_version_not_an_step_index() {
let plan = MigrationPlan {
collection: V::NAME,
stored_version: 7,
target_version: 9,
format_to_version: 9,
format_steps: vec![&CHECKED_STEP_7, &CHECKED_STEP_8],
accepted_fingerprints: vec![(u16::TYPE.h(), 8), (u32::TYPE.h(), 9)],
warnings: vec![],
};
let mut buffers = TransformBuffers::default();
let outcome = plan
.transform_logged(u16::TYPE.h(), b"key", b"v2", &mut buffers)
.expect("intermediate fingerprint has a checked suffix");
assert_eq!(outcome.as_bytes(), Some(&b"v3"[..]));
}
#[test]
fn format_transform_logged_fails_closed_for_an_unknown_fingerprint() {
let plan = MigrationPlan {
collection: V::NAME,
stored_version: 7,
target_version: 8,
format_to_version: 8,
format_steps: vec![&CHECKED_STEP_7],
accepted_fingerprints: vec![(u16::TYPE.h(), 8)],
warnings: vec![],
};
assert!(matches!(
plan.transform_logged(
u32::TYPE.h(),
b"key",
b"v1",
&mut TransformBuffers::default()
),
Err(DbError::TxSchemaMismatch { .. })
));
}
#[test]
fn format_transform_ignores_out_for_keep_and_drop() {
let mut buffers = TransformBuffers::default();
let keep_plan = MigrationPlan {
collection: V::NAME,
stored_version: 1,
target_version: 2,
format_to_version: 2,
format_steps: vec![&KEEP_OUTPUT],
accepted_fingerprints: vec![],
warnings: vec![],
};
assert_eq!(
keep_plan
.transform_from_version(1, b"k", b"old", &mut buffers)
.unwrap()
.as_bytes(),
Some(&b"old"[..])
);
let drop_plan = MigrationPlan {
collection: V::NAME,
stored_version: 1,
target_version: 2,
format_to_version: 2,
format_steps: vec![&DROP_OUTPUT],
accepted_fingerprints: vec![],
warnings: vec![],
};
assert_eq!(
drop_plan
.transform_from_version(1, b"k", b"old", &mut buffers)
.unwrap()
.as_bytes(),
None
);
}
#[test]
fn format_transform_reports_the_collection_that_declared_the_step() {
let plan = MigrationPlan {
collection: V::NAME,
stored_version: 1,
target_version: 2,
format_to_version: 2,
format_steps: vec![&FAILS_STEP],
accepted_fingerprints: vec![],
warnings: vec![],
};
let mut buffers = TransformBuffers::default();
let result = plan.transform_from_version(1, b"key", b"old", &mut buffers);
let error = match result {
Ok(_) => panic!("callback failure must retain context"),
Err(error) => error,
};
assert!(matches!(
error,
DbError::FormatStepFailed {
collection: V::NAME,
from_version: 1,
ref key,
..
} if key == "6b6579"
));
assert!(error.to_string().contains("step failed"), "{error}");
}
#[test]
fn format_step_macro_single_codec_maps_none_to_drop() {
let step = crate::format_step!(1, u8 => u8, crate::ZerocopyCodec, |old| Ok::<_, crate::DbError>(None::<u8>));
let mut out = Vec::new();
assert_eq!(
(step.step)(b"key", &[7], &mut out).unwrap(),
FormatAction::Drop
);
}
#[test]
fn format_step_macro_codec_change_encodes_new_value() {
let step = crate::format_step!(1, u8 => u16, crate::ZerocopyCodec => crate::ZerocopyCodec, |old| Ok::<_, crate::DbError>(Some(old as u16 + 1)));
let mut out = Vec::new();
assert_eq!(
(step.step)(b"key", &[7], &mut out).unwrap(),
FormatAction::Write
);
assert_eq!(out, 8u16.to_ne_bytes());
}
#[test]
fn format_step_macro_key_aware_receives_original_key() {
let step = crate::format_step!(1, u8 => u8, crate::ZerocopyCodec, |key, old| {
assert_eq!(key, b"original-key");
Ok::<_, crate::DbError>(Some(old + 1))
});
let mut out = Vec::new();
assert_eq!(
(step.step)(b"original-key", &[7], &mut out).unwrap(),
FormatAction::Write
);
assert_eq!(out, [8]);
}
#[test]
fn format_step_macro_key_aware_codec_change_compiles() {
let step = crate::format_step!(1, u8 => u16, crate::ZerocopyCodec => crate::ZerocopyCodec, |key, old| {
assert_eq!(key, b"key");
Ok::<_, crate::DbError>(Some(old as u16))
});
let mut out = Vec::new();
assert_eq!(
(step.step)(b"key", &[7], &mut out).unwrap(),
FormatAction::Write
);
}
#[test]
fn format_migration_policy_defaults_to_auto() {
assert_eq!(
crate::Config::balanced().build().format_migration,
FormatMigrationPolicy::Auto
);
assert_eq!(
crate::FixedConfig::balanced()
.format_migration(FormatMigrationPolicy::Refuse)
.build()
.format_migration,
FormatMigrationPolicy::Refuse,
);
}
}
use std::{collections::BTreeMap, fmt::Write};
use crate::{
CollectionMeta, DbError, DbResult, FormatAction, FormatMigration, SchemaMismatchKind,
armour::TypedMigration,
};
pub struct MigrationPlan {
pub(crate) collection: &'static str,
pub(crate) stored_version: u16,
pub(crate) target_version: u16,
pub(crate) format_to_version: u16,
pub(crate) format_steps: Vec<&'static FormatMigration>,
pub(crate) accepted_fingerprints: Vec<(u64, usize)>,
pub(crate) warnings: Vec<String>,
}
pub enum TransformOutcome<'a> {
Write(&'a [u8]),
Keep(&'a [u8]),
Drop,
}
impl TransformOutcome<'_> {
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::Write(bytes) | Self::Keep(bytes) => Some(bytes),
Self::Drop => None,
}
}
}
#[derive(Default)]
pub struct TransformBuffers {
pub a: Vec<u8>,
pub b: Vec<u8>,
}
enum HandlerKind {
Format(&'static FormatMigration),
Value,
}
pub fn build_plan<K, T>(
collection: &'static str,
stored: Option<(u16, u64)>,
value_steps: &[TypedMigration<K, T>],
) -> DbResult<Option<MigrationPlan>>
where
T: CollectionMeta,
{
let Some((stored_version, stored_typ)) = stored else {
return Ok(None);
};
validate_stored::<T>(collection, stored_version, stored_typ)?;
if stored_version == T::VERSION {
return Ok(None);
}
let handlers = collect_handlers::<K, T>(collection, value_steps)?;
build_from_handlers::<T>(collection, stored_version, stored_typ, &handlers, false)
}
pub(crate) fn build_replay_plan<K, T>(
collection: &'static str,
stored: Option<(u16, u64)>,
value_steps: &[TypedMigration<K, T>],
) -> DbResult<Option<MigrationPlan>>
where
T: CollectionMeta,
{
let Some((stored_version, stored_typ)) = stored else {
return Ok(None);
};
validate_stored::<T>(collection, stored_version, stored_typ)?;
let handlers = collect_handlers::<K, T>(collection, value_steps)?;
let Some(first_format) = T::FORMAT_MIGRATIONS
.iter()
.min_by_key(|step| step.from_version)
else {
return Ok(None);
};
let replay_from_version = first_format.from_version;
let replay_from_typ = first_format
.from_typ
.map_or(stored_typ, armour_core::Typ::h);
build_from_handlers::<T>(
collection,
replay_from_version,
replay_from_typ,
&handlers,
true,
)
}
pub(crate) fn build_format_only_plan<T>(
collection: &'static str,
stored: Option<(u16, u64)>,
) -> DbResult<Option<MigrationPlan>>
where
T: CollectionMeta,
{
let Some((stored_version, stored_typ)) = stored else {
return Ok(None);
};
validate_stored::<T>(collection, stored_version, stored_typ)?;
if stored_version == T::VERSION {
return Ok(None);
}
let handlers = collect_handlers::<(), T>(collection, &[])?;
build_from_handlers::<T>(collection, stored_version, stored_typ, &handlers, true)
}
fn validate_stored<T: CollectionMeta>(
collection: &'static str,
stored_version: u16,
stored_typ: u64,
) -> DbResult<()> {
if stored_version == T::VERSION && stored_typ != T::TYPE.h() {
return Err(DbError::SchemaMismatch {
name: collection.to_owned(),
kind: SchemaMismatchKind::TypHash {
stored: stored_typ,
expected: T::TYPE.h(),
},
});
}
if stored_version > T::VERSION {
return Err(DbError::SchemaMismatch {
name: collection.to_owned(),
kind: SchemaMismatchKind::Downgrade {
stored: stored_version,
requested: T::VERSION,
},
});
}
Ok(())
}
fn collect_handlers<K, T>(
collection: &'static str,
value_steps: &[TypedMigration<K, T>],
) -> DbResult<BTreeMap<u16, HandlerKind>>
where
T: CollectionMeta,
{
let mut handlers = BTreeMap::new();
for step in T::FORMAT_MIGRATIONS {
if handlers
.insert(step.from_version, HandlerKind::Format(step))
.is_some()
{
return Err(DbError::DuplicateMigrationStep {
collection,
version: step.from_version,
});
}
}
for (version, _) in value_steps {
if handlers.insert(*version, HandlerKind::Value).is_some() {
return Err(DbError::DuplicateMigrationStep {
collection,
version: *version,
});
}
}
Ok(handlers)
}
fn build_from_handlers<T: CollectionMeta>(
collection: &'static str,
stored_version: u16,
stored_typ: u64,
handlers: &BTreeMap<u16, HandlerKind>,
format_only: bool,
) -> DbResult<Option<MigrationPlan>> {
let mut format_steps = Vec::new();
let mut warnings = Vec::new();
let mut accepted_fingerprints = Vec::new();
let mut value_seen = false;
let mut expected_typ = Some(stored_typ);
let mut format_to_version = stored_version;
for version in stored_version..T::VERSION {
let Some(handler) = handlers.get(&version) else {
if format_only {
if let Some((&later, _)) = handlers
.range(version + 1..T::VERSION)
.find(|(_, handler)| matches!(handler, HandlerKind::Format(_)))
{
return Err(DbError::FormatStepAfterValueStep {
collection,
version: later,
});
}
if !format_steps.is_empty() {
warnings.push(format!(
"value suffix from version {version} is not validated by format-only planning"
));
}
break;
}
return Err(DbError::SchemaMismatch {
name: collection.to_owned(),
kind: SchemaMismatchKind::MissingStep { from: version },
});
};
match handler {
HandlerKind::Value => value_seen = true,
HandlerKind::Format(step) if value_seen => {
return Err(DbError::FormatStepAfterValueStep {
collection,
version,
});
}
HandlerKind::Format(step) => {
if let (Some(expected), Some(from_typ)) = (expected_typ, step.from_typ) {
let actual = from_typ.h();
if expected != actual {
return Err(DbError::FormatTypMismatch {
collection,
from: version,
expected: actual,
stored: expected,
});
}
}
if step.from_typ.is_none() || step.to_typ.is_none() {
tracing::warn!(
collection,
version,
"raw format migration skips local type check"
);
warnings.push(format!("raw type check skipped at version {version}"));
warnings.push(format!(
"raw step at version {version} has no replay fingerprint"
));
expected_typ = None;
} else if let Some(to_typ) = step.to_typ {
expected_typ = Some(to_typ.h());
accepted_fingerprints.push((to_typ.h(), version as usize + 1));
}
format_steps.push(*step);
format_to_version = version + 1;
}
}
}
if format_steps.is_empty() {
return Ok(None);
}
if let Some(stored) = expected_typ
&& stored != T::TYPE.h()
{
return Err(DbError::FormatTypMismatch {
collection,
from: format_to_version,
expected: T::TYPE.h(),
stored,
});
}
Ok(Some(MigrationPlan {
collection,
stored_version,
target_version: T::VERSION,
format_to_version,
format_steps,
accepted_fingerprints,
warnings,
}))
}
impl MigrationPlan {
pub fn transform_from_version<'a>(
&self,
from_version: u16,
key: &[u8],
value: &'a [u8],
buffers: &'a mut TransformBuffers,
) -> DbResult<TransformOutcome<'a>> {
enum Current {
Original,
A,
B,
}
let mut current = Current::Original;
for step in self
.format_steps
.iter()
.copied()
.filter(|step| step.from_version >= from_version)
{
let action = match current {
Current::Original => {
buffers.a.clear();
(step.step)(key, value, &mut buffers.a)
}
Current::A => {
buffers.b.clear();
(step.step)(key, &buffers.a, &mut buffers.b)
}
Current::B => {
buffers.a.clear();
(step.step)(key, &buffers.b, &mut buffers.a)
}
}
.map_err(|error| DbError::FormatStepFailed {
collection: self.collection,
from_version: step.from_version,
key: key_hex(key),
cause: error.to_string(),
})?;
match action {
FormatAction::Write => {
current = match current {
Current::Original | Current::B => Current::A,
Current::A => Current::B,
}
}
FormatAction::Keep => {}
FormatAction::Drop => return Ok(TransformOutcome::Drop),
}
}
Ok(match current {
Current::Original => TransformOutcome::Keep(value),
Current::A => TransformOutcome::Write(&buffers.a),
Current::B => TransformOutcome::Write(&buffers.b),
})
}
}
fn ambiguous_fingerprint(
steps: impl IntoIterator<Item = &'static FormatMigration>,
logged_fingerprint: u64,
_target_fingerprint: u64,
) -> bool {
let mut versions = BTreeMap::new();
let mut raw = false;
for step in steps {
let (Some(from), Some(to)) = (step.from_typ, step.to_typ) else {
raw = true;
continue;
};
for (step_fingerprint, version) in [
(from.h(), step.from_version),
(to.h(), step.from_version + 1),
] {
if versions
.insert(step_fingerprint, version)
.is_some_and(|prior| prior != version)
&& step_fingerprint == logged_fingerprint
{
return true;
}
}
}
raw
}
impl MigrationPlan {
pub(crate) fn logged_fingerprint_is_ambiguous(
&self,
fingerprint: u64,
target_fingerprint: u64,
) -> bool {
ambiguous_fingerprint(
self.format_steps.iter().copied(),
fingerprint,
target_fingerprint,
)
}
}
pub(crate) fn key_hex(key: &[u8]) -> String {
let mut hex = String::with_capacity(key.len() * 2);
for byte in key {
Write::write_fmt(&mut hex, format_args!("{byte:02x}"))
.expect("writing into String cannot fail");
}
hex
}