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_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 }