use crate::types::DatabaseError;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[derive(Debug, Clone)]
pub struct MemoryInfo {
pub available_bytes: u64,
pub total_bytes: Option<u64>,
pub used_bytes: Option<u64>,
}
pub fn console_log(message: &str) {
log(message);
}
pub fn format_bytes(bytes: usize) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
const THRESHOLD: f64 = 1024.0;
if bytes == 0 {
return "0 B".to_string();
}
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= THRESHOLD && unit_index < UNITS.len() - 1 {
size /= THRESHOLD;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", bytes, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
pub fn generate_id() -> String {
let timestamp = js_sys::Date::now() as u64;
let random = (js_sys::Math::random() * 1000000.0) as u32;
format!("{}_{}", timestamp, random)
}
pub fn validate_sql(sql: &str) -> Result<(), String> {
let sql_lower = sql.to_lowercase();
let dangerous_keywords = ["drop", "delete", "truncate", "alter"];
for keyword in dangerous_keywords {
if sql_lower.contains(keyword) {
return Err(format!(
"Potentially dangerous SQL keyword detected: {}",
keyword
));
}
}
Ok(())
}
pub fn check_available_memory() -> Option<MemoryInfo> {
#[cfg(target_arch = "wasm32")]
{
check_memory_wasm()
}
#[cfg(not(target_arch = "wasm32"))]
{
check_memory_native()
}
}
#[cfg(target_arch = "wasm32")]
fn check_memory_wasm() -> Option<MemoryInfo> {
let estimated_total: u64 = 2 * 1024 * 1024 * 1024; let estimated_available: u64 = 1536 * 1024 * 1024; let estimated_used: u64 = estimated_total - estimated_available;
log::debug!(
"WASM memory estimate (conservative): {} MB available, {} MB total",
estimated_available / (1024 * 1024),
estimated_total / (1024 * 1024)
);
Some(MemoryInfo {
available_bytes: estimated_available,
total_bytes: Some(estimated_total),
used_bytes: Some(estimated_used),
})
}
#[cfg(not(target_arch = "wasm32"))]
fn check_memory_native() -> Option<MemoryInfo> {
#[cfg(target_os = "linux")]
{
check_memory_linux()
}
#[cfg(target_os = "macos")]
{
check_memory_macos()
}
#[cfg(target_os = "windows")]
{
check_memory_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
None
}
}
#[cfg(target_os = "linux")]
fn check_memory_linux() -> Option<MemoryInfo> {
use std::fs;
let meminfo = fs::read_to_string("/proc/meminfo").ok()?;
let mut mem_available = None;
let mut mem_total = None;
for line in meminfo.lines() {
if line.starts_with("MemAvailable:") {
mem_available = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u64>().ok())
.map(|kb| kb * 1024); } else if line.starts_with("MemTotal:") {
mem_total = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u64>().ok())
.map(|kb| kb * 1024); }
if mem_available.is_some() && mem_total.is_some() {
break;
}
}
let available_bytes = mem_available?;
let total = mem_total;
let used = total.map(|t| t.saturating_sub(available_bytes));
Some(MemoryInfo {
available_bytes,
total_bytes: total,
used_bytes: used,
})
}
#[cfg(target_os = "macos")]
fn check_memory_macos() -> Option<MemoryInfo> {
use std::process::Command;
let vm_stat_output = Command::new("vm_stat").output().ok()?;
let vm_stat_str = String::from_utf8_lossy(&vm_stat_output.stdout);
let mut page_size = 4096u64; let mut pages_free = 0u64;
let mut pages_inactive = 0u64;
for line in vm_stat_str.lines() {
if line.contains("page size of") {
if let Some(size_str) = line.split("page size of ").nth(1) {
if let Some(size) = size_str.split_whitespace().next() {
page_size = size.parse().unwrap_or(4096);
}
}
} else if line.starts_with("Pages free:") {
pages_free = line
.split(':')
.nth(1)
.and_then(|s| s.trim().trim_end_matches('.').parse().ok())
.unwrap_or(0);
} else if line.starts_with("Pages inactive:") {
pages_inactive = line
.split(':')
.nth(1)
.and_then(|s| s.trim().trim_end_matches('.').parse().ok())
.unwrap_or(0);
}
}
let available_bytes = (pages_free + pages_inactive) * page_size;
let total_output = Command::new("sysctl").arg("hw.memsize").output().ok()?;
let total_str = String::from_utf8_lossy(&total_output.stdout);
let total_bytes = total_str
.split(':')
.nth(1)
.and_then(|s| s.trim().parse().ok());
Some(MemoryInfo {
available_bytes,
total_bytes,
used_bytes: total_bytes.map(|t| t.saturating_sub(available_bytes)),
})
}
#[cfg(target_os = "windows")]
fn check_memory_windows() -> Option<MemoryInfo> {
None
}
pub fn estimate_export_memory_requirement(database_size_bytes: u64) -> u64 {
const BLOCK_BUFFER_SIZE: u64 = 20 * 1024 * 1024; const OVERHEAD_MULTIPLIER: f64 = 1.5; const SAFETY_MARGIN: f64 = 1.2;
let base_requirement = database_size_bytes as f64 * OVERHEAD_MULTIPLIER;
let with_buffers = base_requirement + BLOCK_BUFFER_SIZE as f64;
let with_safety = with_buffers * SAFETY_MARGIN;
with_safety as u64
}
pub fn validate_memory_for_export(database_size_bytes: u64) -> Result<(), DatabaseError> {
let required_memory = estimate_export_memory_requirement(database_size_bytes);
match check_available_memory() {
Some(mem_info) => {
if mem_info.available_bytes < required_memory {
let available_mb = mem_info.available_bytes as f64 / (1024.0 * 1024.0);
let required_mb = required_memory as f64 / (1024.0 * 1024.0);
return Err(DatabaseError::new(
"INSUFFICIENT_MEMORY",
&format!(
"Insufficient memory for export. Available: {:.1} MB, Required: {:.1} MB. \
Consider using streaming export with smaller chunk sizes or closing other applications.",
available_mb, required_mb
),
));
}
log::info!(
"Memory check passed: {} MB available, {} MB required for export",
mem_info.available_bytes / (1024 * 1024),
required_memory / (1024 * 1024)
);
Ok(())
}
None => {
log::warn!(
"Cannot determine available memory. Proceeding with export of {} MB database. \
Monitor memory usage carefully.",
database_size_bytes / (1024 * 1024)
);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1536), "1.5 KB");
assert_eq!(format_bytes(1048576), "1.0 MB");
}
#[test]
fn test_validate_sql() {
assert!(validate_sql("SELECT * FROM users").is_ok());
assert!(validate_sql("INSERT INTO users (name) VALUES ('test')").is_ok());
assert!(validate_sql("DROP TABLE users").is_err());
assert!(validate_sql("DELETE FROM users WHERE id = 1").is_err());
}
}