1use wasm_bindgen::prelude::*;
2use crate::types::DatabaseError;
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!("Potentially dangerous SQL keyword detected: {}", keyword));
69 }
70 }
71
72 Ok(())
73}
74
75pub fn check_available_memory() -> Option<MemoryInfo> {
98 #[cfg(target_arch = "wasm32")]
99 {
100 check_memory_wasm()
102 }
103
104 #[cfg(not(target_arch = "wasm32"))]
105 {
106 check_memory_native()
108 }
109}
110
111#[cfg(target_arch = "wasm32")]
113fn check_memory_wasm() -> Option<MemoryInfo> {
114 let estimated_total: u64 = 2 * 1024 * 1024 * 1024; let estimated_available: u64 = 1536 * 1024 * 1024; let estimated_used: u64 = estimated_total - estimated_available;
126
127 log::debug!(
128 "WASM memory estimate (conservative): {} MB available, {} MB total",
129 estimated_available / (1024 * 1024),
130 estimated_total / (1024 * 1024)
131 );
132
133 Some(MemoryInfo {
134 available_bytes: estimated_available,
135 total_bytes: Some(estimated_total),
136 used_bytes: Some(estimated_used),
137 })
138}
139
140#[cfg(not(target_arch = "wasm32"))]
142fn check_memory_native() -> Option<MemoryInfo> {
143 #[cfg(target_os = "linux")]
144 {
145 check_memory_linux()
146 }
147
148 #[cfg(target_os = "macos")]
149 {
150 check_memory_macos()
151 }
152
153 #[cfg(target_os = "windows")]
154 {
155 check_memory_windows()
156 }
157
158 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
159 {
160 None
162 }
163}
164
165#[cfg(target_os = "linux")]
167fn check_memory_linux() -> Option<MemoryInfo> {
168 use std::fs;
169
170 let meminfo = fs::read_to_string("/proc/meminfo").ok()?;
171
172 let mut mem_available = None;
173 let mut mem_total = None;
174
175 for line in meminfo.lines() {
176 if line.starts_with("MemAvailable:") {
177 mem_available = line
178 .split_whitespace()
179 .nth(1)
180 .and_then(|s| s.parse::<u64>().ok())
181 .map(|kb| kb * 1024); } else if line.starts_with("MemTotal:") {
183 mem_total = line
184 .split_whitespace()
185 .nth(1)
186 .and_then(|s| s.parse::<u64>().ok())
187 .map(|kb| kb * 1024); }
189
190 if mem_available.is_some() && mem_total.is_some() {
191 break;
192 }
193 }
194
195 let available_bytes = mem_available?;
196 let total = mem_total;
197 let used = total.map(|t| t.saturating_sub(available_bytes));
198
199 Some(MemoryInfo {
200 available_bytes,
201 total_bytes: total,
202 used_bytes: used,
203 })
204}
205
206#[cfg(target_os = "macos")]
208fn check_memory_macos() -> Option<MemoryInfo> {
209 use std::process::Command;
210
211 let vm_stat_output = Command::new("vm_stat").output().ok()?;
213 let vm_stat_str = String::from_utf8_lossy(&vm_stat_output.stdout);
214
215 let mut page_size = 4096u64; let mut pages_free = 0u64;
217 let mut pages_inactive = 0u64;
218
219 for line in vm_stat_str.lines() {
220 if line.contains("page size of") {
221 if let Some(size_str) = line.split("page size of ").nth(1) {
222 if let Some(size) = size_str.split_whitespace().next() {
223 page_size = size.parse().unwrap_or(4096);
224 }
225 }
226 } else if line.starts_with("Pages free:") {
227 pages_free = line
228 .split(':')
229 .nth(1)
230 .and_then(|s| s.trim().trim_end_matches('.').parse().ok())
231 .unwrap_or(0);
232 } else if line.starts_with("Pages inactive:") {
233 pages_inactive = line
234 .split(':')
235 .nth(1)
236 .and_then(|s| s.trim().trim_end_matches('.').parse().ok())
237 .unwrap_or(0);
238 }
239 }
240
241 let available_bytes = (pages_free + pages_inactive) * page_size;
242
243 let total_output = Command::new("sysctl")
245 .arg("hw.memsize")
246 .output()
247 .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}