Files
SpotiFLAC-Mobile/rust_backend/crates/extensions/src/logging.rs
T
zarzet aa99726439 feat(backend): migrate production backend to Rust
Replace the Go backend with the Rust workspace and route Android/iOS through
UniFFI bindings. Include the migrated extension runtime, network, providers,
media metadata, downloads and library operations, with version 5.0.0+147.

Remove Go sources, adapters, native build selection and CI dependencies.
Build Rust unconditionally and lock the iOS Rust pod using a relative path.
Retain legacy source and migration evidence in a local ignored archive.

Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch
equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and
diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI
build checkpoint; no new APK or iOS application build for this cleanup.

Known follow-up: URL/URLSearchParams globals are missing from the Rust JS
runtime. A controlled extension replay confirms a URL-resolution regression;
this commit does not fix that runtime gap.
2026-09-14 22:58:27 +07:00

221 lines
6.8 KiB
Rust

//! Process-style diagnostic buffer scoped to the native backend environment.
use crate::host::decode_go_utf8;
use crate::redact::sanitize_bytes;
use serde::Serialize;
use std::collections::VecDeque;
use std::sync::Mutex;
const CAPACITY: usize = 500;
const MAX_MESSAGE: usize = 4000;
#[derive(Clone, Debug, Serialize)]
pub struct LogEntry {
pub timestamp: String,
pub level: String,
pub tag: String,
pub message: String,
}
#[derive(Default)]
struct State {
entries: VecDeque<LogEntry>,
next_index: i64,
enabled: bool,
closed: bool,
}
#[derive(Default)]
pub struct LogBuffer(Mutex<State>);
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("log buffer closed")]
pub struct LogClosed;
impl State {
fn check(&self) -> Result<(), LogClosed> {
if self.closed { Err(LogClosed) } else { Ok(()) }
}
}
impl LogBuffer {
pub fn set_enabled(&self, enabled: bool) -> Result<(), LogClosed> {
let mut state = self.0.lock().expect("log buffer lock");
state.check()?;
state.enabled = enabled;
Ok(())
}
pub fn is_enabled(&self) -> Result<bool, LogClosed> {
let state = self.0.lock().expect("log buffer lock");
state.check()?;
Ok(state.enabled)
}
pub fn add(&self, level: &str, tag: &str, message: &str) -> Result<(), LogClosed> {
self.add_bytes(level, tag, message.as_bytes().to_vec())
}
fn add_bytes(&self, level: &str, tag: &str, message: Vec<u8>) -> Result<(), LogClosed> {
let mut state = self.0.lock().expect("log buffer lock");
state.check()?;
if !state.enabled && level != "ERROR" && level != "FATAL" {
return Ok(());
}
let message = decode_go_utf8(&truncate(sanitize_bytes(message)));
let entry = LogEntry {
timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(),
level: level.into(),
tag: tag.into(),
message,
};
if state.entries.len() == CAPACITY {
state.entries.pop_front();
}
use std::io::Write;
let _ = writeln!(
std::io::stderr().lock(),
"[{}] {}",
entry.tag,
entry.message
);
state.entries.push_back(entry);
state.next_index = state.next_index.wrapping_add(1);
Ok(())
}
pub fn backend(&self, message: &str) -> Result<(), LogClosed> {
self.backend_bytes(message.as_bytes().to_vec())
}
fn backend_bytes(&self, mut message: Vec<u8>) -> Result<(), LogClosed> {
if message.last() == Some(&b'\n') {
message.pop();
}
let mut tag = "Rust".to_owned();
if message.first() == Some(&b'[')
&& let Some(end) = message
.iter()
.position(|byte| *byte == b']')
.filter(|end| *end > 1)
{
tag = decode_go_utf8(&message[1..end]);
message = trim_bytes(&message[end + 1..]);
}
let lowercase = spotiflac_core::matching::lowercase(&decode_go_utf8(&message));
let level = if lowercase.contains("error") || lowercase.contains("failed") {
"ERROR"
} else if lowercase.contains("warning") || lowercase.contains("warn") {
"WARN"
} else if lowercase.contains("success") || lowercase.contains("match found") {
"INFO"
} else if lowercase.contains("searching")
|| lowercase.contains("trying")
|| lowercase.contains("found")
{
"DEBUG"
} else {
"INFO"
};
self.add_bytes(level, &tag, message)
}
pub(crate) fn extension(&self, id: &str, level: &str, arguments: Vec<String>, total: usize) {
let mut message = Vec::new();
for (index, argument) in arguments.into_iter().take(8).enumerate() {
if index > 0 {
message.push(b' ');
}
let bytes = argument.as_bytes();
message.extend_from_slice(&bytes[..bytes.len().min(512)]);
if bytes.len() > 512 {
message.extend_from_slice(b"...[truncated]");
}
}
if total > 8 {
if !message.is_empty() {
message.push(b' ');
}
message.extend_from_slice(format!("...[{} more args]", total - 8).as_bytes());
}
let formatted = truncate(sanitize_bytes(message));
let mut line = if level.is_empty() {
format!("[Extension:{id}] ")
} else {
format!("[Extension:{id}:{level}] ")
}
.into_bytes();
line.extend(formatted);
line.push(b'\n');
let _ = self.backend_bytes(line);
}
pub fn all(&self) -> Result<String, LogClosed> {
let state = self.0.lock().expect("log buffer lock");
state.check()?;
Ok(serde_json::to_string(&state.entries).expect("log entries JSON"))
}
pub fn since(&self, index: i64) -> Result<String, LogClosed> {
let state = self.0.lock().expect("log buffer lock");
state.check()?;
let earliest = state.next_index - state.entries.len() as i64;
let skip = if index >= state.next_index {
state.entries.len()
} else {
(index.max(earliest) - earliest) as usize
};
let logs: Vec<_> = state.entries.iter().skip(skip).collect();
Ok(serde_json::json!({"logs":logs,"next_index":state.next_index}).to_string())
}
pub fn clear(&self) -> Result<(), LogClosed> {
let mut state = self.0.lock().expect("log buffer lock");
state.check()?;
state.entries.clear();
Ok(())
}
pub fn count(&self) -> Result<u64, LogClosed> {
let state = self.0.lock().expect("log buffer lock");
state.check()?;
Ok(state.entries.len() as u64)
}
pub fn shutdown(&self) {
let mut state = self.0.lock().expect("log buffer lock");
state.closed = true;
state.entries.clear();
}
}
impl Drop for LogBuffer {
fn drop(&mut self) {
self.shutdown();
}
}
fn truncate(mut message: Vec<u8>) -> Vec<u8> {
if message.len() > MAX_MESSAGE {
message.truncate(MAX_MESSAGE);
if let Err(error) = std::str::from_utf8(&message) {
message.truncate(error.valid_up_to());
}
message.extend_from_slice(b"...[truncated]");
}
message
}
fn trim_bytes(bytes: &[u8]) -> Vec<u8> {
// Invalid bytes are non-whitespace in Go and are retained until JSON export.
let decoded = decode_go_utf8(bytes);
let left = decoded.len() - decoded.trim_start().len();
let right = decoded.len() - decoded.trim_end().len();
let end = bytes.len().saturating_sub(right);
if left >= end {
Vec::new()
} else {
bytes[left..end].to_vec()
}
}