1use crate::error::FormatError;
7use crate::mmdb::types::RecordSize;
8use crate::validation::EntryValidator;
9use matchy_data_format::{DataEncoder, DataValue};
10use matchy_ip_trie::IpTreeBuilder;
11use matchy_literal_hash::LiteralHashBuilder;
12use matchy_match_mode::MatchMode;
13use matchy_paraglob::ParaglobBuilder;
14use rustc_hash::FxHasher;
15use std::collections::HashMap;
16use std::hash::{Hash, Hasher};
17use std::net::IpAddr;
18
19#[derive(Debug, Clone)]
21pub enum EntryType {
22 IpAddress {
24 addr: IpAddr,
26 prefix_len: u8,
28 },
29 Literal(String),
31 Glob(String),
33}
34
35#[derive(Debug, Clone)]
37struct EntryRef {
38 entry_type: EntryType,
39 data_offset: u32,
40}
41
42pub struct DatabaseBuilder {
44 entries: Vec<EntryRef>,
45 data_encoder: DataEncoder,
46 data_cache: HashMap<u64, u32>,
47 match_mode: MatchMode,
48 database_type: Option<String>,
49 description: HashMap<String, String>,
50 validator: Option<Box<dyn EntryValidator>>,
51 update_url: Option<String>,
52}
53
54impl DatabaseBuilder {
55 #[must_use]
56 pub fn new(match_mode: MatchMode) -> Self {
57 Self {
58 entries: Vec::new(),
59 data_encoder: DataEncoder::new(),
60 data_cache: HashMap::new(),
61 match_mode,
62 database_type: None,
63 description: HashMap::new(),
64 validator: None,
65 update_url: None,
66 }
67 }
68
69 #[must_use]
82 pub fn with_database_type(mut self, db_type: impl Into<String>) -> Self {
83 self.database_type = Some(db_type.into());
84 self
85 }
86
87 #[must_use]
102 pub fn with_description(
103 mut self,
104 language: impl Into<String>,
105 text: impl Into<String>,
106 ) -> Self {
107 self.description.insert(language.into(), text.into());
108 self
109 }
110
111 #[must_use]
113 pub fn with_validator(mut self, validator: Box<dyn EntryValidator>) -> Self {
114 self.validator = Some(validator);
115 self
116 }
117
118 #[must_use]
132 pub fn with_update_url(mut self, url: impl Into<String>) -> Self {
133 self.update_url = Some(url.into());
134 self
135 }
136
137 #[must_use]
152 pub fn with_match_mode(mut self, match_mode: MatchMode) -> Self {
153 self.match_mode = match_mode;
154 self
155 }
156
157 pub fn set_match_mode(&mut self, match_mode: MatchMode) {
162 self.match_mode = match_mode;
163 }
164
165 fn validate_entry(
167 &self,
168 key: &str,
169 data: &HashMap<String, DataValue>,
170 ) -> Result<(), FormatError> {
171 if let Some(ref validator) = self.validator {
172 validator
173 .validate(key, data)
174 .map_err(|e| FormatError::ValidationError(format!("{e}")))?;
175 }
176 Ok(())
177 }
178
179 pub fn add_entry(
187 &mut self,
188 key: &str,
189 data: HashMap<String, DataValue>,
190 ) -> Result<(), FormatError> {
191 self.validate_entry(key, &data)?;
192 let entry_type = Self::detect_entry_type(key)?;
193 let data_offset = self.encode_and_deduplicate_data(data);
194
195 self.entries.push(EntryRef {
196 entry_type,
197 data_offset,
198 });
199
200 Ok(())
201 }
202
203 pub fn add_literal(
226 &mut self,
227 pattern: &str,
228 data: HashMap<String, DataValue>,
229 ) -> Result<(), FormatError> {
230 self.validate_entry(pattern, &data)?;
231 let data_offset = self.encode_and_deduplicate_data(data);
232 self.entries.push(EntryRef {
233 entry_type: EntryType::Literal(pattern.to_string()),
234 data_offset,
235 });
236 Ok(())
237 }
238
239 pub fn add_glob(
261 &mut self,
262 pattern: &str,
263 data: HashMap<String, DataValue>,
264 ) -> Result<(), FormatError> {
265 self.validate_entry(pattern, &data)?;
266 let data_offset = self.encode_and_deduplicate_data(data);
267 self.entries.push(EntryRef {
268 entry_type: EntryType::Glob(pattern.to_string()),
269 data_offset,
270 });
271 Ok(())
272 }
273
274 fn encode_and_deduplicate_data(&mut self, data: HashMap<String, DataValue>) -> u32 {
276 let data_value = DataValue::Map(data);
278 let mut hasher = FxHasher::default();
279 data_value.hash(&mut hasher);
280 let hash = hasher.finish();
281
282 if let Some(&offset) = self.data_cache.get(&hash) {
284 return offset;
285 }
286
287 let offset = self.data_encoder.encode(&data_value);
289 self.data_cache.insert(hash, offset);
290 offset
291 }
292
293 pub fn add_ip(
322 &mut self,
323 ip_or_cidr: &str,
324 data: HashMap<String, DataValue>,
325 ) -> Result<(), FormatError> {
326 self.validate_entry(ip_or_cidr, &data)?;
327 let entry_type = Self::parse_ip_entry(ip_or_cidr)?;
328 let data_offset = self.encode_and_deduplicate_data(data);
329
330 self.entries.push(EntryRef {
331 entry_type,
332 data_offset,
333 });
334 Ok(())
335 }
336
337 fn parse_ip_entry(key: &str) -> Result<EntryType, FormatError> {
339 if let Ok(addr) = key.parse::<IpAddr>() {
341 let prefix_len = if addr.is_ipv4() { 32 } else { 128 };
342 return Ok(EntryType::IpAddress { addr, prefix_len });
343 }
344
345 if let Some(slash_pos) = key.find('/') {
347 let addr_str = &key[..slash_pos];
348 let prefix_str = &key[slash_pos + 1..];
349
350 if let (Ok(addr), Ok(prefix_len)) =
351 (addr_str.parse::<IpAddr>(), prefix_str.parse::<u8>())
352 {
353 let max_prefix = if addr.is_ipv4() { 32 } else { 128 };
355 if prefix_len <= max_prefix {
356 return Ok(EntryType::IpAddress { addr, prefix_len });
357 }
358 }
359 }
360
361 Err(FormatError::InvalidPattern(format!(
362 "Invalid IP address or CIDR: {key}"
363 )))
364 }
365
366 pub fn detect_entry_type(key: &str) -> Result<EntryType, FormatError> {
392 if let Some(stripped) = key.strip_prefix("literal:") {
394 return Ok(EntryType::Literal(stripped.to_string()));
396 }
397
398 if let Some(stripped) = key.strip_prefix("glob:") {
399 matchy_paraglob::validate_glob_pattern(stripped).map_err(|e| {
401 FormatError::InvalidPattern(format!("Invalid glob pattern syntax: {e}"))
402 })?;
403 return Ok(EntryType::Glob(stripped.to_string()));
404 }
405
406 if let Some(stripped) = key.strip_prefix("ip:") {
407 return Self::parse_ip_entry(stripped);
409 }
410
411 if Self::parse_ip_entry(key).is_ok() {
414 return Self::parse_ip_entry(key);
415 }
416
417 if key.contains('*') || key.contains('?') || key.contains('[') {
419 if matchy_paraglob::validate_glob_pattern(key).is_ok() {
421 return Ok(EntryType::Glob(key.to_string()));
422 }
423 }
425
426 Ok(EntryType::Literal(key.to_string()))
428 }
429
430 pub fn build(mut self) -> Result<Vec<u8>, FormatError> {
432 let data_section = self.data_encoder.into_bytes();
434
435 self.data_cache.clear();
437
438 let entry_count = self.entries.len();
441 let mut ip_entries = Vec::with_capacity(entry_count);
442 let mut literal_entries = Vec::with_capacity(entry_count);
443 let mut glob_entries = Vec::with_capacity(entry_count);
444
445 for entry in &self.entries {
446 match &entry.entry_type {
447 EntryType::IpAddress { addr, prefix_len } => {
448 ip_entries.push((*addr, *prefix_len, entry.data_offset));
449 }
450 EntryType::Literal(pattern) => {
451 literal_entries.push((pattern.as_str(), entry.data_offset));
452 }
453 EntryType::Glob(pattern) => {
454 glob_entries.push((pattern.as_str(), entry.data_offset));
455 }
456 }
457 }
458
459 let (ip_tree_bytes, node_count, record_size, ip_version) = if ip_entries.is_empty() {
462 let record_size = RecordSize::Bits24;
464 let tree_builder = IpTreeBuilder::new_v4(record_size);
465 let (tree_bytes, node_cnt) = tree_builder.build()?;
466 (tree_bytes, node_cnt, record_size, 4)
467 } else {
468 let needs_v6 = ip_entries.iter().any(|(addr, _, _)| addr.is_ipv6());
470
471 let estimated_nodes = ip_entries.len();
477 let record_size = if estimated_nodes > 200_000_000 {
478 RecordSize::Bits32
480 } else if estimated_nodes > 15_000_000 {
481 RecordSize::Bits28
483 } else {
484 RecordSize::Bits24
486 };
487
488 ip_entries.sort_unstable_by(|(addr1, prefix1, _), (addr2, prefix2, _)| {
491 prefix2.cmp(prefix1).then_with(|| addr1.cmp(addr2))
492 });
493
494 let mut tree_builder = if needs_v6 {
495 IpTreeBuilder::new_v6(record_size)
496 } else {
497 IpTreeBuilder::new_v4(record_size)
498 };
499
500 tree_builder.reserve_nodes(estimated_nodes + estimated_nodes / 2);
502
503 for (addr, prefix_len, data_offset) in &ip_entries {
505 tree_builder.insert(*addr, *prefix_len, *data_offset)?;
506 }
507
508 let (tree_bytes, node_cnt) = tree_builder.build()?;
510
511 let ip_ver = if needs_v6 { 6 } else { 4 };
512 (tree_bytes, node_cnt, record_size, ip_ver)
513 };
514
515 let (has_globs, glob_section_bytes) = if glob_entries.is_empty() {
517 (false, Vec::new())
518 } else {
519 let mut pattern_builder = ParaglobBuilder::new(self.match_mode);
520 let mut pattern_data = Vec::with_capacity(glob_entries.len());
521
522 for (pattern, data_offset) in &glob_entries {
523 let pattern_id = pattern_builder.add_pattern(pattern)?;
524 pattern_data.push((pattern_id, *data_offset));
525 }
526
527 let paraglob = pattern_builder.build()?;
528 let paraglob_bytes = paraglob.buffer().to_vec();
529
530 let mut section = Vec::new();
532
533 let size_placeholder = vec![0u8; 8]; section.extend_from_slice(&size_placeholder);
536
537 section.extend_from_slice(¶glob_bytes);
539
540 let pattern_count =
542 u32::try_from(pattern_data.len()).expect("Pattern count exceeds u32::MAX");
543 section.extend_from_slice(&pattern_count.to_le_bytes());
544 for (_pattern_id, data_offset) in pattern_data {
545 section.extend_from_slice(&data_offset.to_le_bytes());
546 }
547
548 let total_size =
550 u32::try_from(section.len()).expect("Glob section exceeds u32::MAX bytes");
551 let paraglob_size =
552 u32::try_from(paraglob_bytes.len()).expect("Paraglob data exceeds u32::MAX bytes");
553 section[0..4].copy_from_slice(&total_size.to_le_bytes());
554 section[4..8].copy_from_slice(¶glob_size.to_le_bytes());
555
556 (true, section)
557 };
558
559 let (has_literals, literal_section_bytes) = if literal_entries.is_empty() {
561 (false, Vec::new())
562 } else {
563 let mut literal_builder = LiteralHashBuilder::new(self.match_mode);
564 let mut literal_pattern_data = Vec::with_capacity(literal_entries.len());
565
566 for (next_pattern_id, (literal, data_offset)) in literal_entries.iter().enumerate() {
567 let pid = u32::try_from(next_pattern_id).expect("Literal pattern ID exceeds u32");
568 literal_builder.add_pattern(literal, pid);
569 literal_pattern_data.push((pid, *data_offset));
570 }
571
572 let literal_bytes = literal_builder.build(&literal_pattern_data)?;
573 (true, literal_bytes)
574 };
575
576 let mut database = Vec::new();
578
579 database.extend_from_slice(&ip_tree_bytes);
581 database
582 .extend_from_slice(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); database.extend_from_slice(&data_section);
586
587 if has_globs {
590 let current_offset = database.len() + 16; let padding_needed = (4 - (current_offset % 4)) % 4;
592 database.extend(std::iter::repeat_n(0u8, padding_needed));
593 }
594
595 {
597 let mut metadata = HashMap::new();
599 metadata.insert(
600 "binary_format_major_version".to_string(),
601 DataValue::Uint16(2),
602 );
603 metadata.insert(
604 "binary_format_minor_version".to_string(),
605 DataValue::Uint16(0),
606 );
607 metadata.insert(
608 "build_epoch".to_string(),
609 DataValue::Uint64(
610 web_time::SystemTime::now()
611 .duration_since(web_time::UNIX_EPOCH)
612 .map(|d| d.as_secs())
613 .unwrap_or(0),
614 ),
615 );
616 let db_type = self.database_type.clone().unwrap_or_else(|| {
618 if has_globs || !literal_entries.is_empty() {
619 if ip_entries.is_empty() {
620 "Paraglob-Pattern".to_string()
621 } else {
622 "Paraglob-Combined-IP-Pattern".to_string()
623 }
624 } else {
625 "Paraglob-IP".to_string()
626 }
627 });
628 metadata.insert("database_type".to_string(), DataValue::String(db_type));
629
630 let description_map = if self.description.is_empty() {
632 let mut desc = HashMap::new();
633 desc.insert(
634 "en".to_string(),
635 DataValue::String(
636 "Paraglob unified database with IP and pattern matching".to_string(),
637 ),
638 );
639 desc
640 } else {
641 self.description
642 .iter()
643 .map(|(k, v)| (k.clone(), DataValue::String(v.clone())))
644 .collect()
645 };
646 metadata.insert("description".to_string(), DataValue::Map(description_map));
647 metadata.insert(
648 "languages".to_string(),
649 DataValue::Array(vec![DataValue::String("en".to_string())]),
650 );
651 metadata.insert(
652 "ip_version".to_string(),
653 DataValue::Uint16(u16::try_from(ip_version).unwrap()),
654 );
655 metadata.insert("node_count".to_string(), DataValue::Uint32(node_count));
656 metadata.insert(
657 "record_size".to_string(),
658 DataValue::Uint16(match record_size {
659 RecordSize::Bits24 => 24,
660 RecordSize::Bits28 => 28,
661 RecordSize::Bits32 => 32,
662 }),
663 );
664
665 metadata.insert(
667 "ip_entry_count".to_string(),
668 DataValue::Uint32(u32::try_from(ip_entries.len()).unwrap_or(u32::MAX)),
669 );
670 metadata.insert(
671 "literal_entry_count".to_string(),
672 DataValue::Uint32(u32::try_from(literal_entries.len()).unwrap_or(u32::MAX)),
673 );
674 metadata.insert(
675 "glob_entry_count".to_string(),
676 DataValue::Uint32(u32::try_from(glob_entries.len()).unwrap_or(u32::MAX)),
677 );
678
679 let match_mode_value = match self.match_mode {
681 MatchMode::CaseSensitive => 0u16,
682 MatchMode::CaseInsensitive => 1u16,
683 };
684 metadata.insert(
685 "match_mode".to_string(),
686 DataValue::Uint16(match_mode_value),
687 );
688
689 if let Some(ref url) = self.update_url {
690 metadata.insert("update_url".to_string(), DataValue::String(url.clone()));
691 }
692
693 let tree_and_separator_size = ip_tree_bytes.len() + 16;
696 let data_section_size = data_section.len();
697
698 let padding_before_paraglob = if has_globs {
700 let current_offset = tree_and_separator_size + data_section_size + 16; (4 - (current_offset % 4)) % 4
702 } else {
703 0
704 };
705
706 let pattern_offset = if has_globs {
709 tree_and_separator_size + data_section_size + padding_before_paraglob + 16
710 } else {
712 0 };
714 metadata.insert(
715 "pattern_section_offset".to_string(),
716 DataValue::Uint32(
717 u32::try_from(pattern_offset).expect("Pattern section offset exceeds u32::MAX"),
718 ),
719 );
720
721 let literal_offset = if has_literals {
724 if has_globs {
725 tree_and_separator_size
726 + data_section_size
727 + padding_before_paraglob
728 + 16
729 + glob_section_bytes.len()
730 + 16
731 } else {
732 tree_and_separator_size + data_section_size + 16 }
734 } else {
735 0 };
737 metadata.insert(
738 "literal_section_offset".to_string(),
739 DataValue::Uint32(
740 u32::try_from(literal_offset).expect("Literal section offset exceeds u32::MAX"),
741 ),
742 );
743
744 let mut meta_encoder = DataEncoder::new();
746 let metadata_value = DataValue::Map(metadata);
747 meta_encoder.encode(&metadata_value);
748 let metadata_bytes = meta_encoder.into_bytes();
749
750 if has_globs {
755 database.extend_from_slice(b"MMDB_PATTERN\x00\x00\x00\x00");
756 database.extend_from_slice(&glob_section_bytes);
757 }
758
759 if has_literals {
761 database.extend_from_slice(b"MMDB_LITERAL\x00\x00\x00\x00");
762 database.extend_from_slice(&literal_section_bytes);
763 }
764
765 database.extend_from_slice(b"\xAB\xCD\xEFMaxMind.com");
767 database.extend_from_slice(&metadata_bytes);
768 }
769
770 Ok(database)
771 }
772
773 #[must_use]
775 pub fn stats(&self) -> BuilderStats {
776 let mut ip_count = 0;
777 let mut literal_count = 0;
778 let mut glob_count = 0;
779
780 for entry in &self.entries {
781 match &entry.entry_type {
782 EntryType::IpAddress { .. } => ip_count += 1,
783 EntryType::Literal(_) => literal_count += 1,
784 EntryType::Glob(_) => glob_count += 1,
785 }
786 }
787
788 BuilderStats {
789 total_entries: self.entries.len(),
790 ip_entries: ip_count,
791 literal_entries: literal_count,
792 glob_entries: glob_count,
793 }
794 }
795}
796
797#[derive(Debug, Clone)]
799pub struct BuilderStats {
800 pub total_entries: usize,
802 pub ip_entries: usize,
804 pub literal_entries: usize,
806 pub glob_entries: usize,
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_detect_ip_address() {
816 let result = DatabaseBuilder::detect_entry_type("8.8.8.8").unwrap();
817 match result {
818 EntryType::IpAddress { addr, prefix_len } => {
819 assert_eq!(addr.to_string(), "8.8.8.8");
820 assert_eq!(prefix_len, 32);
821 }
822 _ => panic!("Expected IP address"),
823 }
824 }
825
826 #[test]
827 fn test_detect_cidr() {
828 let result = DatabaseBuilder::detect_entry_type("192.168.0.0/16").unwrap();
829 match result {
830 EntryType::IpAddress { addr, prefix_len } => {
831 assert_eq!(addr.to_string(), "192.168.0.0");
832 assert_eq!(prefix_len, 16);
833 }
834 _ => panic!("Expected CIDR"),
835 }
836 }
837
838 #[test]
839 fn test_detect_ipv6() {
840 let result = DatabaseBuilder::detect_entry_type("2001:4860:4860::8888").unwrap();
841 match result {
842 EntryType::IpAddress { addr, prefix_len } => {
843 assert!(addr.is_ipv6());
844 assert_eq!(prefix_len, 128);
845 }
846 _ => panic!("Expected IPv6"),
847 }
848 }
849
850 #[test]
851 fn test_detect_pattern_wildcard() {
852 let result = DatabaseBuilder::detect_entry_type("*.evil.com").unwrap();
853 match result {
854 EntryType::Glob(p) => assert_eq!(p, "*.evil.com"),
855 _ => panic!("Expected glob pattern"),
856 }
857 }
858
859 #[test]
860 fn test_detect_pattern_literal() {
861 let result = DatabaseBuilder::detect_entry_type("evil.com").unwrap();
862 match result {
863 EntryType::Literal(p) => assert_eq!(p, "evil.com"),
864 _ => panic!("Expected literal pattern"),
865 }
866 }
867
868 #[test]
871 fn test_literal_prefix_forces_literal() {
872 let result = DatabaseBuilder::detect_entry_type("literal:*.not-a-glob.com").unwrap();
874 match result {
875 EntryType::Literal(p) => assert_eq!(p, "*.not-a-glob.com"),
876 _ => panic!("Expected literal, got: {result:?}"),
877 }
878 }
879
880 #[test]
881 fn test_literal_prefix_strips_correctly() {
882 let result = DatabaseBuilder::detect_entry_type("literal:evil.example.com").unwrap();
883 match result {
884 EntryType::Literal(p) => {
885 assert_eq!(p, "evil.example.com");
886 assert!(!p.starts_with("literal:"));
887 }
888 _ => panic!("Expected literal"),
889 }
890 }
891
892 #[test]
893 fn test_glob_prefix_forces_glob() {
894 let result = DatabaseBuilder::detect_entry_type("glob:no-wildcards.com").unwrap();
896 match result {
897 EntryType::Glob(p) => assert_eq!(p, "no-wildcards.com"),
898 _ => panic!("Expected glob, got: {result:?}"),
899 }
900 }
901
902 #[test]
903 fn test_glob_prefix_with_wildcards() {
904 let result = DatabaseBuilder::detect_entry_type("glob:*.evil.com").unwrap();
905 match result {
906 EntryType::Glob(p) => {
907 assert_eq!(p, "*.evil.com");
908 assert!(!p.starts_with("glob:"));
909 }
910 _ => panic!("Expected glob"),
911 }
912 }
913
914 #[test]
915 fn test_glob_prefix_invalid_pattern() {
916 let result = DatabaseBuilder::detect_entry_type("glob:[unclosed");
918 assert!(result.is_err());
919 assert!(result
920 .unwrap_err()
921 .to_string()
922 .contains("Invalid glob pattern syntax"));
923 }
924
925 #[test]
926 fn test_ip_prefix_forces_ip() {
927 let result = DatabaseBuilder::detect_entry_type("ip:8.8.8.8").unwrap();
928 match result {
929 EntryType::IpAddress { addr, prefix_len } => {
930 assert_eq!(addr.to_string(), "8.8.8.8");
931 assert_eq!(prefix_len, 32);
932 }
933 _ => panic!("Expected IP address"),
934 }
935 }
936
937 #[test]
938 fn test_ip_prefix_with_cidr() {
939 let result = DatabaseBuilder::detect_entry_type("ip:10.0.0.0/8").unwrap();
940 match result {
941 EntryType::IpAddress { addr, prefix_len } => {
942 assert_eq!(addr.to_string(), "10.0.0.0");
943 assert_eq!(prefix_len, 8);
944 }
945 _ => panic!("Expected CIDR"),
946 }
947 }
948
949 #[test]
950 fn test_ip_prefix_invalid_ip() {
951 let result = DatabaseBuilder::detect_entry_type("ip:not-an-ip");
952 assert!(result.is_err());
953 }
954
955 #[test]
956 fn test_auto_detection_still_works() {
957 assert!(matches!(
959 DatabaseBuilder::detect_entry_type("1.2.3.4"),
960 Ok(EntryType::IpAddress { .. })
961 ));
962 assert!(matches!(
963 DatabaseBuilder::detect_entry_type("*.example.com"),
964 Ok(EntryType::Glob(_))
965 ));
966 assert!(matches!(
967 DatabaseBuilder::detect_entry_type("example.com"),
968 Ok(EntryType::Literal(_))
969 ));
970 }
971
972 #[test]
973 fn test_prefix_case_sensitive() {
974 let result = DatabaseBuilder::detect_entry_type("LITERAL:test.com").unwrap();
976 match result {
978 EntryType::Literal(p) => {
979 assert_eq!(p, "LITERAL:test.com");
981 }
982 _ => panic!("Expected literal"),
983 }
984 }
985
986 #[test]
987 fn test_literal_prefix_with_question_mark() {
988 let result = DatabaseBuilder::detect_entry_type("literal:file?.txt").unwrap();
989 match result {
990 EntryType::Literal(p) => assert_eq!(p, "file?.txt"),
991 _ => panic!("Expected literal"),
992 }
993 }
994
995 #[test]
996 fn test_literal_prefix_with_brackets() {
997 let result = DatabaseBuilder::detect_entry_type("literal:file[1].txt").unwrap();
998 match result {
999 EntryType::Literal(p) => assert_eq!(p, "file[1].txt"),
1000 _ => panic!("Expected literal"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_builder_add_entry_with_prefix() {
1006 let mut builder = DatabaseBuilder::new(MatchMode::CaseSensitive);
1008
1009 builder
1011 .add_entry("literal:*.test.com", HashMap::new())
1012 .unwrap();
1013
1014 let stats = builder.stats();
1015 assert_eq!(stats.literal_entries, 1);
1016 assert_eq!(stats.glob_entries, 0);
1017 }
1018
1019 #[test]
1020 fn test_builder_add_entry_glob_prefix() {
1021 let mut builder = DatabaseBuilder::new(MatchMode::CaseSensitive);
1022
1023 builder.add_entry("glob:test.com", HashMap::new()).unwrap();
1025
1026 let stats = builder.stats();
1027 assert_eq!(stats.glob_entries, 1);
1028 assert_eq!(stats.literal_entries, 0);
1029 }
1030
1031 #[test]
1032 fn test_empty_prefix_value() {
1033 let result = DatabaseBuilder::detect_entry_type("literal:").unwrap();
1035 match result {
1036 EntryType::Literal(p) => assert_eq!(p, ""),
1037 _ => panic!("Expected literal"),
1038 }
1039 }
1040}