recovery_schedule.rs (3738B)
1 use crate::{TeraAppError, TeraRuntime}; 2 use tera_core::runtime::product_surface::recovery_schedule::NativeRecoverySchedule; 3 4 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 5 pub struct FfiNativeRecoverySchedule { 6 pub schema_version: u16, 7 pub author: String, 8 pub revision: u64, 9 pub after: Option<String>, 10 } 11 12 impl From<NativeRecoverySchedule> for FfiNativeRecoverySchedule { 13 fn from(value: NativeRecoverySchedule) -> Self { 14 Self { 15 schema_version: value.schema_version, 16 author: hex::encode(value.author), 17 revision: value.revision, 18 after: value.after.map(hex::encode), 19 } 20 } 21 } 22 23 fn digest(value: &str) -> Result<[u8; 32], TeraAppError> { 24 let invalid = || TeraAppError::invalid_argument("invalid_recovery_schedule"); 25 if value.len() != 64 26 || !value 27 .bytes() 28 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) 29 { 30 return Err(invalid()); 31 } 32 let mut bytes = [0; 32]; 33 hex::decode_to_slice(value, &mut bytes).map_err(|_| invalid())?; 34 Ok(bytes) 35 } 36 37 impl TryFrom<FfiNativeRecoverySchedule> for NativeRecoverySchedule { 38 type Error = TeraAppError; 39 fn try_from(value: FfiNativeRecoverySchedule) -> Result<Self, Self::Error> { 40 if value.schema_version != 1 41 || value.revision > i64::MAX as u64 42 || (value.revision == 0 && value.after.is_some()) 43 { 44 return Err(TeraAppError::invalid_argument("invalid_recovery_schedule")); 45 } 46 Ok(Self { 47 schema_version: 1, 48 author: digest(&value.author)?, 49 revision: value.revision, 50 after: value.after.as_deref().map(digest).transpose()?, 51 }) 52 } 53 } 54 55 #[cfg_attr(not(coverage_nightly), uniffi::export(async_runtime = "tokio"))] 56 impl TeraRuntime { 57 pub async fn native_recovery_schedule( 58 &self, 59 schema_version: u16, 60 ) -> Result<FfiNativeRecoverySchedule, TeraAppError> { 61 if schema_version != 1 { 62 return Err(TeraAppError::invalid_argument("invalid_recovery_schedule")); 63 } 64 self.inner 65 .native_recovery_schedule() 66 .await 67 .map(Into::into) 68 .map_err(Into::into) 69 } 70 71 pub async fn advance_native_recovery_schedule( 72 &self, 73 expected: FfiNativeRecoverySchedule, 74 after: Option<String>, 75 ) -> Result<FfiNativeRecoverySchedule, TeraAppError> { 76 self.inner 77 .advance_native_recovery_schedule( 78 expected.try_into()?, 79 after.as_deref().map(digest).transpose()?, 80 ) 81 .await 82 .map(Into::into) 83 .map_err(Into::into) 84 } 85 } 86 87 #[cfg(test)] 88 mod tests { 89 use super::*; 90 #[test] 91 fn validates_versioned_bounded_canonical_cursors() { 92 let initial = FfiNativeRecoverySchedule { 93 schema_version: 1, 94 author: "ab".repeat(32), 95 revision: 0, 96 after: None, 97 }; 98 assert!(NativeRecoverySchedule::try_from(initial.clone()).is_ok()); 99 for value in ["", &"A".repeat(64), &"0".repeat(65), &"é".repeat(32)] { 100 assert!(digest(value).is_err()); 101 } 102 for value in [ 103 FfiNativeRecoverySchedule { 104 schema_version: 2, 105 ..initial.clone() 106 }, 107 FfiNativeRecoverySchedule { 108 revision: u64::MAX, 109 ..initial.clone() 110 }, 111 FfiNativeRecoverySchedule { 112 after: Some("00".repeat(32)), 113 ..initial 114 }, 115 ] { 116 assert!(NativeRecoverySchedule::try_from(value).is_err()); 117 } 118 } 119 }