Skip to main content

safebrowsing_db_redb/
lib.rs

1//! Redb-based persistent database backend for Google Safe Browsing API
2//!
3//! This crate provides a persistent database implementation using redb that implements
4//! the Database trait from safebrowsing-db. It stores threat lists on disk and provides
5//! thread-safe access with ACID transactions.
6
7use async_trait::async_trait;
8use redb::{Database as RedbDb, ReadableTable, TableDefinition};
9use safebrowsing_api::{SafeBrowsingApi, ThreatDescriptor};
10use safebrowsing_db::{Database, DatabaseError, DatabaseStats};
11use safebrowsing_hash::{HashPrefix, HashPrefixSet};
12use safebrowsing_proto::{CompressionType, RiceDeltaEncoding};
13use serde::{Deserialize, Serialize};
14use std::borrow::BorrowMut;
15use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::time::{Duration, SystemTime, UNIX_EPOCH};
19use tokio::sync::RwLock;
20use tracing::{debug, info, warn};
21
22type Result<T> = std::result::Result<T, DatabaseError>;
23
24/// Table definitions for redb storage
25const THREAT_LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("threat_lists");
26const METADATA_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
27const HASH_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("hashes");
28
29/// Metadata keys
30const LAST_UPDATE_KEY: &str = "last_update";
31const INITIALIZED_KEY: &str = "initialized";
32const HASH_COUNT_KEY: &str = "hash_count";
33
34/// Serializable threat list entry for storage
35#[derive(Serialize, Deserialize, Clone)]
36struct StoredThreatListEntry {
37    /// Hash prefixes as bytes
38    hash_prefixes: Vec<Vec<u8>>,
39    /// Client state for this list
40    client_state: Vec<u8>,
41    /// Checksum of this list
42    checksum: Vec<u8>,
43    /// Last update timestamp (seconds since epoch)
44    last_update: u64,
45}
46
47impl StoredThreatListEntry {
48    fn from_hash_set(hash_set: &HashPrefixSet, client_state: Vec<u8>, checksum: Vec<u8>) -> Self {
49        let hash_prefixes = hash_set
50            .iter()
51            .map(|prefix| prefix.as_bytes().to_vec())
52            .collect();
53
54        Self {
55            hash_prefixes,
56            client_state,
57            checksum,
58            last_update: SystemTime::now()
59                .duration_since(UNIX_EPOCH)
60                .unwrap_or_default()
61                .as_secs(),
62        }
63    }
64
65    fn to_hash_set(&self) -> Result<HashPrefixSet> {
66        let mut hash_set = HashPrefixSet::new();
67        for prefix_bytes in &self.hash_prefixes {
68            let prefix = HashPrefix::new(prefix_bytes.clone())
69                .map_err(|e| DatabaseError::DecodeError(format!("Invalid hash prefix: {}", e)))?;
70            hash_set.insert(prefix);
71        }
72        Ok(hash_set)
73    }
74}
75
76/// Redb-based persistent database for Safe Browsing
77pub struct RedbDatabase {
78    /// Redb database instance
79    db: Arc<RedbDb>,
80    /// In-memory cache of threat lists for fast access
81    cache: Arc<RwLock<HashMap<ThreatDescriptor, StoredThreatListEntry>>>,
82    /// Maximum database age before it's considered stale
83    max_age: Duration,
84    /// Database file path
85    path: PathBuf,
86}
87
88impl RedbDatabase {
89    /// Create a new redb database at the specified path
90    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
91        let path = path.as_ref().to_path_buf();
92
93        info!("Creating RedbDatabase at {:?}", path);
94
95        // Create parent directory if it doesn't exist
96        if let Some(parent) = path.parent() {
97            std::fs::create_dir_all(parent).map_err(DatabaseError::IoError)?;
98        }
99
100        let db = if path.exists() {
101            info!("Opening existing database at {:?}", path);
102            let mut db = RedbDb::open(&path).map_err(|e| {
103                DatabaseError::DecodeError(format!("Failed to open existing database: {}", e))
104            })?;
105            db.upgrade();
106            db
107        } else {
108            info!("Creating new database at {:?}", path);
109            RedbDb::builder()
110                .create_with_file_format_v3(true)
111                .create(&path)
112                .map_err(|e| {
113                    DatabaseError::DecodeError(format!("Failed to create new database: {}", e))
114                })?
115        };
116
117        // Initialize tables
118        {
119            let write_txn = db.begin_write().map_err(|e| {
120                DatabaseError::DecodeError(format!("Failed to begin write transaction: {}", e))
121            })?;
122
123            write_txn.open_table(THREAT_LISTS_TABLE).map_err(|e| {
124                DatabaseError::DecodeError(format!("Failed to create threat_lists table: {}", e))
125            })?;
126            write_txn.open_table(METADATA_TABLE).map_err(|e| {
127                DatabaseError::DecodeError(format!("Failed to create metadata table: {}", e))
128            })?;
129            write_txn.open_table(HASH_TABLE).map_err(|e| {
130                DatabaseError::DecodeError(format!("Failed to create hash table: {}", e))
131            })?;
132
133            write_txn.commit().map_err(|e| {
134                DatabaseError::DecodeError(format!("Failed to commit transaction: {}", e))
135            })?;
136        }
137
138        let instance = Self {
139            db: Arc::new(db),
140            cache: Arc::new(RwLock::new(HashMap::new())),
141            max_age: safebrowsing_db::DEFAULT_MAX_DATABASE_AGE,
142            path,
143        };
144
145        Ok(instance)
146    }
147
148    /// Initialize the database and load cache
149    pub async fn init(&self) -> Result<()> {
150        info!("Initializing RedbDatabase at {:?}", self.path);
151        self.load_cache().await?;
152        info!("RedbDatabase initialization completed");
153        Ok(())
154    }
155
156    /// Create a new redb database with a custom maximum age
157    pub async fn with_max_age<P: AsRef<Path>>(path: P, max_age: Duration) -> Result<Self> {
158        let mut db = Self::new(path)?;
159        db.max_age = max_age;
160        db.init().await?;
161        Ok(db)
162    }
163
164    /// Get the default database path in the system cache directory
165    pub fn default_path() -> Result<PathBuf> {
166        let cache_dir = dirs::cache_dir().ok_or_else(|| {
167            DatabaseError::DecodeError("Failed to get cache directory".to_string())
168        })?;
169
170        let safebrowsing_dir = cache_dir.join("safebrowsing");
171        Ok(safebrowsing_dir.join("database.redb"))
172    }
173
174    /// Load threat lists from disk into memory cache
175    async fn load_cache(&self) -> Result<()> {
176        debug!("Loading cache from database");
177        let read_txn = self.db.begin_read().map_err(|e| {
178            DatabaseError::DecodeError(format!("Failed to begin read transaction: {}", e))
179        })?;
180
181        let table = read_txn.open_table(THREAT_LISTS_TABLE).map_err(|e| {
182            DatabaseError::DecodeError(format!("Failed to open threat_lists table: {}", e))
183        })?;
184
185        let mut cache = HashMap::new();
186        let mut loaded_count = 0;
187
188        for item in table
189            .iter()
190            .map_err(|e| DatabaseError::DecodeError(format!("Failed to iterate table: {}", e)))?
191        {
192            let (key, value) = item.map_err(|e| {
193                DatabaseError::DecodeError(format!("Failed to read table item: {}", e))
194            })?;
195
196            let threat_descriptor: ThreatDescriptor =
197                serde_json::from_str(key.value()).map_err(|e| {
198                    DatabaseError::DecodeError(format!("Failed to decode threat descriptor: {}", e))
199                })?;
200
201            let entry: StoredThreatListEntry =
202                serde_json::from_slice(value.value()).map_err(|e| {
203                    DatabaseError::DecodeError(format!("Failed to decode threat list entry: {}", e))
204                })?;
205
206            debug!(
207                "Loaded threat list {:?} with {} hashes",
208                threat_descriptor,
209                entry.hash_prefixes.len()
210            );
211            cache.insert(threat_descriptor, entry);
212            loaded_count += 1;
213        }
214
215        debug!(
216            "Loaded {} threat lists from database into cache",
217            loaded_count
218        );
219        let mut cache_guard = self.cache.write().await;
220        *cache_guard = cache;
221
222        Ok(())
223    }
224
225    /// Store a threat list entry both in cache and on disk
226    async fn store_threat_list(
227        &self,
228        threat_descriptor: &ThreatDescriptor,
229        entry: StoredThreatListEntry,
230    ) -> Result<()> {
231        debug!(
232            "Storing threat list {:?} with {} hashes",
233            threat_descriptor,
234            entry.hash_prefixes.len()
235        );
236
237        // Update cache first
238        {
239            let mut cache = self.cache.write().await;
240            cache.insert(threat_descriptor.clone(), entry.clone());
241        }
242
243        // Then persist to disk
244        let threat_descriptor_key = serde_json::to_string(threat_descriptor).map_err(|e| {
245            DatabaseError::DecodeError(format!("Failed to serialize threat descriptor: {}", e))
246        })?;
247
248        let entry_value = serde_json::to_vec(&entry).map_err(|e| {
249            DatabaseError::DecodeError(format!("Failed to serialize threat list entry: {}", e))
250        })?;
251
252        let write_txn = self.db.begin_write().map_err(|e| {
253            DatabaseError::DecodeError(format!("Failed to begin write transaction: {}", e))
254        })?;
255
256        {
257            let mut table = write_txn.open_table(THREAT_LISTS_TABLE).map_err(|e| {
258                DatabaseError::DecodeError(format!("Failed to open threat_lists table: {}", e))
259            })?;
260
261            table
262                .insert(threat_descriptor_key.as_str(), entry_value.as_slice())
263                .map_err(|e| {
264                    DatabaseError::DecodeError(format!("Failed to insert threat list: {}", e))
265                })?;
266        }
267
268        write_txn.commit().map_err(|e| {
269            DatabaseError::DecodeError(format!("Failed to commit transaction: {}", e))
270        })?;
271
272        debug!(
273            "Successfully stored threat list {:?} to database",
274            threat_descriptor
275        );
276        Ok(())
277    }
278
279    /// Store metadata value
280    fn store_metadata(&self, key: &str, value: &[u8]) -> Result<()> {
281        debug!("Storing metadata key: {}", key);
282        let write_txn = self.db.begin_write().map_err(|e| {
283            DatabaseError::DecodeError(format!("Failed to begin write transaction: {}", e))
284        })?;
285
286        {
287            let mut table = write_txn.open_table(METADATA_TABLE).map_err(|e| {
288                DatabaseError::DecodeError(format!("Failed to open metadata table: {}", e))
289            })?;
290
291            table.insert(key, value).map_err(|e| {
292                DatabaseError::DecodeError(format!("Failed to insert metadata: {}", e))
293            })?;
294        }
295
296        write_txn.commit().map_err(|e| {
297            DatabaseError::DecodeError(format!("Failed to commit transaction: {}", e))
298        })?;
299
300        debug!("Successfully stored metadata key: {}", key);
301        Ok(())
302    }
303
304    /// Get metadata value
305    fn get_metadata(&self, key: &str) -> Result<Option<Vec<u8>>> {
306        debug!("Getting metadata key: {}", key);
307        let read_txn = self.db.begin_read().map_err(|e| {
308            DatabaseError::DecodeError(format!("Failed to begin read transaction: {}", e))
309        })?;
310
311        let table = read_txn.open_table(METADATA_TABLE).map_err(|e| {
312            DatabaseError::DecodeError(format!("Failed to open metadata table: {}", e))
313        })?;
314
315        match table.get(key) {
316            Ok(Some(value)) => {
317                debug!(
318                    "Found metadata key: {} with {} bytes",
319                    key,
320                    value.value().len()
321                );
322                Ok(Some(value.value().to_vec()))
323            }
324            Ok(None) => {
325                debug!("Metadata key not found: {}", key);
326                Ok(None)
327            }
328            Err(e) => Err(DatabaseError::DecodeError(format!(
329                "Failed to get metadata: {}",
330                e
331            ))),
332        }
333    }
334
335    /// Update hash count metadata
336    async fn update_hash_count(&self) -> Result<()> {
337        let cache = self.cache.read().await;
338        let total_count: usize = cache.values().map(|entry| entry.hash_prefixes.len()).sum();
339
340        let count_bytes = total_count.to_le_bytes();
341        self.store_metadata(HASH_COUNT_KEY, &count_bytes)?;
342
343        Ok(())
344    }
345
346    /// Process full update for a threat list
347    async fn process_full_update(
348        &self,
349        threat_descriptor: &ThreatDescriptor,
350        list_update: &safebrowsing_proto::fetch_threat_list_updates_response::ListUpdateResponse,
351    ) -> Result<()> {
352        debug!("Processing full update for {:?}", threat_descriptor);
353
354        let mut hash_set = HashPrefixSet::new();
355
356        // Process additions only for full updates
357        for addition_set in &list_update.additions {
358            self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
359        }
360
361        // Create and store the new entry
362        let entry = StoredThreatListEntry::from_hash_set(
363            &hash_set,
364            list_update.new_client_state.clone().to_vec(),
365            list_update
366                .checksum
367                .as_ref()
368                .map_or_else(Vec::new, |c| c.sha256.clone().to_vec()),
369        );
370        self.store_threat_list(threat_descriptor, entry).await?;
371
372        Ok(())
373    }
374
375    /// Process partial update by applying additions to existing data
376    async fn process_partial_update(
377        &self,
378        threat_descriptor: &ThreatDescriptor,
379        list_update: &safebrowsing_proto::fetch_threat_list_updates_response::ListUpdateResponse,
380    ) -> Result<()> {
381        debug!("Processing partial update for {:?}", threat_descriptor);
382
383        // Get existing hash set
384        let mut hash_set = {
385            let cache = self.cache.read().await;
386            match cache.get(threat_descriptor) {
387                Some(entry) => entry.to_hash_set()?,
388                None => {
389                    debug!("No existing data found, treating as full update");
390                    return self
391                        .process_full_update(threat_descriptor, list_update)
392                        .await;
393                }
394            }
395        };
396
397        // Process removals first (order matters for correct indexing)
398        for removal_set in &list_update.removals {
399            debug!(
400                "Processing removal set with {} indices",
401                removal_set
402                    .raw_indices
403                    .as_ref()
404                    .map_or(0, |r| r.indices.len())
405                    + removal_set.rice_indices.as_ref().map_or(0, |_| 1)
406            ); // Rice indices contain multiple values
407            self.process_raw_hashes_removal(&mut hash_set, removal_set)?;
408        }
409
410        // Then process additions
411        for addition_set in &list_update.additions {
412            debug!("Processing addition set");
413            self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
414        }
415
416        debug!(
417            "Partial update complete. Hash set now contains {} entries",
418            hash_set.len()
419        );
420
421        // Create and store the updated entry
422        let entry = StoredThreatListEntry::from_hash_set(
423            &hash_set,
424            list_update.new_client_state.clone().to_vec(),
425            list_update
426                .checksum
427                .as_ref()
428                .map_or_else(Vec::new, |c| c.sha256.clone().to_vec()),
429        );
430        self.store_threat_list(threat_descriptor, entry).await?;
431
432        Ok(())
433    }
434
435    /// Process raw hashes addition
436    fn process_raw_hashes_addition(
437        &self,
438        hash_set: &mut HashPrefixSet,
439        addition_set: &safebrowsing_proto::ThreatEntrySet,
440    ) -> Result<()> {
441        match addition_set.compression_type {
442            x if x == CompressionType::Raw as i32 => {
443                // Raw hashes
444                if let Some(raw_hashes) = &addition_set.raw_hashes {
445                    self.process_raw_hashes(hash_set, raw_hashes)?;
446                }
447            }
448            x if x == CompressionType::Rice as i32 => {
449                // Rice-encoded hashes
450                if let Some(rice_hashes) = &addition_set.rice_hashes {
451                    self.process_rice_hashes(hash_set, rice_hashes)?;
452                }
453            }
454            _ => {
455                debug!(
456                    "Unsupported compression type: {}",
457                    addition_set.compression_type
458                );
459            }
460        }
461
462        Ok(())
463    }
464
465    /// Process raw hashes and add them to the hash set
466    fn process_raw_hashes(
467        &self,
468        hash_set: &mut HashPrefixSet,
469        raw_hashes: &safebrowsing_proto::RawHashes,
470    ) -> Result<()> {
471        let prefix_size = raw_hashes.prefix_size as usize;
472
473        if !(4..=32).contains(&prefix_size) {
474            return Err(DatabaseError::DecodeError(format!(
475                "Invalid prefix size: {prefix_size}"
476            )));
477        }
478
479        let hashes = &raw_hashes.raw_hashes;
480        if hashes.len() % prefix_size != 0 {
481            return Err(DatabaseError::DecodeError(format!(
482                "Raw hashes length {} is not a multiple of prefix size {}",
483                hashes.len(),
484                prefix_size
485            )));
486        }
487
488        for i in (0..hashes.len()).step_by(prefix_size) {
489            let end = i + prefix_size;
490            if end > hashes.len() {
491                break;
492            }
493
494            // Convert to a Vec<u8> to avoid lifetime issues
495            let hash_vec = hashes[i..end].to_vec();
496            match HashPrefix::new(hash_vec) {
497                Ok(hash) => {
498                    hash_set.insert(hash);
499                }
500                Err(e) => {
501                    debug!("Skipping invalid hash: {}", e);
502                }
503            }
504        }
505
506        Ok(())
507    }
508
509    /// Process Rice-encoded hash additions
510    /// Process Rice-encoded hashes and add them to the hash set.
511    ///
512    /// IMPORTANT: This uses little-endian byte order to match the Go implementation.
513    /// The Safe Browsing API's Go reference implementation uses `binary.LittleEndian.PutUint32`
514    /// when converting Rice-decoded integers to hash bytes. Using big-endian would result
515    /// in completely different hash values and checksum mismatches.
516    ///
517    /// See: https://github.com/google/safebrowsing/blob/master/hash.go#L183
518    fn process_rice_hashes(
519        &self,
520        hash_set: &mut HashPrefixSet,
521        rice_hashes: &RiceDeltaEncoding,
522    ) -> Result<()> {
523        let decoded_hashes = self.decode_rice_delta_encoding(rice_hashes)?;
524
525        for hash_value in decoded_hashes {
526            // Rice encoding is for 4-byte hashes
527            // CRITICAL: Use little-endian to match Go implementation
528            // Go code: binary.LittleEndian.PutUint32(buf[:], h)
529            let hash_vec = hash_value.to_le_bytes().to_vec();
530            match HashPrefix::new(hash_vec) {
531                Ok(hash) => {
532                    hash_set.insert(hash);
533                }
534                Err(e) => {
535                    debug!("Skipping invalid hash: {}", e);
536                }
537            }
538        }
539
540        Ok(())
541    }
542
543    /// Decode Rice delta encoding
544    fn decode_rice_delta_encoding(&self, rice: &RiceDeltaEncoding) -> Result<Vec<u32>> {
545        use safebrowsing_hash::rice::decode_rice_integers;
546
547        decode_rice_integers(
548            rice.rice_parameter,
549            rice.first_value,
550            rice.num_entries,
551            &rice.encoded_data,
552        )
553        .map_err(|e| DatabaseError::RiceDecodeError(e.to_string()))
554    }
555
556    /// Process hash removals from a threat entry set
557    fn process_raw_hashes_removal(
558        &self,
559        hash_set: &mut HashPrefixSet,
560        removal_set: &safebrowsing_proto::ThreatEntrySet,
561    ) -> Result<()> {
562        match removal_set.compression_type {
563            x if x == CompressionType::Raw as i32 => {
564                // Raw indices
565                if let Some(raw_indices) = &removal_set.raw_indices {
566                    self.process_raw_indices(hash_set, raw_indices)?;
567                }
568            }
569            x if x == CompressionType::Rice as i32 => {
570                // Rice-encoded indices
571                if let Some(rice_indices) = &removal_set.rice_indices {
572                    self.process_rice_indices(hash_set, rice_indices)?;
573                }
574            }
575            _ => {
576                debug!(
577                    "Unsupported compression type for removal: {}",
578                    removal_set.compression_type
579                );
580            }
581        }
582
583        Ok(())
584    }
585
586    /// Process raw indices for hash removal
587    fn process_raw_indices(
588        &self,
589        hash_set: &mut HashPrefixSet,
590        raw_indices: &safebrowsing_proto::RawIndices,
591    ) -> Result<()> {
592        // Need to sort the hashes and remove by index
593        let sorted_hashes = hash_set.to_sorted_vec();
594
595        // Build a set of indices to remove
596        let mut indices_to_remove = std::collections::HashSet::new();
597        for &index in &raw_indices.indices {
598            if index >= 0 && (index as usize) < sorted_hashes.len() {
599                indices_to_remove.insert(index as usize);
600            } else {
601                return Err(DatabaseError::InvalidIndices(format!(
602                    "Index out of bounds: {} (max: {})",
603                    index,
604                    sorted_hashes.len()
605                )));
606            }
607        }
608
609        // Remove the hashes at the specified indices
610        for (i, hash) in sorted_hashes.iter().enumerate() {
611            if indices_to_remove.contains(&i) {
612                hash_set.remove(hash);
613            }
614        }
615
616        Ok(())
617    }
618
619    /// Process Rice-encoded indices for hash removal
620    fn process_rice_indices(
621        &self,
622        hash_set: &mut HashPrefixSet,
623        rice_indices: &RiceDeltaEncoding,
624    ) -> Result<()> {
625        let decoded_indices = self.decode_rice_delta_encoding(rice_indices)?;
626
627        // Need to sort the hashes and remove by index
628        let sorted_hashes = hash_set.to_sorted_vec();
629
630        // Build a set of indices to remove
631        let mut indices_to_remove = std::collections::HashSet::new();
632        for index in decoded_indices {
633            if (index as usize) < sorted_hashes.len() {
634                indices_to_remove.insert(index as usize);
635            } else {
636                return Err(DatabaseError::InvalidIndices(format!(
637                    "Index out of bounds: {} (max: {})",
638                    index,
639                    sorted_hashes.len()
640                )));
641            }
642        }
643
644        // Remove the hashes at the specified indices
645        for (i, hash) in sorted_hashes.iter().enumerate() {
646            if indices_to_remove.contains(&i) {
647                hash_set.remove(hash);
648            }
649        }
650
651        Ok(())
652    }
653
654    /// Update threat list from API response
655    async fn update_threat_list(
656        &self,
657        api: &SafeBrowsingApi,
658        threat_descriptor: &ThreatDescriptor,
659    ) -> Result<()> {
660        info!("Updating threat list: {:?}", threat_descriptor);
661
662        // Get current client state
663        let client_state = {
664            let cache = self.cache.read().await;
665            cache
666                .get(threat_descriptor)
667                .map(|entry| entry.client_state.clone())
668                .unwrap_or_default()
669        };
670
671        // Fetch updates from API
672        let response = api
673            .fetch_threat_list_update(threat_descriptor, &client_state)
674            .await
675            .map_err(|e| DatabaseError::ApiError(e))?;
676
677        if response.list_update_responses.is_empty() {
678            return Ok(());
679        }
680
681        let list_update = &response.list_update_responses[0];
682        let response_type = list_update.response_type;
683
684        // Process based on response type
685        match response_type {
686            0 => {
687                // Unspecified - check if this is actually a full update
688                debug!("Unspecified update type, checking for additions/removals");
689                if list_update.additions.is_empty() && list_update.removals.is_empty() {
690                    debug!("No additions or removals, skipping update");
691                    return Ok(());
692                }
693                // If we have additions/removals, process as partial update
694                self.process_partial_update(threat_descriptor, list_update)
695                    .await?;
696            }
697            1 => {
698                // Partial update - apply changes to existing data
699                debug!("Processing partial update");
700                self.process_partial_update(threat_descriptor, list_update)
701                    .await?;
702            }
703            2 => {
704                // Full update - replace entire list
705                debug!("Processing full update");
706                self.process_full_update(threat_descriptor, list_update)
707                    .await?;
708            }
709            _ => {
710                return Err(DatabaseError::DecodeError(format!(
711                    "Unknown response type: {}",
712                    response_type
713                )));
714            }
715        }
716
717        Ok(())
718    }
719}
720
721#[async_trait]
722impl Database for RedbDatabase {
723    async fn is_ready(&self) -> Result<bool> {
724        // Check if we have metadata indicating initialization
725        let has_metadata = match self.get_metadata(INITIALIZED_KEY) {
726            Ok(Some(_)) => true,
727            Ok(None) => false,
728            Err(_) => false,
729        };
730
731        if !has_metadata {
732            return Ok(false);
733        }
734
735        // Also check if cache has actual data
736        let cache = self.cache.read().await;
737        let has_data =
738            !cache.is_empty() && cache.values().any(|entry| !entry.hash_prefixes.is_empty());
739
740        if !has_data {
741            warn!("Database metadata indicates initialization but cache is empty - forcing update");
742            return Ok(false);
743        }
744
745        Ok(true)
746    }
747
748    async fn status(&self) -> Result<()> {
749        // Check if database is stale
750        if let Some(duration) = self.time_since_last_update().await {
751            if duration > self.max_age {
752                return Err(DatabaseError::Stale(duration));
753            }
754        }
755
756        // Check if database is ready
757        if !Database::is_ready(self).await? {
758            return Err(DatabaseError::NotReady);
759        }
760
761        Ok(())
762    }
763
764    async fn update(&self, api: &SafeBrowsingApi, threat_lists: &[ThreatDescriptor]) -> Result<()> {
765        for threat_descriptor in threat_lists {
766            // Get current client state
767            let client_state = {
768                let cache = self.cache.read().await;
769                cache
770                    .get(threat_descriptor)
771                    .map(|entry| entry.client_state.clone())
772                    .unwrap_or_default()
773            };
774
775            // Fetch updates from API
776            let response = api
777                .fetch_threat_list_update(threat_descriptor, &client_state)
778                .await
779                .map_err(|e| DatabaseError::ApiError(e))?;
780
781            if response.list_update_responses.is_empty() {
782                continue;
783            }
784
785            let list_update = &response.list_update_responses[0];
786            let response_type = list_update.response_type;
787
788            // Get the current hash set
789            let mut hash_set = {
790                let cache = self.cache.read().await;
791                match cache.get(threat_descriptor) {
792                    Some(entry) => entry.to_hash_set()?,
793                    None => HashPrefixSet::new(),
794                }
795            };
796
797            // Process based on response type
798            match response_type {
799                0 => {
800                    // Unspecified - check if this is actually a full update
801                    if list_update.additions.is_empty() && list_update.removals.is_empty() {
802                        continue;
803                    }
804                    // If we have additions/removals, process as partial update
805                    for removal_set in &list_update.removals {
806                        self.process_raw_hashes_removal(&mut hash_set, removal_set)?;
807                    }
808                    for addition_set in &list_update.additions {
809                        self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
810                    }
811                }
812                1 => {
813                    // Partial update - apply changes to existing data
814                    for removal_set in &list_update.removals {
815                        self.process_raw_hashes_removal(&mut hash_set, removal_set)?;
816                    }
817                    for addition_set in &list_update.additions {
818                        self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
819                    }
820                }
821                2 => {
822                    // Full update - replace entire list
823                    hash_set = HashPrefixSet::new();
824                    for addition_set in &list_update.additions {
825                        self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
826                    }
827                }
828                _ => {
829                    return Err(DatabaseError::DecodeError(format!(
830                        "Unknown response type: {}",
831                        response_type
832                    )));
833                }
834            }
835
836            // Create new entry
837            let entry = StoredThreatListEntry::from_hash_set(
838                &hash_set,
839                list_update.new_client_state.clone().to_vec(),
840                list_update
841                    .checksum
842                    .as_ref()
843                    .map_or_else(Vec::new, |c| c.sha256.clone().to_vec()),
844            );
845
846            // Store in a single transaction
847            let threat_descriptor_key = serde_json::to_string(threat_descriptor).map_err(|e| {
848                DatabaseError::DecodeError(format!("Failed to serialize threat descriptor: {}", e))
849            })?;
850
851            let entry_value = serde_json::to_vec(&entry).map_err(|e| {
852                DatabaseError::DecodeError(format!("Failed to serialize threat list entry: {}", e))
853            })?;
854
855            let write_txn = self.db.begin_write().map_err(|e| {
856                DatabaseError::DecodeError(format!("Failed to begin write transaction: {}", e))
857            })?;
858
859            {
860                let mut table = write_txn.open_table(THREAT_LISTS_TABLE).map_err(|e| {
861                    DatabaseError::DecodeError(format!("Failed to open threat_lists table: {}", e))
862                })?;
863
864                table
865                    .insert(threat_descriptor_key.as_str(), entry_value.as_slice())
866                    .map_err(|e| {
867                        DatabaseError::DecodeError(format!("Failed to insert threat list: {}", e))
868                    })?;
869            }
870
871            write_txn.commit().map_err(|e| {
872                DatabaseError::DecodeError(format!("Failed to commit transaction: {}", e))
873            })?;
874
875            // Update cache
876            {
877                let mut cache = self.cache.write().await;
878                cache.insert(threat_descriptor.clone(), entry);
879            }
880        }
881
882        // Update metadata in separate transactions
883        let now = SystemTime::now()
884            .duration_since(UNIX_EPOCH)
885            .unwrap_or_default()
886            .as_secs();
887
888        self.store_metadata(LAST_UPDATE_KEY, &now.to_le_bytes())?;
889        self.store_metadata(INITIALIZED_KEY, &[1u8])?;
890        self.update_hash_count().await?;
891
892        Ok(())
893    }
894
895    async fn lookup(
896        &self,
897        hash: &HashPrefix,
898    ) -> Result<Option<(HashPrefix, Vec<ThreatDescriptor>)>> {
899        let cache = self.cache.read().await;
900        let mut matching_threats = Vec::new();
901
902        // Search through all threat lists
903        for (threat_descriptor, entry) in cache.iter() {
904            let hash_set = entry.to_hash_set()?;
905            if hash_set.contains(hash) {
906                matching_threats.push(threat_descriptor.clone());
907            }
908        }
909
910        if matching_threats.is_empty() {
911            Ok(None)
912        } else {
913            Ok(Some((hash.clone(), matching_threats)))
914        }
915    }
916
917    async fn time_since_last_update(&self) -> Option<Duration> {
918        match self.get_metadata(LAST_UPDATE_KEY) {
919            Ok(Some(bytes)) if bytes.len() == 8 => {
920                let timestamp = u64::from_le_bytes([
921                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
922                ]);
923
924                let now = SystemTime::now()
925                    .duration_since(UNIX_EPOCH)
926                    .unwrap_or_default()
927                    .as_secs();
928
929                Some(Duration::from_secs(now.saturating_sub(timestamp)))
930            }
931            _ => None,
932        }
933    }
934
935    async fn stats(&self) -> DatabaseStats {
936        let cache = self.cache.read().await;
937
938        let total_hashes = cache.values().map(|entry| entry.hash_prefixes.len()).sum();
939
940        let threat_lists = cache.len();
941
942        let memory_usage = std::mem::size_of::<RedbDatabase>()
943            + cache
944                .iter()
945                .map(|(k, v)| {
946                    std::mem::size_of_val(k)
947                        + std::mem::size_of_val(v)
948                        + v.hash_prefixes.iter().map(|h| h.len()).sum::<usize>()
949                        + v.client_state.len()
950                        + v.checksum.len()
951                })
952                .sum::<usize>();
953
954        let is_stale = self
955            .time_since_last_update()
956            .await
957            .map(|duration| duration > self.max_age)
958            .unwrap_or(true);
959
960        let last_update = match self.get_metadata(LAST_UPDATE_KEY) {
961            Ok(Some(bytes)) if bytes.len() == 8 => {
962                let timestamp = u64::from_le_bytes([
963                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
964                ]);
965
966                Some(std::time::UNIX_EPOCH + Duration::from_secs(timestamp))
967                    .and_then(|t| t.elapsed().ok())
968                    .map(|elapsed| std::time::Instant::now() - elapsed)
969            }
970            _ => None,
971        };
972
973        DatabaseStats {
974            total_hashes,
975            threat_lists,
976            memory_usage,
977            last_update,
978            is_stale,
979        }
980    }
981}
982
983impl Default for RedbDatabase {
984    fn default() -> Self {
985        let db =
986            Self::new(Self::default_path().unwrap_or_else(|_| PathBuf::from("safebrowsing.redb")))
987                .expect("Failed to create default RedbDatabase");
988
989        // Note: Default implementation doesn't load cache automatically
990        // Call init() separately for async initialization
991        db
992    }
993}
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998    use safebrowsing_api::{PlatformType, ThreatEntryType, ThreatType};
999    use tempfile::tempdir;
1000
1001    fn create_test_threat_descriptor() -> ThreatDescriptor {
1002        ThreatDescriptor {
1003            threat_type: ThreatType::Malware,
1004            platform_type: PlatformType::AnyPlatform,
1005            threat_entry_type: ThreatEntryType::Url,
1006        }
1007    }
1008
1009    #[tokio::test]
1010    async fn test_database_creation() {
1011        let temp_dir = tempdir().unwrap();
1012        let db_path = temp_dir.path().join("test.redb");
1013
1014        let db = RedbDatabase::new(&db_path).unwrap();
1015        db.init().await.unwrap();
1016        assert!(db_path.exists());
1017    }
1018
1019    #[tokio::test]
1020    async fn test_database_stats() {
1021        let temp_dir = tempdir().unwrap();
1022        let db_path = temp_dir.path().join("test.redb");
1023
1024        let db = RedbDatabase::new(&db_path).unwrap();
1025        db.init().await.unwrap();
1026        let stats = db.stats().await;
1027
1028        assert_eq!(stats.total_hashes, 0);
1029        assert_eq!(stats.threat_lists, 0);
1030        assert!(stats.is_stale);
1031    }
1032
1033    #[tokio::test]
1034    async fn test_metadata_storage() {
1035        let temp_dir = tempdir().unwrap();
1036        let db_path = temp_dir.path().join("test.redb");
1037
1038        let db = RedbDatabase::new(&db_path).unwrap();
1039        db.init().await.unwrap();
1040
1041        // Test storing and retrieving metadata
1042        db.store_metadata("test_key", b"test_value").unwrap();
1043        let value = db.get_metadata("test_key").unwrap();
1044
1045        assert_eq!(value, Some(b"test_value".to_vec()));
1046    }
1047
1048    #[tokio::test]
1049    async fn test_default_path() {
1050        let path = RedbDatabase::default_path().unwrap();
1051        assert!(path.to_string_lossy().contains("safebrowsing"));
1052        assert!(path.extension().map(|s| s == "redb").unwrap_or(false));
1053    }
1054
1055    #[tokio::test]
1056    async fn test_hash_processing() {
1057        let temp_dir = tempdir().unwrap();
1058        let db_path = temp_dir.path().join("test.redb");
1059
1060        let db = RedbDatabase::new(&db_path).unwrap();
1061        db.init().await.unwrap();
1062
1063        // Test raw hash processing
1064        let mut hash_set = HashPrefixSet::new();
1065
1066        // Create mock raw hashes (4-byte prefixes)
1067        let raw_hashes = safebrowsing_proto::RawHashes {
1068            prefix_size: 4,
1069            raw_hashes: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08].into(),
1070        };
1071
1072        db.process_raw_hashes(&mut hash_set, &raw_hashes).unwrap();
1073
1074        // Should have 2 hash prefixes (8 bytes / 4 bytes per prefix)
1075        assert_eq!(hash_set.len(), 2);
1076
1077        // Test that we can find the hashes
1078        let prefix1 = HashPrefix::new(vec![0x01, 0x02, 0x03, 0x04]).unwrap();
1079        let prefix2 = HashPrefix::new(vec![0x05, 0x06, 0x07, 0x08]).unwrap();
1080
1081        assert!(hash_set.contains(&prefix1));
1082        assert!(hash_set.contains(&prefix2));
1083    }
1084
1085    #[tokio::test]
1086    async fn test_threat_entry_processing() {
1087        let temp_dir = tempdir().unwrap();
1088        let db_path = temp_dir.path().join("test.redb");
1089
1090        let db = RedbDatabase::new(&db_path).unwrap();
1091        db.init().await.unwrap();
1092
1093        // Test processing a threat entry set with raw compression
1094        let mut hash_set = HashPrefixSet::new();
1095
1096        let threat_entry_set = safebrowsing_proto::ThreatEntrySet {
1097            compression_type: safebrowsing_proto::CompressionType::Raw as i32,
1098            raw_hashes: Some(safebrowsing_proto::RawHashes {
1099                prefix_size: 4,
1100                raw_hashes: vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11].into(),
1101            }),
1102            rice_hashes: None,
1103            raw_indices: None,
1104            rice_indices: None,
1105        };
1106
1107        db.process_raw_hashes_addition(&mut hash_set, &threat_entry_set)
1108            .unwrap();
1109
1110        // Should have processed 2 hash prefixes
1111        assert_eq!(hash_set.len(), 2);
1112
1113        // Verify the specific hashes are present
1114        let prefix1 = HashPrefix::new(vec![0xAA, 0xBB, 0xCC, 0xDD]).unwrap();
1115        let prefix2 = HashPrefix::new(vec![0xEE, 0xFF, 0x00, 0x11]).unwrap();
1116
1117        assert!(hash_set.contains(&prefix1));
1118        assert!(hash_set.contains(&prefix2));
1119    }
1120
1121    #[tokio::test]
1122    async fn test_full_update_and_lookup() {
1123        use safebrowsing_api::{PlatformType, ThreatEntryType, ThreatType};
1124
1125        let temp_dir = tempdir().unwrap();
1126        let db_path = temp_dir.path().join("test.redb");
1127
1128        let db = RedbDatabase::new(&db_path).unwrap();
1129        db.init().await.unwrap();
1130
1131        // Create a threat descriptor
1132        let threat_descriptor = ThreatDescriptor {
1133            threat_type: ThreatType::Malware,
1134            platform_type: PlatformType::AnyPlatform,
1135            threat_entry_type: ThreatEntryType::Url,
1136        };
1137
1138        // Create a mock list update response
1139        let list_update =
1140            safebrowsing_proto::fetch_threat_list_updates_response::ListUpdateResponse {
1141                threat_type: ThreatType::Malware as i32,
1142                threat_entry_type: ThreatEntryType::Url as i32,
1143                platform_type: PlatformType::AnyPlatform as i32,
1144                response_type: 2, // Full update
1145                additions: vec![safebrowsing_proto::ThreatEntrySet {
1146                    compression_type: safebrowsing_proto::CompressionType::Raw as i32,
1147                    raw_hashes: Some(safebrowsing_proto::RawHashes {
1148                        prefix_size: 4,
1149                        raw_hashes: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0].into(),
1150                    }),
1151                    rice_hashes: None,
1152                    raw_indices: None,
1153                    rice_indices: None,
1154                }],
1155                removals: vec![],
1156                new_client_state: vec![0x01, 0x02, 0x03].into(),
1157                checksum: Some(safebrowsing_proto::Checksum {
1158                    sha256: vec![0xAA, 0xBB, 0xCC, 0xDD].into(),
1159                }),
1160            };
1161
1162        // Process the update
1163        db.process_full_update(&threat_descriptor, &list_update)
1164            .await
1165            .unwrap();
1166
1167        // Verify the database now has hashes
1168        let stats = db.stats().await;
1169        assert_eq!(stats.threat_lists, 1);
1170        assert_eq!(stats.total_hashes, 2); // 8 bytes / 4 bytes per hash = 2 hashes
1171
1172        // Test lookup - should find matching hash
1173        let test_hash = HashPrefix::new(vec![0x12, 0x34, 0x56, 0x78]).unwrap();
1174        let result = db.lookup(&test_hash).await.unwrap();
1175        assert!(result.is_some());
1176        let (found_hash, threats) = result.unwrap();
1177        assert_eq!(found_hash, test_hash);
1178        assert_eq!(threats.len(), 1);
1179        assert_eq!(threats[0], threat_descriptor);
1180
1181        // Test lookup - should not find non-matching hash
1182        let non_matching_hash = HashPrefix::new(vec![0xFF, 0xFF, 0xFF, 0xFF]).unwrap();
1183        let result = db.lookup(&non_matching_hash).await.unwrap();
1184        assert!(result.is_none());
1185
1186        // Verify persistence - create new instance and check data survives
1187        drop(db);
1188        let db2 = RedbDatabase::new(&db_path).unwrap();
1189        db2.init().await.unwrap();
1190
1191        let stats2 = db2.stats().await;
1192        assert_eq!(stats2.threat_lists, 1);
1193        assert_eq!(stats2.total_hashes, 2);
1194
1195        // Verify lookups still work after reload
1196        let result2 = db2.lookup(&test_hash).await.unwrap();
1197        assert!(result2.is_some());
1198    }
1199}