field_ios

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

media_ownership.rs (6567B)


      1 #![cfg(unix)]
      2 
      3 use radroots_blossom::Sha256;
      4 use std::io::{Read, Write};
      5 use std::os::fd::AsRawFd;
      6 use std::sync::Arc;
      7 use tera_ffi::{
      8     FfiAddCommandType, FfiAddDraftInput, FfiBlossomEndpointAuthority, FfiBlossomHostKind,
      9     FfiMediaFile, FfiPreparedMediaInput, MOBILE_FFI_SCHEMA_VERSION,
     10     PREPARED_MEDIA_FFI_SCHEMA_VERSION,
     11 };
     12 
     13 mod support;
     14 
     15 fn photo() -> (tempfile::NamedTempFile, FfiAddDraftInput) {
     16     let bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x02\0\0\0\x02";
     17     let mut original = tempfile::NamedTempFile::new().unwrap();
     18     original.write_all(bytes).unwrap();
     19     let file =
     20         Arc::new(FfiMediaFile::new(original.as_raw_fd() as u64, bytes.len() as u64).unwrap());
     21     let input = FfiAddDraftInput {
     22         schema_version: MOBILE_FFI_SCHEMA_VERSION,
     23         command_type: FfiAddCommandType::CreatePhotoUpdate,
     24         content: "Synthetic owned-file fixture".to_owned(),
     25         identifier: None,
     26         title: None,
     27         summary: None,
     28         location: None,
     29         event_timing: None,
     30         event_start_date: None,
     31         event_end_date: None,
     32         event_start_unix_s: None,
     33         event_end_unix_s: None,
     34         event_timezone: None,
     35         price_amount: None,
     36         currency: None,
     37         unit: None,
     38         quantity: None,
     39         food_published_at_unix_s: None,
     40         food_status: None,
     41         media: vec![FfiPreparedMediaInput {
     42             schema_version: PREPARED_MEDIA_FFI_SCHEMA_VERSION,
     43             opaque_reference: "media:owned-file".to_owned(),
     44             file,
     45             sha256: Sha256::digest(bytes).to_hex(),
     46             media_type: "image/png".to_owned(),
     47             byte_size: bytes.len() as u64,
     48             width: 2,
     49             height: 2,
     50             alt: "Synthetic photo".to_owned(),
     51             prepared_at_unix_s: 1_800_000_000,
     52         }],
     53     };
     54     (original, input)
     55 }
     56 
     57 #[tokio::test]
     58 async fn actual_runtime_reads_admitted_bytes_after_close_before_first_poll() {
     59     let (_root, runtime) = support::runtime().await;
     60     runtime
     61         .configure_blossom(
     62             FfiBlossomHostKind::Simulator,
     63             FfiBlossomEndpointAuthority::LoopbackDevelopment,
     64             "http://127.0.0.1:3000".to_owned(),
     65             vec![],
     66         )
     67         .await
     68         .unwrap();
     69     let (original, input) = photo();
     70     let digest = input.media[0].sha256.clone();
     71     let weak = Arc::downgrade(&input.media[0].file);
     72     let future = runtime.phase1_save_draft(
     73         "31".repeat(16),
     74         input,
     75         1_800_000_000,
     76         None,
     77         1_800_000_000_000,
     78     );
     79     drop(original);
     80     assert!(weak.upgrade().is_some());
     81     let saved = future
     82         .await
     83         .expect("owned file survives caller close before first poll");
     84     assert_eq!(saved.form.unwrap().media[0].sha256, digest);
     85     assert!(weak.upgrade().is_none());
     86     runtime.shutdown().await.unwrap();
     87 }
     88 
     89 #[tokio::test]
     90 async fn actual_runtime_ignores_reused_descriptor_before_first_poll() {
     91     let (_root, runtime) = support::runtime().await;
     92     runtime
     93         .configure_blossom(
     94             FfiBlossomHostKind::Simulator,
     95             FfiBlossomEndpointAuthority::LoopbackDevelopment,
     96             "http://127.0.0.1:3000".to_owned(),
     97             vec![],
     98         )
     99         .await
    100         .unwrap();
    101     let (mut original, input) = photo();
    102     let digest = input.media[0].sha256.clone();
    103     let future = runtime.phase1_save_draft(
    104         "32".repeat(16),
    105         input,
    106         1_800_000_000,
    107         None,
    108         1_800_000_000_000,
    109     );
    110     let mut replacement = tempfile::NamedTempFile::new().unwrap();
    111     replacement.write_all(b"wrong recycled descriptor").unwrap();
    112     let replacement = std::fs::File::open(replacement.path()).unwrap();
    113     // SAFETY: this test exclusively owns both live descriptors; original
    114     // remains the only RAII owner of the atomically replaced target slot.
    115     assert_eq!(
    116         unsafe { libc::dup2(replacement.as_raw_fd(), original.as_raw_fd()) },
    117         original.as_raw_fd()
    118     );
    119     let mut observed = String::new();
    120     original.read_to_string(&mut observed).unwrap();
    121     assert_eq!(observed, "wrong recycled descriptor");
    122     let saved = future
    123         .await
    124         .expect("reused caller descriptor cannot substitute bytes");
    125     assert_eq!(saved.form.unwrap().media[0].sha256, digest);
    126     runtime.shutdown().await.unwrap();
    127 }
    128 
    129 #[tokio::test]
    130 async fn cancellation_before_first_poll_releases_only_admitted_owner() {
    131     let (_root, runtime) = support::runtime().await;
    132     let (original, input) = photo();
    133     let weak = Arc::downgrade(&input.media[0].file);
    134     let future = runtime.phase1_save_draft(
    135         "33".repeat(16),
    136         input,
    137         1_800_000_000,
    138         None,
    139         1_800_000_000_000,
    140     );
    141     assert!(weak.upgrade().is_some());
    142     drop(future);
    143     assert!(weak.upgrade().is_none());
    144     assert_eq!(original.as_file().metadata().unwrap().len(), 24);
    145     assert!(runtime.phase1_draft_status("33".repeat(16)).await.is_err());
    146     runtime.shutdown().await.unwrap();
    147 }
    148 
    149 #[tokio::test]
    150 async fn actual_runtime_rejects_metadata_and_file_mutation_before_persistence() {
    151     let (_root, runtime) = support::runtime().await;
    152     runtime
    153         .configure_blossom(
    154             FfiBlossomHostKind::Simulator,
    155             FfiBlossomEndpointAuthority::LoopbackDevelopment,
    156             "http://127.0.0.1:3000".to_owned(),
    157             vec![],
    158         )
    159         .await
    160         .unwrap();
    161     for mutation in 0..7 {
    162         let (original, mut input) = photo();
    163         let media = &mut input.media[0];
    164         match mutation {
    165             0 => media.byte_size += 1,
    166             1 => media.sha256 = Sha256::digest(b"wrong").to_hex(),
    167             2 => media.media_type = "image/jpeg".into(),
    168             3 => media.width += 1,
    169             4 => media.height += 1,
    170             5 => {
    171                 use std::os::unix::fs::FileExt;
    172                 original.as_file().write_all_at(b"BAD", 0).unwrap();
    173             }
    174             _ => original.as_file().set_len(3).unwrap(),
    175         }
    176         let id = format!("{:032x}", mutation + 100);
    177         let error = runtime
    178             .phase1_save_draft(id.clone(), input, 1_800_000_000, None, 1_800_000_000_000)
    179             .await
    180             .unwrap_err();
    181         let expected = if mutation == 0 || mutation == 6 {
    182             "media_size_mismatch"
    183         } else {
    184             "media_verification_failed"
    185         };
    186         assert_eq!(error.report().code, expected, "mutation {mutation}");
    187         assert!(runtime.phase1_draft_status(id).await.is_err());
    188     }
    189     runtime.shutdown().await.unwrap();
    190 }