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 }