#![allow(unsafe_code)]
#![allow(clippy::unwrap_used, clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(missing_docs)]
#![allow(clippy::pedantic)]
#![allow(clippy::all)]
#![allow(deprecated)]
#![allow(dead_code)]
pub mod audit;
pub mod config;
pub mod downloader;
pub mod error;
pub mod keychain;
pub mod rollback;
pub use error::{SynqroError, SynqroStatus, SYNQRO_MAX_INPUT_LEN};
pub const SYNQRO_VERSION: &str = env!("CARGO_PKG_VERSION");
use std::sync::{Arc, OnceLock};
use crate::audit::AuditLogger;
use crate::config::SynqroConfig;
use crate::keychain::KeychainProvider;
struct SynqroEngine {
config: SynqroConfig,
audit: Arc<AuditLogger>,
keychain: Arc<dyn KeychainProvider>,
}
unsafe impl Send for SynqroEngine {}
unsafe impl Sync for SynqroEngine {}
static ENGINE: OnceLock<SynqroEngine> = OnceLock::new();
fn get_engine() -> Result<&'static SynqroEngine, SynqroError> {
ENGINE
.get()
.ok_or_else(|| SynqroError::Config("synqro_init() has not been called".into()))
}
pub mod ffi {
#![allow(unsafe_code)]
use std::ffi::{c_char, CStr, CString};
use std::path::Path;
use crate::audit::{AuditEvent, AuditLogger};
use crate::config::load_config;
use crate::error::{next_error_id, validate_input, SynqroError, SynqroResult, SynqroStatus};
use crate::keychain::platform_keychain;
use crate::{get_engine, SynqroEngine, ENGINE, SYNQRO_VERSION};
use std::sync::Arc;
unsafe fn decode_cstr(ptr: *const c_char, field: &str) -> Result<String, SynqroError> {
if ptr.is_null() {
return Err(SynqroError::InvalidInput(format!(
"argument `{}` must not be null",
field
)));
}
let cstr = unsafe { CStr::from_ptr(ptr) };
let s = cstr
.to_str()
.map_err(|_| SynqroError::InvalidInput(format!("`{}` is not valid UTF-8", field)))?;
validate_input(s)?;
Ok(s.to_owned())
}
fn catch_all(
f: impl FnOnce() -> Result<(), SynqroError> + std::panic::UnwindSafe,
) -> SynqroResult {
match std::panic::catch_unwind(f) {
Ok(Ok(())) => SynqroResult::ok(),
Ok(Err(e)) => SynqroResult::from_error(&e),
Err(_panic) => SynqroResult::err(
SynqroStatus::SynqroErrInternal,
"an unexpected internal error occurred",
next_error_id(),
),
}
}
#[no_mangle]
pub unsafe extern "C" fn synqro_init(config_path: *const c_char) -> SynqroResult {
catch_all(|| {
let path_str = unsafe { decode_cstr(config_path, "config_path")? };
let path = Path::new(&path_str);
let config = load_config(path)?;
let _tracing_guard = tracing_subscriber::fmt()
.json()
.with_current_span(false)
.try_init();
let keychain: Arc<dyn crate::keychain::KeychainProvider> =
Arc::from(platform_keychain()?);
let audit = Arc::new(AuditLogger::new(&config.logging, &config.installation_id)?);
ENGINE
.set(SynqroEngine {
config,
audit: Arc::clone(&audit),
keychain,
})
.map_err(|_| SynqroError::Config("synqro_init() called more than once".into()))?;
audit.log(
AuditEvent::SynqroInit,
serde_json::json!({ "version": SYNQRO_VERSION }),
)?;
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn synqro_check_update() -> SynqroResult {
catch_all(|| {
let engine = get_engine()?;
engine
.audit
.log(AuditEvent::UpdateCheckStarted, serde_json::json!({}))?;
let downloader = crate::downloader::SynqroDownloader::new(
&engine.config.update,
&engine.config.source,
&engine.config.installation_id,
&engine.config.crypto,
Arc::clone(&engine.audit),
)?;
match downloader.fetch_and_verify_manifest(engine.keychain.as_ref()) {
Ok(manifest) => {
engine.audit.log(
AuditEvent::UpdateAvailable,
serde_json::json!({ "version": manifest.version }),
)?;
Ok(())
}
Err(e) => Err(e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn synqro_apply_update() -> SynqroResult {
catch_all(|| {
let engine = get_engine()?;
let cfg = &engine.config;
let downloader = crate::downloader::SynqroDownloader::new(
&cfg.update,
&cfg.source,
&cfg.installation_id,
&cfg.crypto,
Arc::clone(&engine.audit),
)?;
let manifest = downloader.fetch_and_verify_manifest(engine.keychain.as_ref())?;
let backup_dir = std::path::PathBuf::from(&cfg.update.backup_dir);
let staging_dir = std::path::PathBuf::from(&cfg.update.staging_dir);
std::fs::create_dir_all(&staging_dir).map_err(|e| {
SynqroError::Permission(format!("Cannot create staging dir: {}", e))
})?;
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::new(Some(b"synqro-audit-v1"), cfg.installation_id.as_bytes());
let mut hmac_key = [0u8; 32];
hk.expand(b"hmac-key", &mut hmac_key)
.map_err(|e| SynqroError::Crypto(format!("HKDF expand failed: {}", e)))?;
let current_exe = std::env::current_exe().map_err(|e| {
SynqroError::Internal(format!("Cannot determine current exe: {}", e))
})?;
crate::rollback::take_snapshot(
&manifest.version,
&[current_exe],
&backup_dir,
&hmac_key,
)?;
let deployed_path = downloader.download_and_verify_artifact(
&manifest,
&staging_dir,
engine.keychain.as_ref(),
)?;
engine.audit.log(
AuditEvent::UpdateApplied,
serde_json::json!({
"version": manifest.version,
"path": deployed_path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default(),
}),
)?;
let grace = cfg.rollback.health_check_timeout_seconds;
let _ = crate::rollback::start_watchdog(std::process::id(), grace, grace / 2);
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn synqro_rollback() -> SynqroResult {
catch_all(|| {
let engine = get_engine()?;
let cfg = &engine.config;
engine
.audit
.log(AuditEvent::RollbackTriggered, serde_json::json!({}))?;
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::new(Some(b"synqro-audit-v1"), cfg.installation_id.as_bytes());
let mut hmac_key = [0u8; 32];
hk.expand(b"hmac-key", &mut hmac_key)
.map_err(|e| SynqroError::Crypto(format!("HKDF expand failed: {}", e)))?;
let backup_dir = std::path::PathBuf::from(&cfg.update.backup_dir);
let version = most_recent_backup_version(&backup_dir)?;
crate::rollback::rollback(&version, &backup_dir, &hmac_key, &engine.audit)?;
Ok(())
})
}
fn most_recent_backup_version(backup_dir: &std::path::Path) -> Result<String, SynqroError> {
let entries = std::fs::read_dir(backup_dir)
.map_err(|e| SynqroError::Rollback(format!("Cannot read backup dir: {}", e)))?;
let mut versions: Vec<String> = entries
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.strip_prefix('v').map(std::string::ToString::to_string)
})
.collect();
versions.sort_by(|a, b| {
b.cmp(a)
});
versions
.into_iter()
.next()
.ok_or_else(|| SynqroError::Rollback("No backup versions found".into()))
}
#[no_mangle]
pub extern "C" fn synqro_version() -> *const c_char {
static VERSION_CSTR: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
VERSION_CSTR
.get_or_init(|| {
CString::new(SYNQRO_VERSION).unwrap_or_else(|_| {
CString::new("unknown").unwrap_or_else(|_| CString::default())
})
})
.as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn synqro_installation_id() -> *mut c_char {
let Ok(engine) = get_engine() else {
return std::ptr::null_mut();
};
let Ok(cstring) = CString::new(engine.config.installation_id.as_str()) else {
return std::ptr::null_mut();
};
cstring.into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn synqro_free_string(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
unsafe {
drop(CString::from_raw(ptr));
}
}
#[no_mangle]
pub unsafe extern "C" fn synqro_free_result(result: *mut SynqroResult) {
if result.is_null() {
return;
}
let r = unsafe { &mut *result };
if !r.message.is_null() {
unsafe {
drop(CString::from_raw(r.message.cast_mut()));
}
r.message = std::ptr::null();
}
}
#[no_mangle]
pub unsafe extern "C" fn synqro_audit_event(
event_type: *const c_char,
data_json: *const c_char,
) -> SynqroResult {
catch_all(|| {
let event_name = unsafe { decode_cstr(event_type, "event_type")? };
let data: serde_json::Value = if data_json.is_null() {
serde_json::json!({})
} else {
let json_str = unsafe { decode_cstr(data_json, "data_json")? };
serde_json::from_str(&json_str).map_err(|e| {
SynqroError::InvalidInput(format!("data_json is not valid JSON: {}", e))
})?
};
let engine = get_engine()?;
let event = crate::audit::AuditEvent::from_str(&event_name).ok_or_else(|| {
SynqroError::InvalidInput(format!("Unknown audit event type: `{}`", event_name))
})?;
engine.audit.log(event, data)?;
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn synqro_health_check() -> SynqroResult {
catch_all(|| {
let engine = get_engine()?;
let downloader = crate::downloader::SynqroDownloader::new(
&engine.config.update,
&engine.config.source,
&engine.config.installation_id,
&engine.config.crypto,
Arc::clone(&engine.audit),
)?;
downloader.connectivity_check()?;
Ok(())
})
}
}
pub use ffi::*;