native/src/macos_bundle.rs
Rama main · réplica de lectura del repositorio original.
#![cfg_attr(not(target_os = "macos"), allow(dead_code))]
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub const BUNDLE_NAME: &str = "Origin Speak.app";
pub const BUNDLE_IDENTIFIER: &str = "com.originspeak.app";
pub const BUNDLE_EXECUTABLE_RELATIVE: &str = "Contents/MacOS/origin-runtime";
const STAGING_MARKER: &str = ".origin-speak-macos-bootstrap-v1";
const TRANSACTION_MARKER_PREFIX: &str = ".origin-speak-transaction-";
const TRANSACTION_MARKER_SUFFIX: &str = ".marker";
const TRANSACTION_MARKER_SCHEMA: &str = "Origin Speak macOS bundle transaction v1";
static STAGING_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Debug)]
struct TransactionPaths {
marker: PathBuf,
extraction_root: PathBuf,
staged_bundle: PathBuf,
backup_bundle: PathBuf,
}
impl TransactionPaths {
fn new(parent: &Path, token: &str) -> Self {
Self {
marker: parent.join(format!(
"{TRANSACTION_MARKER_PREFIX}{token}{TRANSACTION_MARKER_SUFFIX}"
)),
extraction_root: parent.join(format!(".origin-speak-extracting-{token}")),
staged_bundle: parent.join(format!(".origin-speak-installing-{token}.app")),
backup_bundle: parent.join(format!(".origin-speak-backup-{token}.app")),
}
}
}
#[derive(Debug)]
pub struct BundleSwap {
destination: PathBuf,
backup: Option<PathBuf>,
transaction_marker: Option<PathBuf>,
committed: bool,
}
impl BundleSwap {
pub fn rollback(&mut self) -> Result<(), String> {
if !self.committed {
return Ok(());
}
if let Some(backup) = self.backup.as_ref() {
self.validate_backup_ownership(backup)?;
} else if let Some(marker) = self.transaction_marker.as_ref() {
if transaction_from_marker(marker)?.is_none() {
return Err(format!(
"macOS bundle transaction marker is not owned by Origin Speak: {}",
marker.display()
));
}
}
if self.destination.exists() {
remove_tree_no_follow(&self.destination).map_err(|error| {
format!(
"remove replacement macOS runtime bundle {} during rollback: {error}",
self.destination.display()
)
})?;
}
if let Some(backup) = self.backup.as_ref() {
fs::rename(backup, &self.destination).map_err(|error| {
format!(
"restore macOS runtime bundle backup {} to {}: {error}",
backup.display(),
self.destination.display()
)
})?;
}
self.remove_transaction_marker()?;
self.committed = false;
Ok(())
}
pub fn finalize(&mut self) -> Result<(), String> {
if let Some(backup) = self.backup.as_ref() {
self.validate_backup_ownership(backup)?;
cleanup_ephemeral(backup)?;
self.backup = None;
}
self.remove_transaction_marker()?;
self.committed = false;
Ok(())
}
fn validate_backup_ownership(&self, backup: &Path) -> Result<(), String> {
if let Some(marker) = self.transaction_marker.as_ref() {
let transaction = transaction_from_marker(marker)?.ok_or_else(|| {
format!(
"macOS bundle transaction marker is not owned by Origin Speak: {}",
marker.display()
)
})?;
if transaction.backup_bundle != backup {
return Err(format!(
"macOS bundle backup is not claimed by transaction marker: {}",
backup.display()
));
}
}
Ok(())
}
fn remove_transaction_marker(&mut self) -> Result<(), String> {
let Some(marker) = self.transaction_marker.clone() else {
return Ok(());
};
let transaction = transaction_from_marker(&marker)?.ok_or_else(|| {
format!(
"refusing to remove unrecognized macOS bundle transaction marker {}",
marker.display()
)
})?;
if transaction.marker != marker {
return Err(format!(
"macOS bundle transaction marker path changed unexpectedly: {}",
marker.display()
));
}
fs::remove_file(&marker).map_err(|error| {
format!(
"remove completed macOS bundle transaction marker {}: {error}",
marker.display()
)
})?;
self.transaction_marker = None;
Ok(())
}
}
pub fn runtime_executable(bundle: &Path) -> PathBuf {
bundle.join(BUNDLE_EXECUTABLE_RELATIVE)
}
pub fn install_bundle_zip(archive: &Path, destination: &Path) -> Result<bool, String> {
let mut swap = transactional_install_bundle_zip(archive, destination)?;
swap.finalize()?;
Ok(true)
}
pub fn transactional_install_bundle_zip(
archive: &Path,
destination: &Path,
) -> Result<BundleSwap, String> {
validate_destination_shape(destination)?;
let archive_metadata = fs::symlink_metadata(archive).map_err(|error| {
format!(
"inspect macOS runtime archive {}: {error}",
archive.display()
)
})?;
if archive_metadata.file_type().is_symlink() || !archive_metadata.is_file() {
return Err(format!(
"macOS runtime bundle archive must be a regular file: {}",
archive.display()
));
}
let parent = destination
.parent()
.ok_or_else(|| "macOS runtime bundle has no parent directory".to_string())?;
fs::create_dir_all(parent)
.map_err(|error| format!("create macOS runtime parent {}: {error}", parent.display()))?;
let transaction = allocate_staging_paths(parent)?;
let extraction_root = &transaction.extraction_root;
let staged_bundle = &transaction.staged_bundle;
let backup_bundle = &transaction.backup_bundle;
let extraction = Command::new("/usr/bin/ditto")
.args(["-x", "-k"])
.arg(archive)
.arg(extraction_root)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|error| format!("run /usr/bin/ditto to extract runtime bundle: {error}"))?;
if !extraction.status.success() {
let _ = cleanup_owned_transaction(&transaction);
return Err(format!(
"extract macOS runtime bundle with /usr/bin/ditto: {}",
stderr_text(&extraction.stderr)
));
}
let extracted_bundle = match exact_extracted_bundle(&extraction_root) {
Ok(bundle) => bundle,
Err(error) => {
let _ = cleanup_owned_transaction(&transaction);
return Err(error);
}
};
if let Err(error) = fs::rename(&extracted_bundle, staged_bundle) {
let _ = cleanup_owned_transaction(&transaction);
return Err(format!(
"stage extracted macOS runtime bundle {}: {error}",
staged_bundle.display()
));
}
cleanup_owned_extraction(extraction_root)?;
if let Err(error) = verify_bundle(staged_bundle) {
let _ = cleanup_owned_transaction(&transaction);
return Err(error);
}
let had_existing = match fs::symlink_metadata(destination) {
Ok(metadata) => {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
let _ = cleanup_owned_transaction(&transaction);
return Err(format!(
"refusing to replace non-directory macOS runtime bundle path {}",
destination.display()
));
}
if let Err(error) = validate_bundle_identity(destination) {
let _ = cleanup_owned_transaction(&transaction);
return Err(error);
}
true
}
Err(error) if error.kind() == io::ErrorKind::NotFound => false,
Err(error) => {
let _ = cleanup_owned_transaction(&transaction);
return Err(format!(
"inspect installed macOS runtime bundle {}: {error}",
destination.display()
));
}
};
if had_existing {
fs::rename(destination, backup_bundle).map_err(|error| {
let _ = cleanup_owned_transaction(&transaction);
format!(
"move existing macOS runtime bundle to backup {}: {error}",
backup_bundle.display()
)
})?;
}
if let Err(error) = fs::rename(staged_bundle, destination) {
let rollback = if had_existing {
fs::rename(backup_bundle, destination).err()
} else {
None
};
if rollback.is_none() {
let _ = cleanup_owned_transaction(&transaction);
}
return Err(match rollback {
Some(rollback) => format!(
"install macOS runtime bundle {}: {error}; rollback also failed: {rollback}",
destination.display()
),
None => format!(
"install macOS runtime bundle {}: {error}",
destination.display()
),
});
}
if let Err(error) = verify_bundle(destination) {
let _ = remove_tree_no_follow(destination);
let rollback = if had_existing {
fs::rename(backup_bundle, destination).err()
} else {
None
};
if rollback.is_none() {
let _ = cleanup_owned_transaction(&transaction);
}
return Err(match rollback {
Some(rollback) => format!(
"installed macOS runtime bundle failed verification: {error}; rollback also failed: {rollback}"
),
None => format!("installed macOS runtime bundle failed verification: {error}"),
});
}
Ok(BundleSwap {
destination: destination.to_path_buf(),
backup: had_existing.then(|| backup_bundle.clone()),
transaction_marker: Some(transaction.marker.clone()),
committed: true,
})
}
pub fn verify_bundle(bundle: &Path) -> Result<(), String> {
validate_bundle_identity(bundle)?;
let status = Command::new("/usr/bin/codesign")
.args(["--verify", "--deep", "--strict"])
.arg(bundle)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|error| format!("run /usr/bin/codesign for {}: {error}", bundle.display()))?;
if !status.status.success() {
return Err(format!(
"codesign verification failed for {}: {}",
bundle.display(),
stderr_text(&status.stderr)
));
}
Ok(())
}
pub fn validate_bundle_identity(bundle: &Path) -> Result<(), String> {
let metadata = fs::symlink_metadata(bundle)
.map_err(|error| format!("inspect macOS runtime bundle {}: {error}", bundle.display()))?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(format!(
"macOS runtime bundle is not a real directory: {}",
bundle.display()
));
}
let info = bundle.join("Contents/Info.plist");
let executable = runtime_executable(bundle);
require_regular_file(&info, "Info.plist")?;
require_regular_file(&executable, "runtime executable")?;
let identifier = plist_value(&info, "CFBundleIdentifier")?;
if identifier != BUNDLE_IDENTIFIER {
return Err(format!(
"unexpected macOS runtime bundle identifier {identifier:?}; expected {BUNDLE_IDENTIFIER:?}"
));
}
let bundle_executable = plist_value(&info, "CFBundleExecutable")?;
if bundle_executable != "origin-runtime" {
return Err(format!(
"unexpected macOS runtime bundle executable {bundle_executable:?}; expected \"origin-runtime\""
));
}
let package_type = plist_value(&info, "CFBundlePackageType")?;
if package_type != "APPL" {
return Err(format!(
"unexpected macOS runtime bundle package type {package_type:?}; expected \"APPL\""
));
}
Ok(())
}
pub fn verify_runtime_executable(runtime: &Path) -> Result<(), String> {
let bundle = bundle_for_runtime(runtime)?;
verify_bundle(&bundle)
}
pub fn bundle_for_runtime(runtime: &Path) -> Result<PathBuf, String> {
let macos = runtime
.parent()
.ok_or_else(|| "macOS runtime executable has no MacOS directory".to_string())?;
let contents = macos
.parent()
.ok_or_else(|| "macOS runtime executable has no Contents directory".to_string())?;
let bundle = contents
.parent()
.ok_or_else(|| "macOS runtime executable has no app bundle directory".to_string())?;
if runtime_executable(bundle) != runtime
|| bundle.file_name().and_then(|name| name.to_str()) != Some(BUNDLE_NAME)
{
return Err(format!(
"runtime executable is not inside the expected {BUNDLE_NAME} bundle: {}",
runtime.display()
));
}
Ok(bundle.to_path_buf())
}
pub fn bundle_version(bundle: &Path) -> Result<String, String> {
validate_bundle_identity(bundle)?;
plist_value(
&bundle.join("Contents/Info.plist"),
"CFBundleShortVersionString",
)
}
pub fn remove_owned_bundle(bundle: &Path) -> Result<bool, String> {
validate_destination_shape(bundle)?;
match fs::symlink_metadata(bundle) {
Ok(metadata) => {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(format!(
"refusing to remove non-directory macOS runtime bundle path {}",
bundle.display()
));
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(format!(
"inspect macOS runtime bundle {}: {error}",
bundle.display()
));
}
}
validate_bundle_identity(bundle)?;
remove_tree_no_follow(bundle)
.map_err(|error| format!("remove macOS runtime bundle {}: {error}", bundle.display()))?;
Ok(true)
}
fn validate_destination_shape(destination: &Path) -> Result<(), String> {
if destination.file_name().and_then(|name| name.to_str()) != Some(BUNDLE_NAME) {
return Err(format!(
"refusing unexpected macOS runtime bundle path {}",
destination.display()
));
}
if destination.parent().is_none() {
return Err("macOS runtime bundle path has no parent directory".to_string());
}
Ok(())
}
pub fn recover_stale_transactions(destination: &Path) -> Result<(), String> {
recover_stale_transactions_with(destination, verify_bundle)
}
fn recover_stale_transactions_with<F>(destination: &Path, verify: F) -> Result<(), String>
where
F: Fn(&Path) -> Result<(), String>,
{
validate_destination_shape(destination)?;
let parent = destination
.parent()
.ok_or_else(|| "macOS runtime bundle has no parent directory".to_string())?;
let transactions = discover_owned_transactions(parent)?;
let owned_extractions = discover_owned_extractions(parent)?;
if transactions.is_empty() && owned_extractions.is_empty() {
return Ok(());
}
let live_exists = match fs::symlink_metadata(destination) {
Ok(metadata) => {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(format!(
"refusing macOS bundle recovery with non-directory live path {}",
destination.display()
));
}
true
}
Err(error) if error.kind() == io::ErrorKind::NotFound => false,
Err(error) => {
return Err(format!(
"inspect live macOS runtime bundle {} during recovery: {error}",
destination.display()
));
}
};
if live_exists {
verify(destination).map_err(|error| {
format!(
"live macOS runtime bundle failed verification during stale transaction cleanup; preserving recovery artifacts: {error}"
)
})?;
for transaction in &transactions {
cleanup_owned_transaction(transaction)?;
}
cleanup_owned_extractions(&owned_extractions)?;
return Ok(());
}
let mut verified_backups = Vec::new();
let mut rejected_backups = Vec::new();
for transaction in &transactions {
let metadata = match fs::symlink_metadata(&transaction.backup_bundle) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(format!(
"inspect owned macOS runtime backup {}: {error}",
transaction.backup_bundle.display()
));
}
};
if metadata.file_type().is_symlink() || !metadata.is_dir() {
rejected_backups.push(format!(
"{} is not a real directory",
transaction.backup_bundle.display()
));
continue;
}
match verify(&transaction.backup_bundle) {
Ok(()) => verified_backups.push(transaction.backup_bundle.clone()),
Err(error) => rejected_backups.push(format!(
"{} failed verification: {error}",
transaction.backup_bundle.display()
)),
}
}
if verified_backups.len() > 1 {
return Err(format!(
"found multiple verified owned macOS runtime backups while {} is missing; preserved all recovery artifacts for manual inspection",
destination.display()
));
}
if let Some(backup) = verified_backups.pop() {
fs::rename(&backup, destination).map_err(|error| {
format!(
"restore verified macOS runtime backup {} to {}: {error}",
backup.display(),
destination.display()
)
})?;
if let Err(error) = verify(destination) {
let rollback = fs::rename(destination, &backup);
return match rollback {
Ok(()) => Err(format!(
"restored macOS runtime backup failed verification and was returned to its backup path: {error}"
)),
Err(rollback_error) => Err(format!(
"restored macOS runtime backup failed verification ({error}); returning it to {} also failed ({rollback_error})",
backup.display()
)),
};
}
for transaction in &transactions {
cleanup_owned_transaction(transaction)?;
}
cleanup_owned_extractions(&owned_extractions)?;
return Ok(());
}
if !rejected_backups.is_empty() {
return Err(format!(
"owned macOS runtime backup artifacts exist while {} is missing, but none passed verification; preserved them for inspection: {}",
destination.display(),
rejected_backups.join("; ")
));
}
for transaction in &transactions {
cleanup_owned_transaction(transaction)?;
}
cleanup_owned_extractions(&owned_extractions)
}
fn discover_owned_transactions(parent: &Path) -> Result<Vec<TransactionPaths>, String> {
let entries = match fs::read_dir(parent) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(format!(
"inspect macOS runtime bundle parent {} for stale transactions: {error}",
parent.display()
));
}
};
let mut transactions = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| {
format!(
"inspect macOS runtime bundle parent entry in {}: {error}",
parent.display()
)
})?;
let path = entry.path();
if let Some(transaction) = transaction_from_marker(&path)? {
transactions.push(transaction);
}
}
Ok(transactions)
}
fn transaction_from_marker(marker: &Path) -> Result<Option<TransactionPaths>, String> {
let Some(file_name) = marker.file_name().and_then(|name| name.to_str()) else {
return Ok(None);
};
let Some(token) = file_name
.strip_prefix(TRANSACTION_MARKER_PREFIX)
.and_then(|name| name.strip_suffix(TRANSACTION_MARKER_SUFFIX))
else {
return Ok(None);
};
if !valid_transaction_token(token) {
return Ok(None);
}
let metadata = match fs::symlink_metadata(marker) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(format!(
"inspect macOS bundle transaction marker {}: {error}",
marker.display()
));
}
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Ok(None);
}
let parent = match marker.parent() {
Some(parent) => parent,
None => return Ok(None),
};
let transaction = TransactionPaths::new(parent, token);
if transaction.marker != marker {
return Ok(None);
}
let contents = fs::read_to_string(marker).map_err(|error| {
format!(
"read macOS bundle transaction marker {}: {error}",
marker.display()
)
})?;
if contents != transaction_marker_contents(token) {
return Ok(None);
}
Ok(Some(transaction))
}
fn valid_transaction_token(token: &str) -> bool {
let mut parts = token.split('-');
let valid_part =
|part: &str| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit());
matches!(
(parts.next(), parts.next(), parts.next(), parts.next()),
(Some(first), Some(second), Some(third), None)
if valid_part(first) && valid_part(second) && valid_part(third)
)
}
fn transaction_marker_contents(token: &str) -> String {
format!(
"{TRANSACTION_MARKER_SCHEMA}\ntoken={token}\ndestination={BUNDLE_NAME}\nextraction=.origin-speak-extracting-{token}\nstaged=.origin-speak-installing-{token}.app\nbackup=.origin-speak-backup-{token}.app\n"
)
}
fn cleanup_owned_transaction(transaction: &TransactionPaths) -> Result<(), String> {
let validated = transaction_from_marker(&transaction.marker)?.ok_or_else(|| {
format!(
"refusing to clean unrecognized macOS bundle transaction {}",
transaction.marker.display()
)
})?;
if validated.extraction_root != transaction.extraction_root
|| validated.staged_bundle != transaction.staged_bundle
|| validated.backup_bundle != transaction.backup_bundle
{
return Err(
"macOS bundle transaction marker no longer matches its artifact paths".to_string(),
);
}
if fs::symlink_metadata(&transaction.extraction_root).is_ok()
&& validate_staging_marker(&transaction.extraction_root).is_ok()
{
cleanup_owned_extraction(&transaction.extraction_root)?;
}
cleanup_ephemeral(&transaction.staged_bundle)?;
cleanup_ephemeral(&transaction.backup_bundle)?;
fs::remove_file(&transaction.marker).map_err(|error| {
format!(
"remove stale macOS bundle transaction marker {}: {error}",
transaction.marker.display()
)
})
}
fn discover_owned_extractions(parent: &Path) -> Result<Vec<PathBuf>, String> {
let entries = match fs::read_dir(parent) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(format!(
"inspect macOS runtime parent {} for stale extraction paths: {error}",
parent.display()
));
}
};
let mut owned = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| {
format!(
"inspect macOS runtime parent entry in {}: {error}",
parent.display()
)
})?;
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
let Some(token) = name.strip_prefix(".origin-speak-extracting-") else {
continue;
};
if !valid_transaction_token(token) || validate_staging_marker(&path).is_err() {
continue;
}
owned.push(path);
}
Ok(owned)
}
fn cleanup_owned_extractions(paths: &[PathBuf]) -> Result<(), String> {
for path in paths {
if fs::symlink_metadata(path).is_ok() && validate_staging_marker(path).is_ok() {
cleanup_owned_extraction(path)?;
}
}
Ok(())
}
fn exact_extracted_bundle(extraction_root: &Path) -> Result<PathBuf, String> {
validate_staging_marker(extraction_root)?;
let mut entries = fs::read_dir(extraction_root)
.map_err(|error| format!("inspect extracted macOS runtime archive: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("inspect extracted macOS runtime archive entry: {error}"))?
.into_iter()
.filter(|entry| entry.file_name().to_str() != Some(STAGING_MARKER))
.collect::<Vec<_>>();
if entries.len() != 1 {
return Err(format!(
"macOS runtime archive must contain exactly one top-level {BUNDLE_NAME} bundle"
));
}
let entry = entries.pop().expect("single extracted entry");
if entry.file_name().to_str() != Some(BUNDLE_NAME) {
return Err(format!(
"macOS runtime archive top-level entry must be {BUNDLE_NAME}"
));
}
Ok(entry.path())
}
fn allocate_staging_paths(parent: &Path) -> Result<TransactionPaths, String> {
for _ in 0..64 {
let sequence = STAGING_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
let token = format!("{}-{nanos}-{sequence}", std::process::id());
let transaction = TransactionPaths::new(parent, &token);
ensure_paths_absent([
transaction.marker.as_path(),
transaction.extraction_root.as_path(),
transaction.staged_bundle.as_path(),
transaction.backup_bundle.as_path(),
])?;
let marker_result = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&transaction.marker)
.and_then(|mut file| {
file.write_all(transaction_marker_contents(&token).as_bytes())?;
file.sync_all()
});
if let Err(error) = marker_result {
if error.kind() == io::ErrorKind::AlreadyExists {
continue;
}
return Err(format!(
"claim macOS bundle transaction marker {}: {error}",
transaction.marker.display()
));
}
match fs::create_dir(&transaction.extraction_root) {
Ok(()) => {
let marker = transaction.extraction_root.join(STAGING_MARKER);
let marker_result = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&marker)
.and_then(|mut file| {
file.write_all(b"Origin Speak macOS bootstrap staging v1\n")?;
file.sync_all()
});
if let Err(error) = marker_result {
let _ = fs::remove_file(&marker);
let _ = fs::remove_dir(&transaction.extraction_root);
let _ = fs::remove_file(&transaction.marker);
return Err(format!(
"claim macOS runtime staging directory {}: {error}",
transaction.extraction_root.display()
));
}
return Ok(transaction);
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
let _ = fs::remove_file(&transaction.marker);
continue;
}
Err(error) => {
let _ = fs::remove_file(&transaction.marker);
return Err(format!(
"create exclusive macOS runtime staging directory {}: {error}",
transaction.extraction_root.display()
));
}
}
}
Err("could not allocate an exclusive macOS runtime staging directory".to_string())
}
fn ensure_paths_absent<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(), String> {
for path in paths {
match fs::symlink_metadata(path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Ok(_) => {
return Err(format!(
"refusing pre-existing macOS bootstrap staging path {}",
path.display()
));
}
Err(error) => {
return Err(format!(
"inspect macOS bootstrap staging path {}: {error}",
path.display()
));
}
}
}
Ok(())
}
fn validate_staging_marker(extraction_root: &Path) -> Result<(), String> {
let marker = extraction_root.join(STAGING_MARKER);
let metadata = fs::symlink_metadata(&marker)
.map_err(|error| format!("inspect macOS staging ownership marker: {error}"))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(format!(
"macOS staging ownership marker is not a regular file: {}",
marker.display()
));
}
let contents = fs::read_to_string(&marker)
.map_err(|error| format!("read macOS staging ownership marker: {error}"))?;
if contents != "Origin Speak macOS bootstrap staging v1\n" {
return Err(format!(
"macOS staging ownership marker is invalid: {}",
marker.display()
));
}
Ok(())
}
fn cleanup_owned_extraction(path: &Path) -> Result<(), String> {
validate_staging_marker(path)?;
remove_tree_no_follow(path).map_err(|error| {
format!(
"remove owned macOS extraction path {}: {error}",
path.display()
)
})
}
fn plist_value(info: &Path, key: &str) -> Result<String, String> {
let output = Command::new("/usr/bin/plutil")
.args(["-extract", key, "raw", "-o", "-"])
.arg(info)
.stdin(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|error| format!("run /usr/bin/plutil for {key}: {error}"))?;
if !output.status.success() {
return Err(format!(
"read {key} from {}: {}",
info.display(),
stderr_text(&output.stderr)
));
}
String::from_utf8(output.stdout)
.map(|value| value.trim().to_string())
.map_err(|error| format!("decode {key} from {}: {error}", info.display()))
}
fn require_regular_file(path: &Path, label: &str) -> Result<(), String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect macOS bundle {label} {}: {error}", path.display()))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(format!(
"macOS bundle {label} is not a regular file: {}",
path.display()
));
}
#[cfg(unix)]
if label == "runtime executable" {
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 == 0 {
return Err(format!(
"macOS bundle runtime executable is not executable: {}",
path.display()
));
}
}
Ok(())
}
fn cleanup_ephemeral(path: &Path) -> Result<(), String> {
remove_tree_no_follow(path).map_err(|error| {
format!(
"remove temporary macOS bundle path {}: {error}",
path.display()
)
})
}
fn remove_tree_no_follow(path: &Path) -> io::Result<()> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error),
};
if metadata.file_type().is_symlink() {
return fs::remove_file(path).or_else(|_| fs::remove_dir(path));
}
if metadata.is_file() {
return fs::remove_file(path);
}
if !metadata.is_dir() {
return fs::remove_file(path);
}
for entry in fs::read_dir(path)? {
remove_tree_no_follow(&entry?.path())?;
}
fs::remove_dir(path)
}
fn stderr_text(bytes: &[u8]) -> String {
let text = String::from_utf8_lossy(bytes).trim().to_string();
if text.is_empty() {
"command failed without diagnostics".to_string()
} else {
text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_executable_is_inside_bundle() {
assert_eq!(
runtime_executable(Path::new("/Users/example/Applications/Origin Speak.app")),
Path::new("/Users/example/Applications/Origin Speak.app/Contents/MacOS/origin-runtime")
);
}
#[test]
fn bundle_is_recovered_from_runtime_path() {
let runtime =
Path::new("/Users/example/Applications/Origin Speak.app/Contents/MacOS/origin-runtime");
assert_eq!(
bundle_for_runtime(runtime).unwrap(),
Path::new("/Users/example/Applications/Origin Speak.app")
);
assert!(bundle_for_runtime(Path::new("/tmp/origin-runtime")).is_err());
}
#[test]
fn destination_must_be_exact_origin_speak_bundle_name() {
assert!(
validate_destination_shape(Path::new("/Users/example/Applications/Origin Speak.app"))
.is_ok()
);
assert!(
validate_destination_shape(Path::new("/Users/example/Applications/Other.app")).is_err()
);
}
#[test]
fn bundle_swap_rollback_restores_previous_directory() {
let root = std::env::temp_dir().join(format!(
"origin-speak-macos-bundle-rollback-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let live = root.join(BUNDLE_NAME);
let backup = root.join(".origin-speak-backup-test.app");
fs::create_dir(&live).unwrap();
fs::write(live.join("version"), b"new").unwrap();
fs::create_dir(&backup).unwrap();
fs::write(backup.join("version"), b"old").unwrap();
let mut swap = BundleSwap {
destination: live.clone(),
backup: Some(backup),
transaction_marker: None,
committed: true,
};
swap.rollback().unwrap();
assert_eq!(fs::read(live.join("version")).unwrap(), b"old");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn bundle_swap_finalize_removes_backup_directory() {
let root = std::env::temp_dir().join(format!(
"origin-speak-macos-bundle-finalize-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let live = root.join(BUNDLE_NAME);
let backup = root.join(".origin-speak-backup-test.app");
fs::create_dir(&live).unwrap();
fs::create_dir(&backup).unwrap();
fs::write(backup.join("version"), b"old").unwrap();
let mut swap = BundleSwap {
destination: live,
backup: Some(backup.clone()),
transaction_marker: None,
committed: true,
};
swap.finalize().unwrap();
assert!(!backup.exists());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn staging_collision_is_refused_without_deleting_existing_content() {
let root = std::env::temp_dir().join(format!(
"origin-speak-macos-bundle-collision-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let collision = root.join(".origin-speak-installing-collision.app");
fs::create_dir(&collision).unwrap();
fs::write(collision.join("keep.txt"), b"unrelated").unwrap();
assert!(ensure_paths_absent([collision.as_path()]).is_err());
assert_eq!(fs::read(collision.join("keep.txt")).unwrap(), b"unrelated");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn stale_transaction_recovers_exactly_one_verified_owned_backup() {
let root = test_root("recover-owned-backup");
let live = root.join(BUNDLE_NAME);
let transaction = create_test_transaction(&root, "1-2-3");
fs::create_dir(&transaction.backup_bundle).unwrap();
fs::write(transaction.backup_bundle.join("verified"), b"old").unwrap();
fs::create_dir(&transaction.staged_bundle).unwrap();
fs::write(transaction.staged_bundle.join("stale"), b"new").unwrap();
create_owned_extraction(&transaction.extraction_root);
recover_stale_transactions_with(&live, test_bundle_verifier).unwrap();
assert_eq!(fs::read(live.join("verified")).unwrap(), b"old");
assert!(!transaction.backup_bundle.exists());
assert!(!transaction.staged_bundle.exists());
assert!(!transaction.extraction_root.exists());
assert!(!transaction.marker.exists());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn stale_transaction_cleanup_keeps_verified_live_bundle() {
let root = test_root("cleanup-with-live");
let live = root.join(BUNDLE_NAME);
fs::create_dir(&live).unwrap();
fs::write(live.join("verified"), b"live").unwrap();
let transaction = create_test_transaction(&root, "4-5-6");
fs::create_dir(&transaction.backup_bundle).unwrap();
fs::write(transaction.backup_bundle.join("verified"), b"old").unwrap();
fs::create_dir(&transaction.staged_bundle).unwrap();
create_owned_extraction(&transaction.extraction_root);
recover_stale_transactions_with(&live, test_bundle_verifier).unwrap();
assert_eq!(fs::read(live.join("verified")).unwrap(), b"live");
assert!(!transaction.backup_bundle.exists());
assert!(!transaction.staged_bundle.exists());
assert!(!transaction.extraction_root.exists());
assert!(!transaction.marker.exists());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn stale_transaction_recovery_preserves_unowned_colliding_artifacts() {
let root = test_root("preserve-unowned-collisions");
let live = root.join(BUNDLE_NAME);
fs::create_dir(&live).unwrap();
fs::write(live.join("verified"), b"live").unwrap();
let backup = root.join(".origin-speak-backup-7-8-9.app");
let staged = root.join(".origin-speak-installing-7-8-9.app");
let extraction = root.join(".origin-speak-extracting-7-8-9");
fs::create_dir(&backup).unwrap();
fs::create_dir(&staged).unwrap();
fs::create_dir(&extraction).unwrap();
fs::write(backup.join("keep.txt"), b"backup").unwrap();
fs::write(staged.join("keep.txt"), b"staged").unwrap();
fs::write(extraction.join("keep.txt"), b"extraction").unwrap();
let fake_marker = root.join(".origin-speak-transaction-7-8-9.marker");
fs::write(&fake_marker, b"not Origin Speak transaction schema\n").unwrap();
recover_stale_transactions_with(&live, test_bundle_verifier).unwrap();
assert_eq!(fs::read(backup.join("keep.txt")).unwrap(), b"backup");
assert_eq!(fs::read(staged.join("keep.txt")).unwrap(), b"staged");
assert_eq!(
fs::read(extraction.join("keep.txt")).unwrap(),
b"extraction"
);
assert!(fake_marker.exists());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn stale_transaction_recovery_refuses_ambiguous_verified_backups() {
let root = test_root("ambiguous-backups");
let live = root.join(BUNDLE_NAME);
let first = create_test_transaction(&root, "10-11-12");
let second = create_test_transaction(&root, "13-14-15");
for transaction in [&first, &second] {
fs::create_dir(&transaction.backup_bundle).unwrap();
fs::write(transaction.backup_bundle.join("verified"), b"old").unwrap();
}
let error = recover_stale_transactions_with(&live, test_bundle_verifier).unwrap_err();
assert!(error.contains("multiple verified owned macOS runtime backups"));
assert!(!live.exists());
assert!(first.backup_bundle.exists());
assert!(second.backup_bundle.exists());
assert!(first.marker.exists());
assert!(second.marker.exists());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn archive_must_be_a_regular_file_before_any_staging_is_created() {
let root = std::env::temp_dir().join(format!(
"origin-speak-macos-bundle-archive-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let archive_directory = root.join("runtime.zip");
fs::create_dir(&archive_directory).unwrap();
let destination = root.join(BUNDLE_NAME);
assert!(transactional_install_bundle_zip(&archive_directory, &destination).is_err());
assert!(archive_directory.is_dir());
assert!(!destination.exists());
fs::remove_dir_all(root).unwrap();
}
fn test_root(label: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"origin-speak-macos-bundle-{label}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
root
}
fn create_test_transaction(root: &Path, token: &str) -> TransactionPaths {
let transaction = TransactionPaths::new(root, token);
fs::write(&transaction.marker, transaction_marker_contents(token)).unwrap();
transaction
}
fn create_owned_extraction(path: &Path) {
fs::create_dir(path).unwrap();
fs::write(
path.join(STAGING_MARKER),
b"Origin Speak macOS bootstrap staging v1\n",
)
.unwrap();
}
fn test_bundle_verifier(path: &Path) -> Result<(), String> {
if path.join("verified").is_file() {
Ok(())
} else {
Err(format!("test bundle is not verified: {}", path.display()))
}
}
}