1use crate::types::DatabaseError;
2use wasm_bindgen::prelude::*;
3
4#[wasm_bindgen]
7extern "C" {
8 #[wasm_bindgen(js_namespace = console)]
9 fn log(s: &str);
10}
11
12#[derive(Debug, Clone)]
14pub struct MemoryInfo {
15 pub available_bytes: u64,
17 pub total_bytes: Option<u64>,
19 pub used_bytes: Option<u64>,
21}
22
23pub fn console_log(message: &str) {
25 log(message);
26}
27
28pub fn format_bytes(bytes: usize) -> String {
30 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
31 const THRESHOLD: f64 = 1024.0;
32
33 if bytes == 0 {
34 return "0 B".to_string();
35 }
36
37 let mut size = bytes as f64;
38 let mut unit_index = 0;
39
40 while size >= THRESHOLD && unit_index < UNITS.len() - 1 {
41 size /= THRESHOLD;
42 unit_index += 1;
43 }
44
45 if unit_index == 0 {
46 format!("{} {}", bytes, UNITS[unit_index])
47 } else {
48 format!("{:.1} {}", size, UNITS[unit_index])
49 }
50}
51
52pub fn generate_id() -> String {
54 let timestamp = js_sys::Date::now() as u64;
55 let random = (js_sys::Math::random() * 1000000.0) as u32;
56 format!("{}_{}", timestamp, random)
57}
58
59pub fn validate_sql(sql: &str) -> Result<(), String> {
61 let sql_lower = sql.to_lowercase();
62
63 let dangerous_keywords = ["drop", "delete", "truncate", "alter"];
65
66 for keyword in dangerous_keywords {
67 if sql_lower.contains(keyword) {
68 return Err(format!(
69 "Potentially dangerous SQL keyword detected: {}",
70 keyword
71 ));
72 }
73 }
74
75 Ok(())
76}
77
78pub fn check_available_memory() -> Option<MemoryInfo> {
101 #[cfg(target_arch = "wasm32")]
102 {
103 check_memory_wasm()
105 }
106
107 #[cfg(not(target_arch = "wasm32"))]
108 {
109 check_memory_native()
111 }
112}
113
114#[cfg(target_arch = "wasm32")]
116fn check_memory_wasm() -> Option<MemoryInfo> {
117 let estimated_total: u64 = 2 * 1024 * 1024 * 1024; let estimated_available: u64 = 1536 * 1024 * 1024; let estimated_used: u64 = estimated_total - estimated_available;
129
130 log::debug!(
131 "WASM memory estimate (conservative): {} MB available, {} MB total",
132 estimated_available / (1024 * 1024),
133 estimated_total / (1024 * 1024)
134 );
135
136 Some(MemoryInfo {
137 available_bytes: estimated_available,
138 total_bytes: Some(estimated_total),
139 used_bytes: Some(estimated_used),
140 })
141}
142
143#[cfg(not(target_arch = "wasm32"))]
145fn check_memory_native() -> Option<MemoryInfo> {
146 #[cfg(target_os = "linux")]
147 {
148 check_memory_linux()
149 }
150
151 #[cfg(target_os = "macos")]
152 {
153 check_memory_macos()
154 }
155
156 #[cfg(target_os = "windows")]
157 {
158 check_memory_windows()
159 }
160
161 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
162 {
163 None
165 }
166}
167
168#[cfg(target_os = "linux")]
170fn check_memory_linux() -> Option<MemoryInfo> {
171 use std::fs;
172
173 let meminfo = fs::read_to_string("/proc/meminfo").ok()?;
174
175 let mut mem_available = None;
176 let mut mem_total = None;
177
178 for line in meminfo.lines() {
179 if line.starts_with("MemAvailable:") {
180 mem_available = line
181 .split_whitespace()
182 .nth(1)
183 .and_then(|s| s.parse::<u64>().ok())
184 .map(|kb| kb * 1024); } else if line.starts_with("MemTotal:") {
186 mem_total = line
187 .split_whitespace()
188 .nth(1)
189 .and_then(|s| s.parse::<u64>().ok())
190 .map(|kb| kb * 1024); }
192
193 if mem_available.is_some() && mem_total.is_some() {
194 break;
195 }
196 }
197
198 let available_bytes = mem_available?;
199 let total = mem_total;
200 let used = total.map(|t| t.saturating_sub(available_bytes));
201
202 Some(MemoryInfo {
203 available_bytes,
204 total_bytes: total,
205 used_bytes: used,
206 })
207}
208
209#[cfg(target_os = "macos")]
211fn check_memory_macos() -> Option<MemoryInfo> {
212 use std::process::Command;
213
214 let vm_stat_output = Command::new("vm_stat").output().ok()?;
216 let vm_stat_str = String::from_utf8_lossy(&vm_stat_output.stdout);
217
218 let mut page_size = 4096u64; let mut pages_free = 0u64;
220 let mut pages_inactive = 0u64;
221
222 for line in vm_stat_str.lines() {
223 if line.contains("page size of") {
224 if let Some(size_str) = line.split("page size of ").nth(1) {
225 if let Some(size) = size_str.split_whitespace().next() {
226 page_size = size.parse().unwrap_or(4096);
227 }
228 }
229 } else if line.starts_with("Pages free:") {
230 pages_free = line
231 .split(':')
232 .nth(1)
233 .and_then(|s| s.trim().trim_end_matches('.').parse().ok())
234 .unwrap_or(0);
235 } else if line.starts_with("Pages inactive:") {
236 pages_inactive = line
237 .split(':')
238 .nth(1)
239 .and_then(|s| s.trim().trim_end_matches('.').parse().ok())
240 .unwrap_or(0);
241 }
242 }
243
244 let available_bytes = (pages_free + pages_inactive) * page_size;
245
246 let total_output = Command::new("sysctl").arg("hw.memsize").output().ok()?;
248
249 let total_str = String::from_utf8_lossy(&total_output.stdout);
250 let total_bytes = total_str
251 .split(':')
252 .nth(1)
253 .and_then(|s| s.trim().parse().ok());
254
255 Some(MemoryInfo {
256 available_bytes,
257 total_bytes,
258 used_bytes: total_bytes.map(|t| t.saturating_sub(available_bytes)),
259 })
260}
261
262#[cfg(target_os = "windows")]
264fn check_memory_windows() -> Option<MemoryInfo> {
265 None
269}
270
271pub fn estimate_export_memory_requirement(database_size_bytes: u64) -> u64 {
295 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;
306 let with_buffers = base_requirement + BLOCK_BUFFER_SIZE as f64;
307 let with_safety = with_buffers * SAFETY_MARGIN;
308
309 with_safety as u64
310}
311
312pub fn validate_memory_for_export(database_size_bytes: u64) -> Result<(), DatabaseError> {
336 let required_memory = estimate_export_memory_requirement(database_size_bytes);
337
338 match check_available_memory() {
339 Some(mem_info) => {
340 if mem_info.available_bytes < required_memory {
341 let available_mb = mem_info.available_bytes as f64 / (1024.0 * 1024.0);
342 let required_mb = required_memory as f64 / (1024.0 * 1024.0);
343
344 return Err(DatabaseError::new(
345 "INSUFFICIENT_MEMORY",
346 &format!(
347 "Insufficient memory for export. Available: {:.1} MB, Required: {:.1} MB. \
348 Consider using streaming export with smaller chunk sizes or closing other applications.",
349 available_mb, required_mb
350 ),
351 ));
352 }
353
354 log::info!(
356 "Memory check passed: {} MB available, {} MB required for export",
357 mem_info.available_bytes / (1024 * 1024),
358 required_memory / (1024 * 1024)
359 );
360
361 Ok(())
362 }
363 None => {
364 log::warn!(
366 "Cannot determine available memory. Proceeding with export of {} MB database. \
367 Monitor memory usage carefully.",
368 database_size_bytes / (1024 * 1024)
369 );
370
371 Ok(())
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_format_bytes() {
382 assert_eq!(format_bytes(0), "0 B");
383 assert_eq!(format_bytes(512), "512 B");
384 assert_eq!(format_bytes(1024), "1.0 KB");
385 assert_eq!(format_bytes(1536), "1.5 KB");
386 assert_eq!(format_bytes(1048576), "1.0 MB");
387 }
388
389 #[test]
390 fn test_validate_sql() {
391 assert!(validate_sql("SELECT * FROM users").is_ok());
392 assert!(validate_sql("INSERT INTO users (name) VALUES ('test')").is_ok());
393 assert!(validate_sql("DROP TABLE users").is_err());
394 assert!(validate_sql("DELETE FROM users WHERE id = 1").is_err());
395 }
396}