media_file.rs (5267B)
1 //! Synchronous admission of a native file into an independently owned resource. 2 //! Async callers carry this object, never a borrowed descriptor number. 3 4 use std::fs::File; 5 6 #[cfg(unix)] 7 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; 8 #[cfg(unix)] 9 use std::os::unix::fs::FileExt; 10 11 use crate::TeraAppError; 12 13 pub(crate) const MEDIA_FILE_MAX_BYTES: u64 = 10 * 1024 * 1024; 14 15 #[cfg(all(test, unix))] 16 #[path = "media_file_tests.rs"] 17 mod tests; 18 19 /// An immutable owner. Dropping a foreign reference cannot close a file still 20 /// retained by an admitted Rust future. No public operation closes other owners. 21 #[derive(uniffi::Object)] 22 pub struct FfiMediaFile { 23 file: File, 24 byte_size: u64, 25 } 26 27 impl std::fmt::Debug for FfiMediaFile { 28 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 formatter 30 .debug_struct("FfiMediaFile") 31 .field("byte_size", &self.byte_size) 32 .finish_non_exhaustive() 33 } 34 } 35 36 #[uniffi::export] 37 impl FfiMediaFile { 38 /// Acquire while the caller still owns the original descriptor. This is 39 /// deliberately synchronous; no file bytes are read or hashed at admission. 40 #[uniffi::constructor] 41 pub fn new(file_descriptor: u64, byte_size: u64) -> Result<Self, TeraAppError> { 42 if byte_size == 0 || byte_size > MEDIA_FILE_MAX_BYTES { 43 return Err(TeraAppError::invalid_argument("invalid_media_reference")); 44 } 45 let file = acquire(file_descriptor, byte_size)?; 46 Ok(Self { file, byte_size }) 47 } 48 } 49 50 impl FfiMediaFile { 51 #[cfg(unix)] 52 pub(crate) fn read(&self, expected_size: u64) -> Result<Vec<u8>, TeraAppError> { 53 self.read_with(expected_size, |file, bytes| file.read_exact_at(bytes, 0)) 54 } 55 56 #[cfg(unix)] 57 fn read_with( 58 &self, 59 expected_size: u64, 60 read: impl FnOnce(&File, &mut [u8]) -> std::io::Result<()>, 61 ) -> Result<Vec<u8>, TeraAppError> { 62 // File contents can change through another descriptor. Recheck the 63 // exact size and retain the existing downstream digest/image validation. 64 if expected_size != self.byte_size { 65 return Err(TeraAppError::invalid_argument("media_size_mismatch")); 66 } 67 validate_size(&self.file, expected_size)?; 68 let count = usize::try_from(expected_size) 69 .map_err(|_| TeraAppError::invalid_argument("media_size_mismatch"))?; 70 let mut bytes = vec![0; count]; 71 read(&self.file, &mut bytes) 72 .map_err(|_| TeraAppError::invalid_argument("media_read_failed"))?; 73 // A concurrent append must not turn a valid prefix into an admitted 74 // complete file. Downstream hashing binds the retained exact bytes; 75 // this second size check also rejects growth/shrink during the read. 76 validate_size(&self.file, expected_size)?; 77 Ok(bytes) 78 } 79 80 #[cfg(not(unix))] 81 pub(crate) fn read(&self, _expected_size: u64) -> Result<Vec<u8>, TeraAppError> { 82 Err(unsupported()) 83 } 84 } 85 86 #[cfg(unix)] 87 fn acquire(file_descriptor: u64, byte_size: u64) -> Result<File, TeraAppError> { 88 let original = RawFd::try_from(file_descriptor) 89 .map_err(|_| TeraAppError::invalid_argument("media_handle_unavailable"))?; 90 // SAFETY: fcntl accepts an integer descriptor and reports invalid handles. 91 // Only the successful duplicate becomes an owned Rust descriptor. 92 let duplicate = unsafe { libc::fcntl(original, libc::F_DUPFD_CLOEXEC, 0) }; 93 if duplicate < 0 { 94 return Err(TeraAppError::invalid_argument("media_handle_unavailable")); 95 } 96 // SAFETY: a successful F_DUPFD_CLOEXEC returns a new descriptor owned by 97 // this call. Every later failure drops File and releases that duplicate. 98 let file = File::from(unsafe { OwnedFd::from_raw_fd(duplicate) }); 99 validate_size(&file, byte_size)?; 100 // SAFETY: File owns a live descriptor. These calls inspect access and 101 // seekability without changing the shared file position. 102 let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) }; 103 if flags < 0 || flags & libc::O_ACCMODE == libc::O_WRONLY { 104 return Err(TeraAppError::invalid_argument("media_handle_unavailable")); 105 } 106 // SAFETY: the owned descriptor remains live and SEEK_CUR with offset zero 107 // leaves the caller's file position unchanged. 108 if unsafe { libc::lseek(file.as_raw_fd(), 0, libc::SEEK_CUR) } < 0 { 109 return Err(TeraAppError::invalid_argument("media_handle_unavailable")); 110 } 111 Ok(file) 112 } 113 114 #[cfg(unix)] 115 fn validate_size(file: &File, byte_size: u64) -> Result<(), TeraAppError> { 116 let metadata = file 117 .metadata() 118 .map_err(|_| TeraAppError::invalid_argument("media_handle_unavailable"))?; 119 if !metadata.is_file() || metadata.len() != byte_size { 120 return Err(TeraAppError::invalid_argument("media_size_mismatch")); 121 } 122 Ok(()) 123 } 124 125 #[cfg(not(unix))] 126 fn acquire(_file_descriptor: u64, _byte_size: u64) -> Result<File, TeraAppError> { 127 Err(unsupported()) 128 } 129 130 #[cfg(not(unix))] 131 fn unsupported() -> TeraAppError { 132 TeraAppError::failure( 133 "media_handle_unsupported", 134 "capability", 135 false, 136 &[], 137 "Protected media handles are unsupported on this platform.", 138 ) 139 }