commit 6373d4698488b161c2e87930e2ea0439be4e36b2
parent 34837875a75779076a945be3dde90966b6db94a3
Author: triesap <tyson@radroots.org>
Date: Thu, 6 Aug 2026 07:11:57 +0000
mobile: move global logging policy into mobileffi
- relocate host logging initialization and exported entry points to mobile_ffi
- relocate bounded rotation and filesystem policy with its attack tests
- remove tracing and logging dependencies from ordinary mobile_core builds
- preserve native error behavior and qualify the new boundary with strict checks
Diffstat:
5 files changed, 0 insertions(+), 533 deletions(-)
diff --git a/core/crates/tera_core/Cargo.toml b/core/crates/tera_core/Cargo.toml
@@ -34,11 +34,5 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
uniffi = { workspace = true }
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tracing = { workspace = true }
-tracing-appender = { workspace = true }
-tracing-subscriber = { workspace = true }
-
[dev-dependencies]
-tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
diff --git a/core/crates/tera_core/src/lib.rs b/core/crates/tera_core/src/lib.rs
@@ -7,8 +7,6 @@
uniffi::setup_scaffolding!("radroots_mobile_core");
pub mod error;
-#[cfg(not(target_arch = "wasm32"))]
-pub mod logging;
pub mod runtime;
pub use error::{RadrootsAppError, SdkErrorRecord};
diff --git a/core/crates/tera_core/src/logging.rs b/core/crates/tera_core/src/logging.rs
@@ -1,248 +0,0 @@
-mod writer;
-
-use std::fs;
-use std::path::{Component, Path, PathBuf};
-use std::sync::Mutex;
-
-use tracing_appender::non_blocking::WorkerGuard;
-use tracing_subscriber::prelude::*;
-
-use self::writer::{LogRotation, SizeRotatingWriter};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct LoggingOptions {
- dir: Option<PathBuf>,
- file_name: String,
- stdout: bool,
- rotation: LogRotation,
-}
-
-impl Default for LoggingOptions {
- fn default() -> Self {
- Self {
- dir: None,
- file_name: "radroots.log".to_owned(),
- stdout: true,
- rotation: LogRotation::default(),
- }
- }
-}
-
-struct ActiveLogging {
- options: LoggingOptions,
- _file_guard: Option<WorkerGuard>,
-}
-
-static LOGGING: Mutex<Option<ActiveLogging>> = Mutex::new(None);
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-pub fn init_logging(
- dir: Option<String>,
- file_name: Option<String>,
- is_stdout: Option<bool>,
-) -> Result<(), crate::RadrootsAppError> {
- let opts = logging_options(dir, file_name, is_stdout);
- initialize(opts).map_err(crate::RadrootsAppError::initialization)
-}
-
-fn logging_options(
- dir: Option<String>,
- file_name: Option<String>,
- is_stdout: Option<bool>,
-) -> LoggingOptions {
- LoggingOptions {
- dir: dir.map(PathBuf::from),
- file_name: file_name.unwrap_or_else(|| "radroots.log".to_string()),
- stdout: is_stdout.unwrap_or(true),
- ..LoggingOptions::default()
- }
-}
-
-fn initialize(options: LoggingOptions) -> Result<(), String> {
- validate_options(&options)?;
- let mut active = LOGGING
- .lock()
- .map_err(|_| "logging initialization state is poisoned".to_owned())?;
- if let Some(existing) = active.as_ref() {
- return if existing.options == options {
- Ok(())
- } else {
- Err("logging is already initialized with a different configuration".to_owned())
- };
- }
-
- let (file_writer, file_guard) = build_file_writer(&options)?;
- let file_layer = file_writer.as_ref().map(|writer| {
- tracing_subscriber::fmt::layer()
- .with_writer(writer.clone())
- .with_ansi(false)
- .with_target(false)
- });
- let stdout_layer = options.stdout.then(|| {
- tracing_subscriber::fmt::layer()
- .with_writer(std::io::stdout)
- .with_target(false)
- });
- tracing_subscriber::registry()
- .with(file_layer)
- .with(stdout_layer)
- .try_init()
- .map_err(|error| error.to_string())?;
- *active = Some(ActiveLogging {
- options,
- _file_guard: file_guard,
- });
- Ok(())
-}
-
-fn validate_options(options: &LoggingOptions) -> Result<(), String> {
- if options.dir.is_none() && !options.stdout {
- return Err("logging requires at least one configured output".to_owned());
- }
- if options.file_name.is_empty()
- || Path::new(&options.file_name).components().count() != 1
- || !matches!(
- Path::new(&options.file_name).components().next(),
- Some(Component::Normal(_))
- )
- {
- return Err("log file_name must be a safe basename".to_owned());
- }
- Ok(())
-}
-
-type FileWriter = tracing_appender::non_blocking::NonBlocking;
-
-fn build_file_writer(
- options: &LoggingOptions,
-) -> Result<(Option<FileWriter>, Option<WorkerGuard>), String> {
- let Some(dir) = options.dir.as_ref() else {
- return Ok((None, None));
- };
- fs::create_dir_all(dir).map_err(|error| error.to_string())?;
- let metadata = fs::symlink_metadata(dir).map_err(|error| error.to_string())?;
- if metadata.file_type().is_symlink() || !metadata.is_dir() {
- return Err("log directory is not a safe directory".to_owned());
- }
- let writer = SizeRotatingWriter::new(dir.join(&options.file_name), options.rotation)
- .map_err(|error| error.to_string())?;
- let (writer, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
- .buffered_lines_limit(8192)
- .lossy(false)
- .thread_name("radroots-app-log-writer")
- .finish(writer);
- Ok((Some(writer), Some(guard)))
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-pub fn init_logging_stdout() -> Result<(), crate::RadrootsAppError> {
- initialize(LoggingOptions::default()).map_err(crate::RadrootsAppError::initialization)
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-pub fn log_info(msg: String) -> Result<(), crate::RadrootsAppError> {
- tracing::info!("{msg}");
- Ok(())
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-pub fn log_error(msg: String) -> Result<(), crate::RadrootsAppError> {
- tracing::error!("{msg}");
- Ok(())
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-pub fn log_debug(msg: String) -> Result<(), crate::RadrootsAppError> {
- tracing::debug!("{msg}");
- Ok(())
-}
-
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-mod tests {
- use super::{
- LogRotation, LoggingOptions, build_file_writer, initialize, log_debug, log_error, log_info,
- logging_options, validate_options,
- };
- use std::path::PathBuf;
-
- #[test]
- fn logging_options_adopt_bounded_library_defaults() {
- let options = logging_options(
- Some("logs".to_owned()),
- Some("mobile.log".to_owned()),
- Some(false),
- );
-
- assert_eq!(options.dir, Some(PathBuf::from("logs")));
- assert_eq!(options.file_name, "mobile.log");
- assert!(!options.stdout);
- assert_eq!(options.rotation, LogRotation::default());
- }
-
- #[test]
- fn logging_options_preserve_public_api_defaults() {
- let options = logging_options(None, None, None);
-
- assert_eq!(options.file_name, "radroots.log");
- assert!(options.stdout);
- assert_eq!(options, LoggingOptions::default());
- }
-
- #[test]
- fn validation_rejects_missing_outputs_and_unsafe_names() {
- let mut options = LoggingOptions {
- stdout: false,
- ..LoggingOptions::default()
- };
- assert!(validate_options(&options).is_err());
-
- options.stdout = true;
- for name in ["", "../radroots.log", "nested/radroots.log", "."] {
- options.file_name = name.to_owned();
- assert!(validate_options(&options).is_err(), "accepted {name:?}");
- }
-
- options.dir = Some(PathBuf::from("logs"));
- options.file_name = "radroots.log".to_owned();
- options.stdout = false;
- validate_options(&options).expect("file output is sufficient");
- }
-
- #[test]
- fn file_writer_is_optional_and_rejects_non_directories() {
- let options = LoggingOptions::default();
- let (writer, guard) = build_file_writer(&options).expect("stdout only");
- assert!(writer.is_none());
- assert!(guard.is_none());
-
- let directory = tempfile::tempdir().expect("temporary directory");
- let file = directory.path().join("not-a-directory");
- std::fs::write(&file, b"not a directory").expect("fixture");
- let options = LoggingOptions {
- dir: Some(file),
- ..LoggingOptions::default()
- };
- assert!(build_file_writer(&options).is_err());
- }
-
- #[test]
- fn logging_entry_points_accept_secret_safe_messages() {
- log_info("info".to_owned()).expect("info");
- log_error("error".to_owned()).expect("error");
- log_debug("debug".to_owned()).expect("debug");
- }
-
- #[test]
- fn initialization_is_idempotent_but_rejects_reconfiguration() {
- let directory = tempfile::tempdir().expect("temporary directory");
- let options = LoggingOptions {
- dir: Some(directory.path().to_owned()),
- stdout: false,
- ..LoggingOptions::default()
- };
- initialize(options.clone()).expect("first initialization");
- initialize(options).expect("idempotent initialization");
- assert!(initialize(LoggingOptions::default()).is_err());
- }
-}
diff --git a/core/crates/tera_core/src/logging/writer.rs b/core/crates/tera_core/src/logging/writer.rs
@@ -1,268 +0,0 @@
-use std::fs::{self, File, OpenOptions};
-use std::io::{self, Write};
-use std::path::{Path, PathBuf};
-
-const DEFAULT_MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
-const DEFAULT_RETAINED_FILES: usize = 5;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(super) struct LogRotation {
- max_file_bytes: u64,
- retained_files: usize,
-}
-
-impl Default for LogRotation {
- fn default() -> Self {
- Self {
- max_file_bytes: DEFAULT_MAX_FILE_BYTES,
- retained_files: DEFAULT_RETAINED_FILES,
- }
- }
-}
-
-pub(super) struct SizeRotatingWriter {
- path: PathBuf,
- file: Option<File>,
- bytes_written: u64,
- policy: LogRotation,
-}
-
-impl SizeRotatingWriter {
- pub(super) fn new(path: PathBuf, policy: LogRotation) -> io::Result<Self> {
- reject_unsafe_target(&path)?;
- let file = open_append(&path)?;
- let bytes_written = file.metadata()?.len();
- let mut writer = Self {
- path,
- file: Some(file),
- bytes_written,
- policy,
- };
- if writer.bytes_written >= writer.policy.max_file_bytes {
- writer.rotate()?;
- }
- Ok(writer)
- }
-
- fn rotate(&mut self) -> io::Result<()> {
- if let Some(mut file) = self.file.take() {
- file.flush()?;
- file.sync_data()?;
- }
- if self.policy.retained_files == 1 {
- remove_if_present(&self.path)?;
- } else {
- remove_if_present(&rotated_path(&self.path, self.policy.retained_files - 1))?;
- for index in (2..self.policy.retained_files).rev() {
- rename_if_present(
- &rotated_path(&self.path, index - 1),
- &rotated_path(&self.path, index),
- )?;
- }
- rename_if_present(&self.path, &rotated_path(&self.path, 1))?;
- }
- self.file = Some(open_append(&self.path)?);
- self.bytes_written = 0;
- Ok(())
- }
-
- fn file_mut(&mut self) -> io::Result<&mut File> {
- self.file
- .as_mut()
- .ok_or_else(|| io::Error::other("log file is unavailable"))
- }
-}
-
-impl Write for SizeRotatingWriter {
- fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
- let incoming = u64::try_from(buffer.len())
- .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "log event is too large"))?;
- if incoming > self.policy.max_file_bytes {
- return Err(io::Error::new(
- io::ErrorKind::InvalidData,
- "log event exceeds the configured file limit",
- ));
- }
- if self.bytes_written > 0
- && self.bytes_written.saturating_add(incoming) > self.policy.max_file_bytes
- {
- self.rotate()?;
- }
- let written = self.file_mut()?.write(buffer)?;
- self.bytes_written = self
- .bytes_written
- .saturating_add(u64::try_from(written).unwrap_or(u64::MAX));
- Ok(written)
- }
-
- fn flush(&mut self) -> io::Result<()> {
- self.file_mut()?.flush()
- }
-}
-
-fn reject_unsafe_target(path: &Path) -> io::Result<()> {
- match fs::symlink_metadata(path) {
- Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => Err(
- io::Error::new(io::ErrorKind::InvalidInput, "log target is not a safe file"),
- ),
- Ok(_) => Ok(()),
- Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
- Err(error) => Err(error),
- }
-}
-
-fn open_append(path: &Path) -> io::Result<File> {
- let mut options = OpenOptions::new();
- options.create(true).append(true);
- #[cfg(unix)]
- {
- use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
- options.mode(0o600);
- let file = options.open(path)?;
- file.set_permissions(fs::Permissions::from_mode(0o600))?;
- Ok(file)
- }
- #[cfg(not(unix))]
- options.open(path)
-}
-
-fn rotated_path(path: &Path, index: usize) -> PathBuf {
- let mut value = path.as_os_str().to_owned();
- value.push(format!(".{index}"));
- PathBuf::from(value)
-}
-
-fn remove_if_present(path: &Path) -> io::Result<()> {
- match fs::remove_file(path) {
- Ok(()) => Ok(()),
- Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
- Err(error) => Err(error),
- }
-}
-
-fn rename_if_present(source: &Path, target: &Path) -> io::Result<()> {
- match fs::rename(source, target) {
- Ok(()) => Ok(()),
- Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
- Err(error) => Err(error),
- }
-}
-
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-mod tests {
- use super::{
- LogRotation, SizeRotatingWriter, reject_unsafe_target, remove_if_present,
- rename_if_present, rotated_path,
- };
- use std::io::Write;
-
- #[test]
- fn rotation_is_size_bounded_and_retention_is_finite() {
- let directory = tempfile::tempdir().expect("temporary log directory");
- let path = directory.path().join("radroots.log");
- let mut writer = SizeRotatingWriter::new(
- path.clone(),
- LogRotation {
- max_file_bytes: 5,
- retained_files: 3,
- },
- )
- .expect("writer");
- for value in [b"1111", b"2222", b"3333", b"4444"] {
- writer.write_all(value).expect("log entry");
- }
- writer.flush().expect("flush");
- assert_eq!(std::fs::read(&path).expect("current"), b"4444");
- assert_eq!(
- std::fs::read(rotated_path(&path, 1)).expect("first retained"),
- b"3333"
- );
- assert_eq!(
- std::fs::read(rotated_path(&path, 2)).expect("second retained"),
- b"2222"
- );
- assert!(!rotated_path(&path, 3).exists());
- }
-
- #[test]
- fn single_file_rotation_removes_the_previous_file() {
- let directory = tempfile::tempdir().expect("temporary log directory");
- let path = directory.path().join("radroots.log");
- std::fs::write(&path, b"full!").expect("fixture");
- let mut writer = SizeRotatingWriter::new(
- path.clone(),
- LogRotation {
- max_file_bytes: 5,
- retained_files: 1,
- },
- )
- .expect("writer");
- writer.write_all(b"next").expect("write");
- writer.flush().expect("flush");
- assert_eq!(std::fs::read(path).expect("current"), b"next");
- }
-
- #[test]
- fn oversized_events_and_unavailable_files_fail_closed() {
- let directory = tempfile::tempdir().expect("temporary log directory");
- let path = directory.path().join("radroots.log");
- let mut writer = SizeRotatingWriter::new(
- path,
- LogRotation {
- max_file_bytes: 3,
- retained_files: 2,
- },
- )
- .expect("writer");
- assert_eq!(
- writer.write(b"four").expect_err("oversized").kind(),
- std::io::ErrorKind::InvalidData
- );
- writer.write_all(b"a").expect("first short write");
- writer.write_all(b"b").expect("second short write");
- writer.file = None;
- assert_eq!(
- writer.flush().expect_err("missing file").kind(),
- std::io::ErrorKind::Other
- );
- }
-
- #[test]
- fn unsafe_targets_and_absent_rotation_files_are_handled() {
- let directory = tempfile::tempdir().expect("temporary directory");
- assert!(reject_unsafe_target(directory.path()).is_err());
- let missing = directory.path().join("missing");
- assert!(reject_unsafe_target(&missing).is_ok());
- assert!(remove_if_present(&missing).is_ok());
- assert!(rename_if_present(&missing, &directory.path().join("target")).is_ok());
- assert!(remove_if_present(directory.path()).is_err());
-
- let source = directory.path().join("source");
- std::fs::write(&source, b"source").expect("source");
- assert!(rename_if_present(&source, directory.path()).is_err());
-
- #[cfg(unix)]
- {
- std::os::unix::fs::symlink(directory.path(), &missing).expect("symlink");
- assert!(reject_unsafe_target(&missing).is_err());
- }
- }
-
- #[test]
- fn rotation_without_an_open_file_recreates_the_target() {
- let directory = tempfile::tempdir().expect("temporary directory");
- let path = directory.path().join("radroots.log");
- let mut writer = SizeRotatingWriter::new(
- path,
- LogRotation {
- max_file_bytes: 3,
- retained_files: 2,
- },
- )
- .expect("writer");
- writer.file = None;
- writer.rotate().expect("rotation");
- writer.write_all(b"ok").expect("write after rotation");
- }
-}
diff --git a/core/crates/tera_core/tests/logging_error.rs b/core/crates/tera_core/tests/logging_error.rs
@@ -1,9 +0,0 @@
-use radroots_mobile_core::RadrootsAppError;
-use radroots_mobile_core::logging;
-
-#[test]
-fn init_logging_stdout_maps_global_subscriber_error() {
- let _ = tracing_subscriber::fmt().try_init();
- let err = logging::init_logging_stdout();
- assert!(matches!(err, Err(RadrootsAppError::Initialization(_))));
-}