field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

restore.rs (4440B)


      1 //! Native-owned recovery files and typed application restore commands.
      2 use crate::{FfiBackupManifest, FfiBackupRequest, TeraAppError};
      3 use tera_core::runtime::restore::{ApplicationRestoreGuard, RestoreError, RestoreRequest};
      4 
      5 mod adapter;
      6 pub(crate) use adapter::RestoreHostAdapter;
      7 mod records;
      8 pub use records::*;
      9 
     10 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     11 pub struct FfiRestoreRequest {
     12     pub attempt_id: String,
     13     pub backup: FfiBackupRequest,
     14     pub requested_at_ms: u64,
     15 }
     16 
     17 impl FfiRestoreRequest {
     18     pub(crate) fn decode(&self) -> Result<RestoreRequest, RestoreError> {
     19         RestoreRequest::new(
     20             bytes(&self.attempt_id)?,
     21             self.backup.decode()?,
     22             self.requested_at_ms,
     23         )
     24     }
     25 }
     26 
     27 impl From<&RestoreRequest> for FfiRestoreRequest {
     28     fn from(value: &RestoreRequest) -> Self {
     29         Self {
     30             attempt_id: hex::encode(value.attempt_id()),
     31             backup: value.backup().into(),
     32             requested_at_ms: value.requested_at_ms(),
     33         }
     34     }
     35 }
     36 
     37 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     38 pub struct FfiRestoreGuard {
     39     pub request: FfiRestoreRequest,
     40     pub relative_path: String,
     41     pub bytes: Vec<u8>,
     42 }
     43 
     44 impl TryFrom<ApplicationRestoreGuard> for FfiRestoreGuard {
     45     type Error = RestoreError;
     46     fn try_from(value: ApplicationRestoreGuard) -> Result<Self, Self::Error> {
     47         Ok(Self {
     48             request: value.request().into(),
     49             relative_path: relative_path(value.request().backup().author()),
     50             bytes: value.encode()?,
     51         })
     52     }
     53 }
     54 
     55 #[uniffi::export]
     56 pub fn application_restore_guard_limit() -> u64 {
     57     tera_core::runtime::restore::RESTORE_GUARD_MAX_BYTES as u64
     58 }
     59 
     60 #[uniffi::export]
     61 pub fn application_restore_guard_path(public_key: String) -> Result<String, TeraAppError> {
     62     let author = bytes::<32>(&public_key)?;
     63     // Reuse validated account construction without touching the filesystem.
     64     crate::backup::validate_application_backup_request(FfiBackupRequest {
     65         schema_version: tera_core::runtime::backup::APPLICATION_BACKUP_VERSION,
     66         backup_id: hex::encode([1; 16]),
     67         public_key,
     68         source_generation: hex::encode([1; 32]),
     69         requested_at_unix_ms: 1,
     70         maximum_bytes: 4096,
     71     })?;
     72     Ok(relative_path(author))
     73 }
     74 
     75 fn relative_path(author: [u8; 32]) -> String {
     76     format!(
     77         "radroots/users/{}/{}",
     78         hex::encode(author),
     79         tera_core::runtime::store::RESTORE_GUARD_FILENAME
     80     )
     81 }
     82 
     83 #[uniffi::export]
     84 pub fn validate_application_restore_guard(bytes: Vec<u8>) -> Result<FfiRestoreGuard, TeraAppError> {
     85     ApplicationRestoreGuard::decode(&bytes)?
     86         .try_into()
     87         .map_err(Into::into)
     88 }
     89 
     90 #[uniffi::export(callback_interface)]
     91 #[async_trait::async_trait]
     92 pub trait TeraRestoreHost: Send + Sync {
     93     async fn require_quiescent(&self) -> Result<(), TeraAppError>;
     94     async fn load_completed(&self, request: FfiRestoreRequest) -> Result<Vec<u8>, TeraAppError>;
     95     async fn restore_media(&self, manifest: FfiBackupManifest) -> Result<(), TeraAppError>;
     96     async fn arm_guard(&self, guard: FfiRestoreGuard) -> Result<(), TeraAppError>;
     97 }
     98 
     99 pub(crate) fn bytes<const N: usize>(value: &str) -> Result<[u8; N], RestoreError> {
    100     if value.len() != N * 2
    101         || !value
    102             .bytes()
    103             .all(|v| v.is_ascii_digit() || (b'a'..=b'f').contains(&v))
    104     {
    105         return Err(RestoreError::InvalidRequest);
    106     }
    107     let mut bytes = [0; N];
    108     hex::decode_to_slice(value, &mut bytes).map_err(|_| RestoreError::InvalidRequest)?;
    109     Ok(bytes)
    110 }
    111 
    112 impl From<RestoreError> for TeraAppError {
    113     fn from(value: RestoreError) -> Self {
    114         Self::failure(
    115             value.code(),
    116             "restore",
    117             matches!(value, RestoreError::Busy | RestoreError::Unavailable),
    118             &["review_restore"],
    119             &value.to_string(),
    120         )
    121     }
    122 }
    123 
    124 fn host_error(value: TeraAppError) -> RestoreError {
    125     match value.report().code.as_str() {
    126         "restore_busy" => RestoreError::Busy,
    127         "restore_identity_mismatch" => RestoreError::IdentityMismatch,
    128         "restore_generation_mismatch" => RestoreError::GenerationMismatch,
    129         "restore_media_unavailable" => RestoreError::MediaUnavailable,
    130         "restore_capacity_exceeded" => RestoreError::CapacityExceeded,
    131         "restore_conflict" => RestoreError::Conflict,
    132         _ => RestoreError::RecoveryRequired,
    133     }
    134 }