#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![doc(html_root_url = "https://docs.smix.dev/smix-simctl")]
pub mod registry;
pub mod screenshot_pacer;
use screenshot_pacer::{ScreenshotPacer, ScreenshotPacerConfig};
use serde::{Deserialize, Serialize};
use std::io;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tokio::process::Command;
use tokio::time::sleep;
#[derive(Debug, Error)]
pub enum SimctlError {
#[error("spawn xcrun simctl failed: {0}")]
Spawn(#[from] io::Error),
#[error("xcrun simctl {subcommand} exited {code}: {stderr}")]
NonZeroExit {
subcommand: String,
code: i32,
stderr: String,
},
#[error("xcrun simctl {subcommand} returned malformed output: {detail}")]
Malformed {
subcommand: String,
detail: String,
},
#[error("xcrun simctl {subcommand} timed out after {ms}ms")]
Timeout {
subcommand: String,
ms: u64,
},
#[error("screenshot pacer circuit open; retry after {retry_after:?}")]
CaptureBackpressure {
retry_after: Duration,
},
}
#[derive(Debug)]
pub struct RecordingHandle {
pub(crate) child: tokio::process::Child,
pub path: String,
pub started_at: std::time::Instant,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimctlRuntime {
pub identifier: String,
pub name: String,
pub version: String,
pub is_available: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimctlDevice {
pub udid: String,
pub name: String,
pub state: String,
pub is_available: bool,
#[serde(rename = "deviceTypeIdentifier", default)]
pub device_type_identifier: String,
#[serde(rename = "runtimeIdentifier", default)]
pub runtime_identifier: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SimctlPermission {
Camera,
Photos,
Location,
LocationAlways,
Notifications,
Microphone,
Contacts,
Calendar,
Reminders,
Media,
Motion,
HomeKit,
Health,
Bluetooth,
Faceid,
AddressBook,
}
impl SimctlPermission {
pub fn as_str(self) -> &'static str {
match self {
SimctlPermission::Camera => "camera",
SimctlPermission::Photos => "photos",
SimctlPermission::Location => "location",
SimctlPermission::LocationAlways => "location-always",
SimctlPermission::Notifications => "notifications",
SimctlPermission::Microphone => "microphone",
SimctlPermission::Contacts => "contacts",
SimctlPermission::Calendar => "calendar",
SimctlPermission::Reminders => "reminders",
SimctlPermission::Media => "media-library",
SimctlPermission::Motion => "motion",
SimctlPermission::HomeKit => "homekit",
SimctlPermission::Health => "health",
SimctlPermission::Bluetooth => "bluetooth",
SimctlPermission::Faceid => "faceid",
SimctlPermission::AddressBook => "addressbook",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Appearance {
Light,
Dark,
}
impl Appearance {
pub fn as_str(self) -> &'static str {
match self {
Appearance::Light => "light",
Appearance::Dark => "dark",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LaunchResult {
pub pid: u32,
}
async fn simctl_capture(args: &[&str]) -> Result<(Vec<u8>, String), SimctlError> {
simctl_capture_env(args, &[]).await
}
async fn simctl_capture_env(
args: &[&str],
env: &[(String, String)],
) -> Result<(Vec<u8>, String), SimctlError> {
let mut cmd = Command::new("xcrun");
cmd.arg("simctl");
for a in args {
cmd.arg(a);
}
for (k, v) in env {
cmd.env(k, v);
}
let output = cmd.output().await?;
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
if !output.status.success() {
return Err(SimctlError::NonZeroExit {
subcommand: args.first().map(|s| s.to_string()).unwrap_or_default(),
code: output.status.code().unwrap_or(-1),
stderr,
});
}
Ok((output.stdout, stderr))
}
async fn simctl_run(args: &[&str]) -> Result<String, SimctlError> {
let (stdout, _) = simctl_capture(args).await?;
Ok(String::from_utf8_lossy(&stdout).into_owned())
}
async fn simctl_run_env(args: &[&str], env: &[(String, String)]) -> Result<String, SimctlError> {
let (stdout, _) = simctl_capture_env(args, env).await?;
Ok(String::from_utf8_lossy(&stdout).into_owned())
}
pub fn compose_child_env(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| {
let key = if k.starts_with("SIMCTL_CHILD_") {
(*k).to_string()
} else {
format!("SIMCTL_CHILD_{k}")
};
(key, (*v).to_string())
})
.collect()
}
#[derive(Debug)]
pub struct SimctlClient {
screenshot_pacer: Arc<std::sync::Mutex<ScreenshotPacer>>,
}
impl Default for SimctlClient {
fn default() -> Self {
Self::new()
}
}
impl SimctlClient {
pub fn new() -> Self {
SimctlClient {
screenshot_pacer: Arc::new(std::sync::Mutex::new(ScreenshotPacer::new(
ScreenshotPacerConfig::default(),
))),
}
}
#[must_use]
pub fn with_screenshot_pacer(self, config: ScreenshotPacerConfig) -> Self {
{
let mut guard = self
.screenshot_pacer
.lock()
.expect("screenshot pacer mutex must not be poisoned");
*guard = ScreenshotPacer::new(config);
}
self
}
#[must_use]
pub fn with_sim_health(self, monitor: smix_sim_health::SimHealthMonitor) -> Self {
{
let mut guard = self
.screenshot_pacer
.lock()
.expect("screenshot pacer mutex must not be poisoned");
guard.set_monitor(monitor);
}
self
}
pub async fn list_runtimes(&self) -> Result<Vec<SimctlRuntime>, SimctlError> {
let raw = simctl_run(&["list", "runtimes", "-j"]).await?;
#[derive(Deserialize)]
struct Wrap {
runtimes: Vec<RawRuntime>,
}
#[derive(Deserialize)]
struct RawRuntime {
identifier: String,
name: String,
version: String,
#[serde(rename = "isAvailable", default)]
is_available: bool,
}
let w: Wrap = serde_json::from_str(&raw).map_err(|e| SimctlError::Malformed {
subcommand: "list runtimes".into(),
detail: e.to_string(),
})?;
Ok(w.runtimes
.into_iter()
.map(|r| SimctlRuntime {
identifier: r.identifier,
name: r.name,
version: r.version,
is_available: r.is_available,
})
.collect())
}
pub async fn list_devices(&self) -> Result<Vec<SimctlDevice>, SimctlError> {
let raw = simctl_run(&["list", "devices", "-j"]).await?;
#[derive(Deserialize)]
struct Wrap {
devices: std::collections::BTreeMap<String, Vec<RawDevice>>,
}
#[derive(Deserialize)]
struct RawDevice {
udid: String,
name: String,
state: String,
#[serde(rename = "isAvailable", default)]
is_available: bool,
#[serde(rename = "deviceTypeIdentifier", default)]
device_type_identifier: String,
}
let w: Wrap = serde_json::from_str(&raw).map_err(|e| SimctlError::Malformed {
subcommand: "list devices".into(),
detail: e.to_string(),
})?;
let mut out = Vec::new();
for (runtime_id, devices) in w.devices {
for d in devices {
out.push(SimctlDevice {
udid: d.udid,
name: d.name,
state: d.state,
is_available: d.is_available,
device_type_identifier: d.device_type_identifier,
runtime_identifier: runtime_id.clone(),
});
}
}
Ok(out)
}
pub async fn boot(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["boot", udid]).await?;
Ok(())
}
pub async fn shutdown(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["shutdown", udid]).await?;
Ok(())
}
pub async fn current_locale(&self, udid: &str) -> Result<Option<String>, SimctlError> {
let out =
match simctl_run(&["spawn", udid, "defaults", "read", "-g", "AppleLanguages"]).await {
Ok(s) => s,
Err(SimctlError::NonZeroExit { .. }) => return Ok(None),
Err(e) => return Err(e),
};
if let Some(start) = out.find('"') {
let rest = &out[start + 1..];
if let Some(end) = rest.find('"') {
return Ok(Some(rest[..end].to_string()));
}
}
Ok(None)
}
pub async fn set_locale(&self, udid: &str, locale: &str) -> Result<(), SimctlError> {
simctl_run(&[
"spawn",
udid,
"defaults",
"write",
"-g",
"AppleLanguages",
"-array",
locale,
])
.await?;
let locale_underscore = locale.replace('-', "_");
simctl_run(&[
"spawn",
udid,
"defaults",
"write",
"-g",
"AppleLocale",
&locale_underscore,
])
.await?;
Ok(())
}
pub async fn boot_and_wait(&self, udid: &str, timeout: Duration) -> Result<(), SimctlError> {
let _ = simctl_run(&["boot", udid]).await;
let start = std::time::Instant::now();
loop {
let devices = self.list_devices().await?;
if devices
.iter()
.any(|d| d.udid == udid && d.state == "Booted")
{
return Ok(());
}
if start.elapsed() > timeout {
return Err(SimctlError::Timeout {
subcommand: format!("boot {}", udid),
ms: timeout.as_millis() as u64,
});
}
sleep(Duration::from_millis(500)).await;
}
}
pub async fn erase(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["erase", udid]).await?;
Ok(())
}
pub async fn install(&self, udid: &str, app_path: &str) -> Result<(), SimctlError> {
simctl_run(&["install", udid, app_path]).await?;
Ok(())
}
pub async fn uninstall(&self, udid: &str, bundle_id: &str) -> Result<(), SimctlError> {
simctl_run(&["uninstall", udid, bundle_id]).await?;
Ok(())
}
pub async fn terminate(&self, udid: &str, bundle_id: &str) -> Result<(), SimctlError> {
simctl_run(&["terminate", udid, bundle_id]).await?;
Ok(())
}
pub async fn launch(&self, udid: &str, bundle_id: &str) -> Result<LaunchResult, SimctlError> {
self.launch_with_args(udid, bundle_id, &[]).await
}
pub async fn launch_with_args(
&self,
udid: &str,
bundle_id: &str,
args: &[String],
) -> Result<LaunchResult, SimctlError> {
self.launch_with_args_and_env(udid, bundle_id, args, &[])
.await
}
pub async fn launch_with_args_and_env(
&self,
udid: &str,
bundle_id: &str,
args: &[String],
child_env: &[(&str, &str)],
) -> Result<LaunchResult, SimctlError> {
let mut argv: Vec<&str> = vec!["launch", udid, bundle_id];
if !args.is_empty() {
argv.push("--");
for a in args {
argv.push(a.as_str());
}
}
let composed = compose_child_env(child_env);
let out = simctl_run_env(&argv, &composed).await?;
let pid_str =
out.rsplit(':')
.next()
.map(str::trim)
.ok_or_else(|| SimctlError::Malformed {
subcommand: "launch".into(),
detail: format!("unexpected stdout shape: {}", out.trim()),
})?;
let pid: u32 = pid_str.parse().map_err(|_| SimctlError::Malformed {
subcommand: "launch".into(),
detail: format!("non-numeric pid in stdout: {}", out.trim()),
})?;
Ok(LaunchResult { pid })
}
pub async fn privacy_reset_all(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError> {
simctl_run(&["privacy", udid, "reset", "all", bundle_id]).await?;
Ok(())
}
pub async fn clear_app_sandbox(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError> {
let raw = simctl_run(&["get_app_container", udid, bundle_id, "data"]).await?;
let container = raw.trim();
if container.is_empty() {
return Err(SimctlError::Malformed {
subcommand: "clear_app_sandbox".into(),
detail: format!("empty Data container path for bundle {bundle_id}"),
});
}
let documents = format!("{container}/Documents");
let library = format!("{container}/Library");
let tmp = format!("{container}/tmp");
simctl_run(&[
"spawn", udid, "rm", "-rf", &documents, &library, &tmp,
])
.await?;
Ok(())
}
pub async fn open_url(&self, udid: &str, url: &str) -> Result<(), SimctlError> {
let argv = openurl_argv(udid, url);
let refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
simctl_run(&refs).await?;
Ok(())
}
}
#[doc(hidden)]
pub fn openurl_argv(udid: &str, url: &str) -> [String; 3] {
["openurl".to_string(), udid.to_string(), url.to_string()]
}
impl SimctlClient {
pub async fn send_push(
&self,
udid: &str,
bundle_id: &str,
apns_json_path: &str,
) -> Result<(), SimctlError> {
simctl_run(&["push", udid, bundle_id, apns_json_path]).await?;
Ok(())
}
pub async fn set_appearance(&self, udid: &str, mode: Appearance) -> Result<(), SimctlError> {
simctl_run(&["ui", udid, "appearance", mode.as_str()]).await?;
Ok(())
}
pub async fn grant_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError> {
simctl_run(&["privacy", udid, "grant", permission.as_str(), bundle_id]).await?;
Ok(())
}
pub async fn revoke_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError> {
simctl_run(&["privacy", udid, "revoke", permission.as_str(), bundle_id]).await?;
Ok(())
}
pub async fn location_set(
&self,
udid: &str,
latitude: f64,
longitude: f64,
) -> Result<(), SimctlError> {
let coord = format!("{latitude},{longitude}");
simctl_run(&["location", udid, "set", &coord]).await?;
Ok(())
}
pub async fn location_start(
&self,
udid: &str,
points: &[(f64, f64)],
speed_mps: Option<f64>,
) -> Result<(), SimctlError> {
if points.len() < 2 {
return Err(SimctlError::Malformed {
subcommand: "location-start".into(),
detail: format!("requires ≥2 waypoints, got {}", points.len()),
});
}
let mut args: Vec<String> = vec!["location".into(), udid.into(), "start".into()];
if let Some(s) = speed_mps {
args.push(format!("--speed={s}"));
}
for (lat, lng) in points {
args.push(format!("{lat},{lng}"));
}
let args_ref: Vec<&str> = args.iter().map(String::as_str).collect();
simctl_run(&args_ref).await?;
Ok(())
}
pub async fn location_clear(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["location", udid, "clear"]).await?;
Ok(())
}
pub async fn add_media(&self, udid: &str, paths: &[String]) -> Result<(), SimctlError> {
if paths.is_empty() {
return Err(SimctlError::Malformed {
subcommand: "addmedia".into(),
detail: "no paths supplied".into(),
});
}
let mut args: Vec<&str> = vec!["addmedia", udid];
for p in paths {
args.push(p.as_str());
}
simctl_run(&args).await?;
Ok(())
}
pub async fn record_video_start(
&self,
udid: &str,
path: &str,
) -> Result<RecordingHandle, SimctlError> {
let child = tokio::process::Command::new("xcrun")
.args(["simctl", "io", udid, "recordVideo", path])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(RecordingHandle {
child,
path: path.to_string(),
started_at: std::time::Instant::now(),
})
}
pub async fn record_video_stop(&self, mut handle: RecordingHandle) -> Result<(), SimctlError> {
let pid = handle.child.id().ok_or_else(|| SimctlError::Malformed {
subcommand: "recordVideo-stop".into(),
detail: "child already reaped".into(),
})?;
let rc = unsafe { libc::kill(pid as i32, libc::SIGINT) };
if rc != 0 {
return Err(SimctlError::Malformed {
subcommand: "recordVideo-stop".into(),
detail: format!(
"kill SIGINT failed: errno={}",
std::io::Error::last_os_error()
),
});
}
let wait_result =
tokio::time::timeout(std::time::Duration::from_secs(10), handle.child.wait()).await;
match wait_result {
Ok(Ok(_status)) => Ok(()),
Ok(Err(e)) => Err(SimctlError::Malformed {
subcommand: "recordVideo-stop".into(),
detail: format!("wait failed: {e}"),
}),
Err(_timeout) => {
let _ = handle.child.kill().await;
Err(SimctlError::Malformed {
subcommand: "recordVideo-stop".into(),
detail: "SIGINT timeout (10s) — escalated SIGKILL; output mp4 likely truncated. Inspect simctl recordVideo stderr.".into(),
})
}
}
}
pub async fn reset_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError> {
simctl_run(&["privacy", udid, "reset", permission.as_str(), bundle_id]).await?;
Ok(())
}
pub async fn keychain_reset(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["keychain", udid, "reset"]).await?;
Ok(())
}
pub async fn pasteboard_get(&self, udid: &str) -> Result<String, SimctlError> {
simctl_run(&["pbpaste", udid]).await
}
pub async fn pasteboard_set(&self, udid: &str, text: &str) -> Result<(), SimctlError> {
use tokio::io::AsyncWriteExt;
let mut cmd = Command::new("xcrun");
cmd.arg("simctl").arg("pbcopy").arg(udid);
cmd.stdin(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes()).await?;
drop(stdin); }
let status = child.wait().await?;
if !status.success() {
return Err(SimctlError::NonZeroExit {
subcommand: "pbcopy".into(),
code: status.code().unwrap_or(-1),
stderr: String::new(),
});
}
Ok(())
}
pub async fn set_reduce_motion(&self, udid: &str, enabled: bool) -> Result<(), SimctlError> {
let val = if enabled { "1" } else { "0" };
simctl_run(&[
"spawn",
udid,
"defaults",
"write",
"com.apple.UIKit",
"UIAccessibilityReduceMotionEnabled",
"-bool",
val,
])
.await?;
Ok(())
}
pub async fn screenshot(&self, udid: &str) -> Result<Vec<u8>, SimctlError> {
let wait = {
let mut pacer = self
.screenshot_pacer
.lock()
.expect("screenshot pacer mutex must not be poisoned");
pacer
.compute_wait()
.map_err(|retry_after| SimctlError::CaptureBackpressure { retry_after })?
};
if !wait.is_zero() {
sleep(wait).await;
}
let call_start = std::time::Instant::now();
let tmp =
std::env::temp_dir().join(format!("smix-screenshot-{udid}-{}.png", std::process::id()));
let tmp_str = tmp.display().to_string();
let result = simctl_capture(&["io", udid, "screenshot", &tmp_str]).await;
let bytes = result.and_then(|_| {
std::fs::read(&tmp).map_err(|e| SimctlError::Malformed {
subcommand: "screenshot".into(),
detail: format!("read {tmp_str}: {e}"),
})
});
let _ = std::fs::remove_file(&tmp);
let wall = call_start.elapsed();
let failed = bytes.is_err();
{
let mut pacer = self
.screenshot_pacer
.lock()
.expect("screenshot pacer mutex must not be poisoned");
pacer.record(wall, failed);
}
let bytes = bytes?;
if bytes.len() < 8 {
return Err(SimctlError::Malformed {
subcommand: "screenshot".into(),
detail: format!("screenshot file too short: {} bytes", bytes.len()),
});
}
Ok(ensure_srgb_chunk(bytes))
}
pub async fn create_device(
&self,
name: &str,
device_type: &str,
runtime_id: &str,
) -> Result<String, SimctlError> {
let out = simctl_run(&["create", name, device_type, runtime_id]).await?;
Ok(out.trim().to_string())
}
pub async fn delete_device(&self, udid: &str) -> Result<(), SimctlError> {
simctl_run(&["delete", udid]).await?;
Ok(())
}
}
const PNG_MAGIC: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
pub fn ensure_srgb_chunk(bytes: Vec<u8>) -> Vec<u8> {
if bytes.len() < 8 || &bytes[..8] != PNG_MAGIC {
return bytes;
}
let Some((idat_offset, has_srgb)) = scan_png_chunks(&bytes) else {
return bytes;
};
if has_srgb {
return bytes;
}
let mut out = Vec::with_capacity(bytes.len() + 13);
out.extend_from_slice(&bytes[..idat_offset]);
out.extend_from_slice(&synthesized_srgb_chunk());
out.extend_from_slice(&bytes[idat_offset..]);
out
}
fn scan_png_chunks(bytes: &[u8]) -> Option<(usize, bool)> {
let mut i: usize = 8;
let mut has_srgb = false;
while i + 8 <= bytes.len() {
let length =
u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]) as usize;
let ctype = &bytes[i + 4..i + 8];
if ctype == b"IDAT" {
return Some((i, has_srgb));
}
if ctype == b"sRGB" {
has_srgb = true;
}
let end = i.checked_add(12)?.checked_add(length)?;
if end > bytes.len() {
return None;
}
i = end;
}
None
}
fn synthesized_srgb_chunk() -> [u8; 13] {
let mut crc_input = [0u8; 5];
crc_input[0..4].copy_from_slice(b"sRGB");
crc_input[4] = 0; let crc = crc32_ieee(&crc_input);
let mut chunk = [0u8; 13];
chunk[0..4].copy_from_slice(&1u32.to_be_bytes()); chunk[4..8].copy_from_slice(b"sRGB");
chunk[8] = 0;
chunk[9..13].copy_from_slice(&crc.to_be_bytes());
chunk
}
fn crc32_ieee(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &b in bytes {
crc ^= u32::from(b);
for _ in 0..8 {
let mask = 0u32.wrapping_sub(crc & 1);
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compose_child_env_adds_prefix() {
let composed = compose_child_env(&[
("SMIX_PERF_RECEIVER_URL", "http://127.0.0.1:9999"),
("LAUNCH_FORCE_PUSH", "true"),
]);
assert_eq!(
composed,
vec![
(
"SIMCTL_CHILD_SMIX_PERF_RECEIVER_URL".to_string(),
"http://127.0.0.1:9999".to_string(),
),
(
"SIMCTL_CHILD_LAUNCH_FORCE_PUSH".to_string(),
"true".to_string(),
),
]
);
}
#[test]
fn compose_child_env_already_prefixed_passes_through() {
let composed = compose_child_env(&[("SIMCTL_CHILD_FOO", "bar")]);
assert_eq!(
composed,
vec![("SIMCTL_CHILD_FOO".to_string(), "bar".to_string())]
);
}
#[test]
fn compose_child_env_empty_input_is_empty_output() {
assert!(compose_child_env(&[]).is_empty());
}
#[test]
fn openurl_argv_preserves_url_verbatim() {
let udid = "12345678-1234-5678-1234-567812345678";
let url = "exp+focus-ai-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081";
let argv = super::openurl_argv(udid, url);
assert_eq!(argv[0], "openurl");
assert_eq!(argv[1], udid);
assert_eq!(argv[2], url);
assert!(argv[2].contains("?url="));
assert!(argv[2].contains("%3A"));
assert!(argv[2].contains("%2F"));
}
#[test]
fn openurl_argv_preserves_ampersand_and_hash() {
let udid = "12345678-1234-5678-1234-567812345678";
let url = "insight://dev-mutate?action=env&value=staging#anchor";
let argv = super::openurl_argv(udid, url);
assert_eq!(argv[2], url);
assert!(argv[2].contains('&'));
assert!(argv[2].contains('#'));
}
#[test]
fn openurl_argv_preserves_unicode() {
let udid = "12345678-1234-5678-1234-567812345678";
let url = "insight://route?name=%E7%94%B0%E4%B8%AD";
let argv = super::openurl_argv(udid, url);
assert_eq!(argv[2], url);
}
fn synth_png(with_srgb: bool) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(super::PNG_MAGIC);
let ihdr_data: [u8; 13] = [
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0,
];
emit_chunk(&mut out, b"IHDR", &ihdr_data);
if with_srgb {
emit_chunk(&mut out, b"sRGB", &[0]);
}
emit_chunk(&mut out, b"IDAT", &[0x78, 0x01, 0x00, 0x00]);
emit_chunk(&mut out, b"IEND", &[]);
out
}
fn emit_chunk(out: &mut Vec<u8>, ctype: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(ctype);
out.extend_from_slice(data);
let mut crc_in = Vec::with_capacity(4 + data.len());
crc_in.extend_from_slice(ctype);
crc_in.extend_from_slice(data);
out.extend_from_slice(&super::crc32_ieee(&crc_in).to_be_bytes());
}
#[test]
fn ensure_srgb_passthrough_when_chunk_present() {
let png = synth_png(true);
let original_len = png.len();
let out = super::ensure_srgb_chunk(png.clone());
assert_eq!(out.len(), original_len);
assert_eq!(out, png);
}
#[test]
fn ensure_srgb_inserts_chunk_when_absent() {
let png = synth_png(false);
let original_len = png.len();
let out = super::ensure_srgb_chunk(png);
assert_eq!(out.len(), original_len + 13);
assert_eq!(&out[..8], super::PNG_MAGIC);
let mut found = false;
for w in out.windows(4) {
if w == b"sRGB" {
found = true;
break;
}
}
assert!(found, "sRGB chunk should have been spliced in");
}
#[test]
fn ensure_srgb_preserves_idat_bytes_verbatim() {
let png = synth_png(false);
let out = super::ensure_srgb_chunk(png.clone());
assert_eq!(extract_idat_data(&png), extract_idat_data(&out));
}
fn extract_idat_data(bytes: &[u8]) -> Vec<u8> {
let mut i = 8;
while i + 8 <= bytes.len() {
let length =
u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]) as usize;
let ctype = &bytes[i + 4..i + 8];
if ctype == b"IDAT" {
return bytes[i + 8..i + 8 + length].to_vec();
}
i += 12 + length;
}
vec![]
}
#[test]
fn ensure_srgb_passthrough_on_bad_magic() {
let bytes = vec![0u8; 32];
let out = super::ensure_srgb_chunk(bytes.clone());
assert_eq!(out, bytes);
}
#[test]
fn crc32_matches_known_iend() {
assert_eq!(super::crc32_ieee(b"IEND"), 0xAE42_6082);
}
}