1use crate::error::ScllError;
15use crate::report::CardLifeCycle;
16
17const P2_INITIALIZED: u8 = 0x07;
19const P2_SECURED: u8 = 0x0F;
21const P2_CARD_LOCKED: u8 = 0x7F;
23
24pub fn check_transition(
33 current: CardLifeCycle,
34 target: CardLifeCycle,
35 force: bool,
36) -> Result<TransitionPlan, ScllError> {
37 use CardLifeCycle::{CardLocked, Initialized, OpReady, Secured, Terminated, Unknown};
38
39 if matches!(target, Terminated) {
41 return Err(ScllError::TerminateOutOfScope);
42 }
43 if matches!(target, Unknown(_)) {
45 return Err(ScllError::IllegalLifecycleTransition);
46 }
47 if current == target {
50 return Ok(TransitionPlan::NoOp);
51 }
52 if matches!(current, Terminated | Unknown(_)) {
55 return Err(ScllError::IllegalLifecycleTransition);
56 }
57
58 let p2 = match (current, target) {
59 (OpReady, Initialized) => P2_INITIALIZED, (Initialized | CardLocked, Secured) => P2_SECURED, (OpReady, Secured) if force => P2_SECURED, (Secured, CardLocked) => P2_CARD_LOCKED, _ => return Err(ScllError::IllegalLifecycleTransition), };
65 Ok(TransitionPlan::Apply { p2 })
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum TransitionPlan {
71 NoOp, Apply { p2: u8 }, }
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::report::CardLifeCycle::{
79 CardLocked, Initialized, OpReady, Secured, Terminated, Unknown,
80 };
81
82 const VALID: [CardLifeCycle; 4] = [OpReady, Initialized, Secured, CardLocked];
83
84 #[test]
85 fn terminated_is_never_a_target_under_any_force() {
86 for ¤t in &[
87 OpReady,
88 Initialized,
89 Secured,
90 CardLocked,
91 Terminated,
92 Unknown(0x42),
93 ] {
94 for force in [false, true] {
95 assert!(matches!(
96 check_transition(current, Terminated, force),
97 Err(ScllError::TerminateOutOfScope)
98 ));
99 }
100 }
101 }
102
103 #[test]
104 fn forward_provisioning_is_one_way() {
105 assert_eq!(
106 check_transition(OpReady, Initialized, false).unwrap(),
107 TransitionPlan::Apply { p2: 0x07 }
108 );
109 assert_eq!(
110 check_transition(Initialized, Secured, false).unwrap(),
111 TransitionPlan::Apply { p2: 0x0F }
112 );
113 for force in [false, true] {
115 for &(from, to) in &[
116 (Initialized, OpReady),
117 (Secured, Initialized),
118 (Secured, OpReady),
119 (CardLocked, Initialized),
120 (CardLocked, OpReady),
121 ] {
122 assert!(matches!(
123 check_transition(from, to, force),
124 Err(ScllError::IllegalLifecycleTransition)
125 ));
126 }
127 }
128 }
129
130 #[test]
131 fn skip_ahead_to_secured_requires_force() {
132 assert!(matches!(
133 check_transition(OpReady, Secured, false),
134 Err(ScllError::IllegalLifecycleTransition)
135 ));
136 assert_eq!(
137 check_transition(OpReady, Secured, true).unwrap(),
138 TransitionPlan::Apply { p2: 0x0F }
139 );
140 }
141
142 #[test]
143 fn lock_and_unlock_are_reversible() {
144 assert_eq!(
145 check_transition(Secured, CardLocked, false).unwrap(),
146 TransitionPlan::Apply { p2: 0x7F }
147 );
148 assert_eq!(
149 check_transition(CardLocked, Secured, false).unwrap(),
150 TransitionPlan::Apply { p2: 0x0F }
151 );
152 }
153
154 #[test]
155 fn same_state_is_a_no_op() {
156 for &s in &VALID {
157 assert_eq!(check_transition(s, s, false).unwrap(), TransitionPlan::NoOp);
158 }
159 }
160
161 #[test]
162 fn unknown_states_are_refused() {
163 assert!(matches!(
165 check_transition(Secured, Unknown(0x99), false),
166 Err(ScllError::IllegalLifecycleTransition)
167 ));
168 assert!(matches!(
170 check_transition(Unknown(0x99), Secured, true),
171 Err(ScllError::IllegalLifecycleTransition)
172 ));
173 }
174
175 #[test]
176 fn nothing_leaves_terminated() {
177 for &to in &VALID {
178 assert!(matches!(
179 check_transition(Terminated, to, true),
180 Err(ScllError::IllegalLifecycleTransition)
181 ));
182 }
183 }
184}