commit 255da079561540603b924321cebc47443aaa9a6e
parent 02aa4f43c1cb1022d1592379c00b86ea6b9af2b9
Author: triesap <tyson@radroots.org>
Date: Mon, 10 Aug 2026 21:59:13 +0000
mobile: expose verified media bytes to native hosts
Diffstat:
4 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/crates/mobile_core/src/runtime/product_surface/media.rs b/crates/mobile_core/src/runtime/product_surface/media.rs
@@ -549,6 +549,7 @@ impl Phase1VerifiedMediaReceipt {
pub struct Phase1LocalMediaArtifact {
artifact_id: Phase1MediaArtifactId,
local_path: PathBuf,
+ bytes: Vec<u8>,
byte_size: u64,
media_type: String,
width: u32,
@@ -580,6 +581,10 @@ impl Phase1LocalMediaArtifact {
self.local_path.as_path()
}
+ pub fn bytes(&self) -> &[u8] {
+ self.bytes.as_slice()
+ }
+
pub const fn byte_size(&self) -> u64 {
self.byte_size
}
@@ -1067,8 +1072,8 @@ pub(crate) async fn write_verified_artifact(
let final_path = artifact_path(directory, receipt.artifact_id, receipt.extension.as_str())?;
match tokio::fs::symlink_metadata(&final_path).await {
Ok(_) => {
- verify_artifact_file(&final_path, receipt).await?;
- return Ok(local_artifact(final_path, receipt));
+ let verified_bytes = verify_artifact_file(&final_path, receipt).await?;
+ return Ok(local_artifact(final_path, receipt, verified_bytes));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return Err(Phase1InboundMediaError::CacheIo),
@@ -1116,8 +1121,8 @@ pub(crate) async fn write_verified_artifact(
if write_result.is_err() {
let _ = tokio::fs::remove_file(&temporary_path).await;
}
- write_result?;
- Ok(local_artifact(final_path, receipt))
+ let verified_bytes = write_result?;
+ Ok(local_artifact(final_path, receipt, verified_bytes))
}
#[cfg(feature = "mobile-social")]
@@ -1150,8 +1155,8 @@ pub(crate) async fn verified_artifact(
receipt.validate_intrinsic()?;
ensure_cache_directory(directory).await?;
let path = artifact_path(directory, receipt.artifact_id, receipt.extension.as_str())?;
- verify_artifact_file(&path, receipt).await?;
- Ok(local_artifact(path, receipt))
+ let verified_bytes = verify_artifact_file(&path, receipt).await?;
+ Ok(local_artifact(path, receipt, verified_bytes))
}
#[cfg(feature = "mobile-social")]
@@ -1186,7 +1191,7 @@ fn artifact_path(
async fn verify_artifact_file(
path: &Path,
receipt: &Phase1VerifiedMediaReceipt,
-) -> Result<(), Phase1InboundMediaError> {
+) -> Result<Vec<u8>, Phase1InboundMediaError> {
let metadata = tokio::fs::symlink_metadata(path)
.await
.map_err(|_| Phase1InboundMediaError::CorruptArtifact)?;
@@ -1202,7 +1207,7 @@ async fn verify_artifact_file(
if Sha256::digest(bytes.as_slice()).to_hex() != receipt.observed_sha256 {
return Err(Phase1InboundMediaError::CorruptArtifact);
}
- Ok(())
+ Ok(bytes)
}
#[cfg(feature = "mobile-social")]
@@ -1218,10 +1223,12 @@ async fn sync_cache_directory(directory: &Path) -> Result<(), Phase1InboundMedia
fn local_artifact(
local_path: PathBuf,
receipt: &Phase1VerifiedMediaReceipt,
+ bytes: Vec<u8>,
) -> Phase1LocalMediaArtifact {
Phase1LocalMediaArtifact {
artifact_id: receipt.artifact_id,
local_path,
+ bytes,
byte_size: receipt.byte_size,
media_type: receipt.media_type.clone(),
width: receipt.width,
@@ -1549,6 +1556,7 @@ mod tests {
let left = left.unwrap();
let right = right.unwrap();
assert_eq!(left, right);
+ assert_eq!(left.bytes(), bytes);
assert_eq!(tokio::fs::read(left.local_path()).await.unwrap(), bytes);
assert_eq!(
std::fs::read_dir(&directory).unwrap().count(),
diff --git a/crates/mobile_core/src/runtime/product_surface/settings.rs b/crates/mobile_core/src/runtime/product_surface/settings.rs
@@ -1520,7 +1520,8 @@ mod tests {
#[test]
fn settings_reject_mixed_network_environments() {
- let settings = MobileSettings::default().with_blossom(BlossomPreferences::simulator_default());
+ let settings =
+ MobileSettings::default().with_blossom(BlossomPreferences::simulator_default());
assert_eq!(
ReplaceMobileSettings::new(settings.revision(), settings).unwrap_err(),
SettingsError::NetworkEnvironmentMismatch
diff --git a/crates/mobile_core/src/runtime/product_surface/today.rs b/crates/mobile_core/src/runtime/product_surface/today.rs
@@ -3209,6 +3209,7 @@ mod tests {
.starts_with(owner_directory.join("inbound_media.v1"))
);
assert_eq!(tokio::fs::read(artifact.local_path()).await.unwrap(), bytes);
+ assert_eq!(artifact.bytes(), bytes);
assert!(
!tokio::fs::symlink_metadata(artifact.local_path())
.await
diff --git a/crates/mobile_ffi/src/operations.rs b/crates/mobile_ffi/src/operations.rs
@@ -620,17 +620,34 @@ impl FfiMediaOperation {
}
}
-#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+#[derive(Clone, Eq, PartialEq, uniffi::Record)]
pub struct FfiVerifiedMediaArtifactRecord {
pub schema_version: u16,
pub operation_id: Option<String>,
pub artifact_id: String,
+ pub bytes: Vec<u8>,
pub byte_size: u64,
pub media_type: String,
pub width: u32,
pub height: u32,
}
+impl std::fmt::Debug for FfiVerifiedMediaArtifactRecord {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter
+ .debug_struct("FfiVerifiedMediaArtifactRecord")
+ .field("schema_version", &self.schema_version)
+ .field("operation_id", &self.operation_id)
+ .field("artifact_id", &self.artifact_id)
+ .field("bytes", &"<redacted>")
+ .field("byte_size", &self.byte_size)
+ .field("media_type", &self.media_type)
+ .field("width", &self.width)
+ .field("height", &self.height)
+ .finish()
+ }
+}
+
impl FfiVerifiedMediaArtifactRecord {
pub(crate) fn from_artifact(
value: Phase1LocalMediaArtifact,
@@ -640,6 +657,7 @@ impl FfiVerifiedMediaArtifactRecord {
schema_version: MOBILE_FFI_SCHEMA_VERSION,
operation_id,
artifact_id: value.artifact_id().to_hex(),
+ bytes: value.bytes().to_vec(),
byte_size: value.byte_size(),
media_type: value.media_type().to_owned(),
width: value.width(),
@@ -723,6 +741,23 @@ mod tests {
}
#[test]
+ fn verified_media_artifact_debug_never_exposes_renderable_bytes() {
+ let artifact = FfiVerifiedMediaArtifactRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ operation_id: None,
+ artifact_id: "11".repeat(32),
+ bytes: b"private farm image".to_vec(),
+ byte_size: 18,
+ media_type: "image/png".to_owned(),
+ width: 1,
+ height: 1,
+ };
+ let debug = format!("{artifact:?}");
+ assert!(debug.contains("<redacted>"));
+ assert!(!debug.contains("private farm image"));
+ }
+
+ #[test]
fn identity_commands_reject_fields_outside_the_selected_variant() {
let error = IdentityCommand::try_from(FfiIdentityCommandRecord {
schema_version: MOBILE_FFI_SCHEMA_VERSION,