pub mod app; pub mod audio; pub mod avatar; pub mod background; pub mod codec; pub mod config; pub mod core; pub mod discovery; pub mod dsp; pub mod files; pub mod friends; pub mod game; pub mod hotkeys; pub mod identity; pub mod network; pub mod notify; pub mod playlist; pub mod presence; pub mod presence_net; pub mod protocol; pub mod recents; pub mod sanitize; pub mod screenshare; pub mod theme; pub mod widget; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::OnceLock; const LOG_MAX_BYTES: u64 = 5 * 1024 * 1024; // Owner-only log permissions are a Unix concept (mode bits); on Windows the log // inherits the directory's default ACL. Only referenced under `cfg(unix)`. #[cfg(unix)] const LOG_MODE: u32 = 0o600; /// Resolves the log file path once: `$XDG_STATE_HOME/peerspeak/peerspeak.log` /// (via `dirs::state_dir`), falling back to the system temp dir. Computed lazily /// so we never hardcode a per-user path. fn log_path() -> &'static PathBuf { static LOG_PATH: OnceLock = OnceLock::new(); LOG_PATH.get_or_init(|| { let mut dir = dirs::state_dir() .or_else(dirs::cache_dir) .unwrap_or_else(std::env::temp_dir); dir.push("peerspeak"); let _ = std::fs::create_dir_all(&dir); dir.push("peerspeak.log"); dir }) } /// The resolved log file path. Exposed so diagnostic tools (e.g. the audio /// probe) can tail the same log the app writes to. pub fn log_file_path() -> PathBuf { log_path().clone() } /// Short, human-matchable id prefix for diagnostics. Never use this where the /// full value is needed for protocol behavior. pub fn short_id(id: &str) -> String { id.chars().take(8).collect() } /// Redact a capability-bearing value for logs while keeping a tiny prefix for /// support correlation. Tickets and endpoint addresses are bearer capabilities: /// logging the full string is equivalent to leaking the room/share. pub fn redact_for_log(value: &str) -> String { let value = value.trim(); if value.is_empty() { "".to_string() } else { format!("", short_id(value)) } } pub fn short_bytes_hex(bytes: &[u8]) -> String { bytes .iter() .take(6) .map(|b| format!("{b:02x}")) .collect::>() .join("") } fn rotated_log_path(path: &Path) -> PathBuf { let file_name = path .file_name() .and_then(|n| n.to_str()) .unwrap_or("peerspeak.log"); path.with_file_name(format!("{file_name}.1")) } fn prepare_log_file(path: &Path) -> std::io::Result { prepare_log_file_with_limit(path, LOG_MAX_BYTES) } fn prepare_log_file_with_limit(path: &Path, max_bytes: u64) -> std::io::Result { if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } if std::fs::metadata(path).is_ok_and(|m| m.len() > max_bytes) { let rotated = rotated_log_path(path); let _ = std::fs::remove_file(&rotated); if std::fs::rename(path, &rotated).is_err() { let _ = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(path); } } let mut opts = std::fs::OpenOptions::new(); opts.create(true).append(true); // The log can carry capability-bearing values (redacted, but still): keep it // owner-only on Unix via the open mode. Windows has no mode bits; it inherits // the directory ACL, so this hardening is Unix-only. #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; opts.mode(LOG_MODE); } let file = opts.open(path)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // Re-assert the mode in case the file pre-existed with looser perms. let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(LOG_MODE)); } Ok(file) } pub fn log_msg(msg: &str) { // Format the whole line into one buffer first, then emit it with a single // `write_all`. The file is opened with `O_APPEND`, so a lone `write()` is // atomic w.r.t. concurrent writers; building the line up front avoids the // multi-syscall `writeln!` path that would let threads interleave fragments. let line = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { Ok(time) => format!("[{}.{:03}] {}\n", time.as_secs(), time.subsec_millis(), msg), Err(_) => format!("{}\n", msg), }; if let Ok(mut file) = prepare_log_file(log_path()) { use std::io::Write; let _ = file.write_all(line.as_bytes()); } } #[cfg(test)] mod tests { use super::*; use std::io::Write; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; fn temp_log_dir() -> PathBuf { let stamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); std::env::temp_dir().join(format!("peerspeak-log-test-{}-{stamp}", std::process::id())) } #[test] fn redaction_keeps_only_a_short_prefix() { let secret = "abcdefghijklmnopqrstuvwxyz"; let redacted = redact_for_log(secret); assert!(redacted.contains("abcdefgh")); assert!(!redacted.contains("ijklmnopqrstuvwxyz")); assert_eq!(redact_for_log(" "), ""); } // Owner-only log perms are a Unix concept; on Windows the file inherits the // directory ACL and there's no mode to assert. #[cfg(unix)] #[test] fn log_file_is_created_private() { let dir = temp_log_dir(); let path = dir.join("peerspeak.log"); let _file = prepare_log_file(&path).unwrap(); let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777; assert_eq!(mode, LOG_MODE); let _ = std::fs::remove_dir_all(dir); } #[test] fn oversized_log_is_rotated_on_open() { let dir = temp_log_dir(); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("peerspeak.log"); { let mut file = std::fs::File::create(&path).unwrap(); file.write_all(b"oversized").unwrap(); } let _file = prepare_log_file_with_limit(&path, 4).unwrap(); let rotated = rotated_log_path(&path); assert_eq!(std::fs::read_to_string(rotated).unwrap(), "oversized"); assert_eq!(std::fs::metadata(&path).unwrap().len(), 0); let _ = std::fs::remove_dir_all(dir); } }