absurder_sql/
utils.rs

1use crate::types::DatabaseError;
2use wasm_bindgen::prelude::*;
3
4/// Utility functions for the SQLite IndexedDB library
5
6#[wasm_bindgen]
7extern "C" {
8    #[wasm_bindgen(js_namespace = console)]
9    fn log(s: &str);
10}
11
12/// Memory information for the current system
13#[derive(Debug, Clone)]
14pub struct MemoryInfo {
15    /// Available memory in bytes
16    pub available_bytes: u64,
17    /// Total system memory in bytes (if available)
18    pub total_bytes: Option<u64>,
19    /// Used memory in bytes (if available)
20    pub used_bytes: Option<u64>,
21}
22
23/// Log a message to the browser console
24pub fn console_log(message: &str) {
25    log(message);
26}
27
28/// Format bytes as a human-readable string
29pub 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
52/// Generate a unique identifier
53pub 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
59/// Validate SQL query for security
60pub fn validate_sql(sql: &str) -> Result<(), String> {
61    let sql_lower = sql.to_lowercase();
62
63    // Basic security checks
64    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
78/// Check available memory on the current system
79///
80/// Returns memory information if available, None if memory info cannot be determined.
81///
82/// # Platform Support
83/// - **Native (Linux)**: Reads /proc/meminfo for available memory
84/// - **Native (macOS)**: Uses sysctl for memory statistics
85/// - **Native (Windows)**: Uses GlobalMemoryStatusEx
86/// - **WASM/Browser**: Uses Performance.memory API if available (Chrome/Edge)
87///
88/// # Returns
89/// - `Some(MemoryInfo)` with available and optionally total/used memory
90/// - `None` if memory information is unavailable
91///
92/// # Example
93/// ```rust
94/// use absurder_sql::utils::check_available_memory;
95///
96/// if let Some(mem_info) = check_available_memory() {
97///     println!("Available memory: {} bytes", mem_info.available_bytes);
98/// }
99/// ```
100pub fn check_available_memory() -> Option<MemoryInfo> {
101    #[cfg(target_arch = "wasm32")]
102    {
103        // WASM/Browser environment - try to use Performance.memory API
104        check_memory_wasm()
105    }
106
107    #[cfg(not(target_arch = "wasm32"))]
108    {
109        // Native environment - use platform-specific APIs
110        check_memory_native()
111    }
112}
113
114/// Check memory in WASM/browser environment
115#[cfg(target_arch = "wasm32")]
116fn check_memory_wasm() -> Option<MemoryInfo> {
117    // WASM/Browser environment - return conservative estimates
118    //
119    // WASM linear memory is limited to 4GB max, but browsers typically
120    // impose lower limits. We use 2GB as a safe conservative estimate
121    // for available memory to prevent OOM errors.
122    //
123    // Note: Performance.memory API is Chrome-only and non-standard,
124    // so we provide a conservative estimate instead.
125
126    let estimated_total: u64 = 2 * 1024 * 1024 * 1024; // 2GB total estimate
127    let estimated_available: u64 = 1536 * 1024 * 1024; // 1.5GB available estimate
128    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/// Check memory in native environment
144#[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        // Unsupported platform - return conservative estimate
164        None
165    }
166}
167
168/// Check memory on Linux by reading /proc/meminfo
169#[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); // Convert KB to bytes
185        } 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); // Convert KB to bytes
191        }
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/// Check memory on macOS using sysctl
210#[cfg(target_os = "macos")]
211fn check_memory_macos() -> Option<MemoryInfo> {
212    use std::process::Command;
213
214    // Get total memory
215    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; // Default page size
219    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    // Get total memory
247    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/// Check memory on Windows
263#[cfg(target_os = "windows")]
264fn check_memory_windows() -> Option<MemoryInfo> {
265    // Windows memory checking would require winapi crate
266    // For now, return None (conservative approach)
267    // In production, implement using GlobalMemoryStatusEx from winapi
268    None
269}
270
271/// Estimate memory requirement for exporting a database
272///
273/// Calculates the estimated memory needed to export a database of given size.
274/// Includes overhead for:
275/// - Database content buffer
276/// - Intermediate block buffers
277/// - SQLite header and metadata
278/// - Safety margin
279///
280/// # Arguments
281/// * `database_size_bytes` - Size of the database to export
282///
283/// # Returns
284/// Estimated memory requirement in bytes (typically 1.5x-2x database size)
285///
286/// # Example
287/// ```rust
288/// use absurder_sql::utils::estimate_export_memory_requirement;
289///
290/// let db_size = 100 * 1024 * 1024; // 100MB
291/// let required_memory = estimate_export_memory_requirement(db_size);
292/// println!("Estimated memory needed: {} bytes", required_memory);
293/// ```
294pub fn estimate_export_memory_requirement(database_size_bytes: u64) -> u64 {
295    // Memory requirement breakdown:
296    // 1. Database content: database_size_bytes (1x)
297    // 2. Block read buffers: ~10-20MB for batch reads
298    // 3. Intermediate concatenation: ~database_size_bytes * 0.5 (worst case)
299    // 4. Safety margin: 20%
300
301    const BLOCK_BUFFER_SIZE: u64 = 20 * 1024 * 1024; // 20MB for block buffers
302    const OVERHEAD_MULTIPLIER: f64 = 1.5; // 50% overhead for intermediate buffers
303    const SAFETY_MARGIN: f64 = 1.2; // 20% safety margin
304
305    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
312/// Validate that sufficient memory is available for export operation
313///
314/// Checks if the system has enough available memory to safely export
315/// a database of the given size. Returns an error if insufficient memory
316/// is available.
317///
318/// # Arguments
319/// * `database_size_bytes` - Size of the database to export
320///
321/// # Returns
322/// * `Ok(())` - Sufficient memory is available
323/// * `Err(DatabaseError)` - Insufficient memory or cannot determine availability
324///
325/// # Example
326/// ```rust
327/// use absurder_sql::utils::validate_memory_for_export;
328///
329/// let db_size = 100 * 1024 * 1024; // 100MB
330/// match validate_memory_for_export(db_size) {
331///     Ok(_) => println!("Sufficient memory available"),
332///     Err(e) => eprintln!("Memory check failed: {}", e.message),
333/// }
334/// ```
335pub 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            // Memory is sufficient
355            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            // Cannot determine memory availability - log warning but allow operation
365            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}