use std::num::NonZeroU64;
use wire::PrivacyControl;
use super::controls::definition;
use wire::PrivacySettlement;
use super::state::{Holdings, Intent};
use super::target::{Reading, Snapshot};
pub(super) struct Request<'r> {
pub(super) control: PrivacyControl,
pub(super) operation: NonZeroU64,
pub(super) expected: &'r Snapshot,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum ApplyPlan {
Write(Intent),
Unresolved,
Changed,
TargetAbsent,
AlreadyWritten,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum RestorePlan {
Write(Intent),
Unresolved,
NothingOwned,
Changed,
Conflict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Readback {
Matches,
StillOriginal,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Verdict {
Applied,
Restored,
NotApplied,
RollBack,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RollbackEnd {
RolledBack,
Lost,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum ReconcilePlan {
Settle(PrivacySettlement),
Changed,
}
pub(super) fn plan_apply(
holdings: &Holdings<'_>,
request: &Request<'_>,
fresh: &Reading,
) -> ApplyPlan {
if holdings.unresolved().is_some() {
return ApplyPlan::Unresolved;
}
if *request.expected != fresh.value {
return ApplyPlan::Changed;
}
match definition(request.control).written.target(&fresh.value) {
None => ApplyPlan::TargetAbsent,
Some(target) if target == fresh.value => ApplyPlan::AlreadyWritten,
Some(target) => ApplyPlan::Write(Intent {
operation: request.operation,
control: request.control,
owner: holdings.owner().clone(),
original: fresh.value.clone(),
modified: target,
key_existed: fresh.key_existed,
restore_of: None,
}),
}
}
pub(super) fn plan_restore(
holdings: &Holdings<'_>,
request: &Request<'_>,
fresh: &Reading,
) -> RestorePlan {
if holdings.unresolved().is_some() {
return RestorePlan::Unresolved;
}
let Some(top) = holdings.latest_owned(request.control) else {
return RestorePlan::NothingOwned;
};
if *request.expected != fresh.value {
return RestorePlan::Changed;
}
if fresh.value != top.modified {
return RestorePlan::Conflict;
}
RestorePlan::Write(Intent {
operation: request.operation,
control: request.control,
owner: holdings.owner().clone(),
original: fresh.value.clone(),
modified: top.original.clone(),
key_existed: fresh.key_existed,
restore_of: Some(top.operation),
})
}
pub(super) fn judge_readback(intent: &Intent, value: &Snapshot) -> Readback {
if *value == intent.modified {
Readback::Matches
} else if *value == intent.original {
Readback::StillOriginal
} else {
Readback::Other
}
}
pub(super) fn settle(intent: &Intent, readback: Readback) -> Verdict {
match (readback, intent.restore_of) {
(Readback::Matches, None) => Verdict::Applied,
(Readback::Matches, Some(_)) => Verdict::Restored,
(Readback::StillOriginal, None | Some(_)) => Verdict::NotApplied,
(Readback::Other, None | Some(_)) => Verdict::RollBack,
}
}
pub(super) fn settle_rollback(intent: &Intent, value: &Snapshot) -> RollbackEnd {
match judge_readback(intent, value) {
Readback::StillOriginal => RollbackEnd::RolledBack,
Readback::Matches | Readback::Other => RollbackEnd::Lost,
}
}
pub(super) fn plan_reconcile(
unresolved: &Intent,
expected: &Snapshot,
fresh: &Snapshot,
) -> ReconcilePlan {
if expected != fresh {
return ReconcilePlan::Changed;
}
ReconcilePlan::Settle(reconciled(unresolved, fresh))
}
fn reconciled(intent: &Intent, value: &Snapshot) -> PrivacySettlement {
match settle(intent, judge_readback(intent, value)) {
Verdict::Applied => PrivacySettlement::Applied,
Verdict::Restored => PrivacySettlement::Restored,
Verdict::NotApplied => PrivacySettlement::NotApplied,
Verdict::RollBack => PrivacySettlement::Abandoned,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, reason = "test code")]
mod tests {
use super::super::state::fixtures::{apply, dword, finished, operation, owner, prepared};
use super::super::state::{History, Line, Outcome};
use super::super::target::{RawValue, TaskState};
use super::*;
use kernel::{AxCode, AxError, SecretRef};
const ONE: PrivacyControl = PrivacyControl::FeedbackNotifications;
const TASK: PrivacyControl = PrivacyControl::CeipConsolidatorTask;
const OTHER: PrivacyControl = PrivacyControl::StartLaunchTracking;
enum Owner {
Seven,
Eight,
}
fn absent() -> Snapshot {
Snapshot::Registry(RawValue::Absent)
}
fn text() -> Snapshot {
Snapshot::Registry(RawValue::Present {
kind: 1,
bytes: vec![49, 0, 0, 0],
})
}
fn task(state: fn([u8; 32]) -> TaskState) -> Snapshot {
Snapshot::Task(state([5; 32]))
}
fn enabled(definition_sha256: [u8; 32]) -> TaskState {
TaskState::Enabled { definition_sha256 }
}
fn disabled(definition_sha256: [u8; 32]) -> TaskState {
TaskState::Disabled { definition_sha256 }
}
fn applied() -> Intent {
Intent {
key_existed: false,
..apply(1, ONE, absent(), dword(1))
}
}
fn lines(phase: Phase, owned: &[Intent]) -> Vec<Line> {
let mut lines = Vec::new();
for intent in owned {
lines.extend([prepared(intent), finished(intent, Outcome::Applied)]);
}
let earlier = apply(9, OTHER, dword(1), dword(0));
match phase {
Phase::Idle if owned.is_empty() => {
lines.extend([prepared(&earlier), finished(&earlier, Outcome::NotApplied)]);
}
Phase::Idle => (),
Phase::Prepared => lines.push(prepared(&earlier)),
Phase::Unknown => {
lines.extend([prepared(&earlier), finished(&earlier, Outcome::Unknown)]);
}
}
lines
}
#[derive(Clone, Copy)]
enum Phase {
Idle,
Prepared,
Unknown,
}
fn authorize(who: &Owner) -> impl FnOnce(Option<&SecretRef>) -> Result<SecretRef, AxError> {
move |recorded| match who {
Owner::Seven => Ok(recorded.cloned().unwrap_or_else(owner)),
Owner::Eight => Err(AxError::failure(
AxCode::ConfigInvalid,
"verify privacy owner",
"privacy history belongs to another identity",
)
.with_recovery("run as the owner")),
}
}
struct Row {
phase: Phase,
owned: Vec<Intent>,
who: Owner,
control: PrivacyControl,
live: Snapshot,
expected: Snapshot,
key_existed: bool,
}
fn row(phase: Phase, who: Owner, control: PrivacyControl, live: Snapshot) -> Row {
Row {
phase,
owned: Vec::new(),
who,
control,
expected: live.clone(),
live,
key_existed: false,
}
}
fn decide<P>(row: &Row, plan: fn(&Holdings<'_>, &Request<'_>, &Reading) -> P) -> Option<P> {
let history = History::fold(lines(row.phase, &row.owned)).unwrap();
let holdings = history.holdings(authorize(&row.who)).ok()?;
let request = Request {
control: row.control,
operation: operation(2),
expected: &row.expected,
};
let fresh = Reading {
value: row.live.clone(),
key_existed: row.key_existed,
};
Some(plan(&holdings, &request, &fresh))
}
#[test]
fn apply_vectors_replay() {
let rows = [
row(Phase::Idle, Owner::Eight, ONE, absent()),
row(Phase::Unknown, Owner::Seven, ONE, absent()),
Row {
expected: absent(),
..row(Phase::Idle, Owner::Seven, ONE, dword(0))
},
row(Phase::Idle, Owner::Seven, ONE, dword(1)),
row(Phase::Idle, Owner::Seven, ONE, absent()),
Row {
key_existed: true,
..row(Phase::Idle, Owner::Seven, ONE, text())
},
row(
Phase::Idle,
Owner::Seven,
TASK,
Snapshot::Task(TaskState::Absent),
),
row(Phase::Idle, Owner::Seven, TASK, task(disabled)),
row(Phase::Idle, Owner::Seven, TASK, task(enabled)),
];
let write = |control, original, modified, key_existed| {
Some(ApplyPlan::Write(Intent {
key_existed,
..apply(2, control, original, modified)
}))
};
assert_eq!(
rows.iter()
.map(|row| decide(row, plan_apply))
.collect::<Vec<_>>(),
[
None,
Some(ApplyPlan::Unresolved),
Some(ApplyPlan::Changed),
Some(ApplyPlan::AlreadyWritten),
write(ONE, absent(), dword(1), false),
write(ONE, text(), dword(1), true),
Some(ApplyPlan::TargetAbsent),
Some(ApplyPlan::AlreadyWritten),
write(TASK, task(enabled), task(disabled), false),
]
);
}
#[test]
fn restore_vectors_replay() {
let owning = |phase, who, live: Snapshot, expected| Row {
owned: vec![applied()],
expected,
key_existed: true,
..row(phase, who, ONE, live)
};
let rows = [
owning(Phase::Idle, Owner::Eight, dword(1), dword(1)),
owning(Phase::Prepared, Owner::Seven, dword(1), dword(1)),
Row {
key_existed: true,
..row(Phase::Idle, Owner::Seven, ONE, dword(1))
},
owning(Phase::Idle, Owner::Seven, dword(1), dword(0)),
owning(Phase::Idle, Owner::Seven, dword(0), dword(0)),
owning(Phase::Idle, Owner::Seven, dword(1), dword(1)),
];
assert_eq!(
rows.iter()
.map(|row| decide(row, plan_restore))
.collect::<Vec<_>>(),
[
None,
Some(RestorePlan::Unresolved),
Some(RestorePlan::NothingOwned),
Some(RestorePlan::Changed),
Some(RestorePlan::Conflict),
Some(RestorePlan::Write(Intent {
operation: operation(2),
original: dword(1),
modified: absent(),
key_existed: true,
restore_of: Some(operation(1)),
..applied()
})),
]
);
}
#[test]
fn readback_vectors_replay() {
let restore = Intent {
operation: operation(2),
original: dword(1),
modified: absent(),
restore_of: Some(operation(1)),
..applied()
};
let judged: Vec<_> = [applied(), restore]
.iter()
.flat_map(|intent| {
[intent.modified.clone(), intent.original.clone(), dword(0)].map(|value| {
let readback = judge_readback(intent, &value);
(
readback,
settle(intent, readback),
reconciled(intent, &value),
)
})
})
.collect();
assert_eq!(
judged,
[
(
Readback::Matches,
Verdict::Applied,
PrivacySettlement::Applied
),
(
Readback::StillOriginal,
Verdict::NotApplied,
PrivacySettlement::NotApplied
),
(
Readback::Other,
Verdict::RollBack,
PrivacySettlement::Abandoned
),
(
Readback::Matches,
Verdict::Restored,
PrivacySettlement::Restored
),
(
Readback::StillOriginal,
Verdict::NotApplied,
PrivacySettlement::NotApplied
),
(
Readback::Other,
Verdict::RollBack,
PrivacySettlement::Abandoned
),
]
);
}
}