1use 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
24const 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
29const LAST_UPDATE_KEY: &str = "last_update";
31const INITIALIZED_KEY: &str = "initialized";
32const HASH_COUNT_KEY: &str = "hash_count";
33
34#[derive(Serialize, Deserialize, Clone)]
36struct StoredThreatListEntry {
37 hash_prefixes: Vec<Vec<u8>>,
39 client_state: Vec<u8>,
41 checksum: Vec<u8>,
43 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
76pub struct RedbDatabase {
78 db: Arc<RedbDb>,
80 cache: Arc<RwLock<HashMap<ThreatDescriptor, StoredThreatListEntry>>>,
82 max_age: Duration,
84 path: PathBuf,
86}
87
88impl RedbDatabase {
89 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 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 {
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 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 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 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 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 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 {
239 let mut cache = self.cache.write().await;
240 cache.insert(threat_descriptor.clone(), entry.clone());
241 }
242
243 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 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 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 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 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 for addition_set in &list_update.additions {
358 self.process_raw_hashes_addition(&mut hash_set, addition_set)?;
359 }
360
361 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 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 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 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 ); self.process_raw_hashes_removal(&mut hash_set, removal_set)?;
408 }
409
410 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 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 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 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 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 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 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 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 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 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 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 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 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 fn process_raw_indices(
588 &self,
589 hash_set: &mut HashPrefixSet,
590 raw_indices: &safebrowsing_proto::RawIndices,
591 ) -> Result<()> {
592 let sorted_hashes = hash_set.to_sorted_vec();
594
595 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 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 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 let sorted_hashes = hash_set.to_sorted_vec();
629
630 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 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 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 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 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 match response_type {
686 0 => {
687 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 self.process_partial_update(threat_descriptor, list_update)
695 .await?;
696 }
697 1 => {
698 debug!("Processing partial update");
700 self.process_partial_update(threat_descriptor, list_update)
701 .await?;
702 }
703 2 => {
704 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 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 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 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 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 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 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 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 match response_type {
799 0 => {
800 if list_update.additions.is_empty() && list_update.removals.is_empty() {
802 continue;
803 }
804 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 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 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 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 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 {
877 let mut cache = self.cache.write().await;
878 cache.insert(threat_descriptor.clone(), entry);
879 }
880 }
881
882 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 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 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 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 let mut hash_set = HashPrefixSet::new();
1065
1066 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 assert_eq!(hash_set.len(), 2);
1076
1077 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 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 assert_eq!(hash_set.len(), 2);
1112
1113 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 let threat_descriptor = ThreatDescriptor {
1133 threat_type: ThreatType::Malware,
1134 platform_type: PlatformType::AnyPlatform,
1135 threat_entry_type: ThreatEntryType::Url,
1136 };
1137
1138 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, 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 db.process_full_update(&threat_descriptor, &list_update)
1164 .await
1165 .unwrap();
1166
1167 let stats = db.stats().await;
1169 assert_eq!(stats.threat_lists, 1);
1170 assert_eq!(stats.total_hashes, 2); 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 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 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 let result2 = db2.lookup(&test_hash).await.unwrap();
1197 assert!(result2.is_some());
1198 }
1199}