1use std::collections::HashMap;
55
56use crate::error::{Aria2Error, Result};
57use crate::request::request_group::DownloadOptions;
58
59pub fn escape_uri(s: &str) -> String {
65 s.replace('\\', "\\\\")
66 .replace('\t', "\\t")
67 .replace('\n', "\\n")
68}
69
70pub fn unescape_uri(s: &str) -> String {
72 let mut result = String::with_capacity(s.len());
73 let mut chars = s.chars().peekable();
74
75 while let Some(c) = chars.next() {
76 if c == '\\' {
77 if let Some(&next) = chars.peek() {
78 match next {
79 't' => {
80 result.push('\t');
81 chars.next();
82 }
83 'n' => {
84 result.push('\n');
85 chars.next();
86 }
87 '\\' => {
88 result.push('\\');
89 chars.next();
90 }
91 _ => {
92 result.push(c);
93 }
94 }
95 } else {
96 result.push(c);
97 }
98 } else {
99 result.push(c);
100 }
101 }
102
103 result
104}
105
106pub fn decode_hex(hex: &str) -> Result<Vec<u8>> {
108 if !hex.len().is_multiple_of(2) {
109 return Err(Aria2Error::Io(format!(
110 "Hex string has odd length: {}",
111 hex.len()
112 )));
113 }
114
115 let mut bytes = Vec::with_capacity(hex.len() / 2);
116
117 for i in (0..hex.len()).step_by(2) {
118 let byte_str = &hex[i..i + 2];
119 let byte = u8::from_str_radix(byte_str, 16).map_err(|e| {
120 Aria2Error::Io(format!("Invalid hex character at position {}: {}", i, e))
121 })?;
122 bytes.push(byte);
123 }
124
125 Ok(bytes)
126}
127
128pub fn download_options_to_map(opts: &DownloadOptions) -> HashMap<String, String> {
138 let mut map = HashMap::new();
139
140 if let Some(v) = opts.split {
142 map.insert("split".to_string(), v.to_string());
143 }
144 if let Some(v) = opts.max_connection_per_server {
145 map.insert("max-connection-per-server".to_string(), v.to_string());
146 }
147 if let Some(v) = opts.max_download_limit {
148 map.insert("max-download-limit".to_string(), v.to_string());
149 }
150 if let Some(v) = opts.max_upload_limit {
151 map.insert("max-upload-limit".to_string(), v.to_string());
152 }
153 if let Some(ref v) = opts.dir {
154 map.insert("dir".to_string(), v.clone());
155 }
156 if let Some(ref v) = opts.out {
157 map.insert("out".to_string(), v.clone());
158 }
159 if let Some(v) = opts.seed_time {
160 map.insert("seed-time".to_string(), v.to_string());
161 }
162 if let Some(v) = opts.seed_ratio {
163 map.insert("seed-ratio".to_string(), v.to_string());
164 }
165
166 if let Some(ref v) = opts.file_allocation {
168 map.insert("file-allocation".to_string(), v.clone());
169 }
170 if let Some(v) = opts.mmap_threshold {
171 map.insert("mmap-threshold".to_string(), v.to_string());
172 }
173 if opts.secure_falloc {
174 map.insert("secure-falloc".to_string(), "true".to_string());
175 }
176
177 if let Some((ref algo, ref val)) = opts.checksum {
179 map.insert("checksum".to_string(), format!("{}={}", algo, val));
180 }
181
182 if let Some(ref v) = opts.cookie_file {
184 map.insert("cookie-file".to_string(), v.clone());
185 }
186 if let Some(ref v) = opts.cookies {
187 map.insert("cookies".to_string(), v.clone());
188 }
189
190 if opts.bt_force_encrypt {
192 map.insert("bt-force-encrypt".to_string(), "true".to_string());
193 }
194 if opts.bt_require_crypto {
195 map.insert("bt-require-crypto".to_string(), "true".to_string());
196 }
197 if !opts.enable_dht {
199 map.insert("enable-dht".to_string(), "false".to_string());
200 }
201 if let Some(v) = opts.dht_listen_port {
202 map.insert("dht-listen-port".to_string(), v.to_string());
203 }
204 if let Some(ref v) = opts.dht_entry_point {
205 map.insert("dht-entry-point".to_string(), v.join(","));
206 }
207 if !opts.enable_public_trackers {
209 map.insert("enable-public-trackers".to_string(), "false".to_string());
210 }
211 if !opts.bt_piece_selection_strategy.is_empty() {
212 map.insert(
213 "bt-piece-selection-strategy".to_string(),
214 opts.bt_piece_selection_strategy.clone(),
215 );
216 }
217 if opts.bt_endgame_threshold > 0 {
218 map.insert(
219 "bt-endgame-threshold".to_string(),
220 opts.bt_endgame_threshold.to_string(),
221 );
222 }
223 if let Some(v) = opts.bt_max_upload_slots {
224 map.insert("bt-max-upload-slots".to_string(), v.to_string());
225 }
226 if let Some(v) = opts.bt_optimistic_unchoke_interval {
227 map.insert("bt-optimistic-unchoke-interval".to_string(), v.to_string());
228 }
229 if let Some(v) = opts.bt_snubbed_timeout {
230 map.insert("bt-snubbed-timeout".to_string(), v.to_string());
231 }
232 if !opts.bt_prioritize_piece.is_empty() {
233 map.insert(
234 "bt-prioritize-piece".to_string(),
235 opts.bt_prioritize_piece.clone(),
236 );
237 }
238 if opts.enable_utp {
239 map.insert("enable-utp".to_string(), "true".to_string());
240 }
241 if let Some(v) = opts.utp_listen_port {
242 map.insert("utp-listen-port".to_string(), v.to_string());
243 }
244
245 if opts.max_retries > 0 {
247 map.insert("max-retries".to_string(), opts.max_retries.to_string());
248 }
249 if opts.retry_wait > 0 {
250 map.insert("retry-wait".to_string(), opts.retry_wait.to_string());
251 }
252
253 if let Some(ref v) = opts.dht_file_path {
255 map.insert("dht-file-path".to_string(), v.clone());
256 }
257
258 if let Some(ref v) = opts.http_proxy {
260 map.insert("http-proxy".to_string(), v.clone());
261 }
262 if let Some(ref v) = opts.all_proxy {
263 map.insert("all-proxy".to_string(), v.clone());
264 }
265 if let Some(ref v) = opts.https_proxy {
266 map.insert("https-proxy".to_string(), v.clone());
267 }
268 if let Some(ref v) = opts.ftp_proxy {
269 map.insert("ftp-proxy".to_string(), v.clone());
270 }
271 if let Some(ref v) = opts.no_proxy {
272 map.insert("no-proxy".to_string(), v.clone());
273 }
274
275 if !opts.header.is_empty() {
277 map.insert("header".to_string(), opts.header.join(","));
278 }
279 if let Some(ref v) = opts.user_agent {
280 map.insert("user-agent".to_string(), v.clone());
281 }
282 if let Some(ref v) = opts.referer {
283 map.insert("referer".to_string(), v.clone());
284 }
285
286 map
287}
288
289#[derive(Debug, Clone)]
312pub struct SessionEntry {
313 pub gid: u64,
315
316 pub uris: Vec<String>,
318
319 pub options: HashMap<String, String>,
321
322 pub paused: bool,
324
325 pub total_length: u64,
328
329 pub completed_length: u64,
331
332 pub upload_length: u64,
334
335 pub download_speed: u64,
337
338 pub status: String,
340
341 pub error_code: Option<i32>,
343
344 pub bitfield: Option<Vec<u8>>,
349
350 pub num_pieces: Option<u32>,
353
354 pub piece_length: Option<u32>,
357
358 pub info_hash_hex: Option<String>,
361
362 pub resume_offset: Option<u64>,
366}
367
368impl SessionEntry {
369 pub fn new(gid: u64, uris: Vec<String>) -> Self {
396 SessionEntry {
397 gid,
398 uris,
399 options: HashMap::new(),
400 paused: false,
401
402 total_length: 0,
404 completed_length: 0,
405 upload_length: 0,
406 download_speed: 0,
407 status: "active".to_string(),
408 error_code: None,
409
410 bitfield: None,
412 num_pieces: None,
413 piece_length: None,
414 info_hash_hex: None,
415
416 resume_offset: None,
418 }
419 }
420
421 pub fn with_options(mut self, options: HashMap<String, String>) -> Self {
446 self.options = options;
447 self
448 }
449
450 pub fn paused(mut self) -> Self {
466 self.paused = true;
467 self
468 }
469
470 #[allow(dead_code)] fn get_option(&self, key: &str) -> Option<&str> {
481 self.options.get(key).map(|s| s.as_str())
482 }
483
484 }
488
489#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn test_serialize_single_entry() {
497 let entry = SessionEntry::new(0xd270c8a2, vec!["http://example.com/file.zip".to_string()]);
498 let text = entry.serialize();
499 assert!(
500 text.contains("http://example.com/file.zip"),
501 "Should contain URI"
502 );
503 assert!(text.contains("GID=d270c8a2"), "Should contain GID");
504 }
505
506 #[test]
507 fn test_serialize_multiple_entries_roundtrip() {
508 let entries = vec![
509 SessionEntry::new(1, vec!["http://a.com/1.bin".to_string()]).with_options({
510 let mut m = HashMap::new();
511 m.insert("split".to_string(), "4".to_string());
512 m.insert("dir".to_string(), "/tmp".to_string());
513 m
514 }),
515 SessionEntry::new(
516 2,
517 vec![
518 "ftp://b.com/2.iso".to_string(),
519 "http://mirror.b.com/2.iso".to_string(),
520 ],
521 )
522 .paused(),
523 ];
524
525 let mut serialized = String::new();
526 for e in &entries {
527 serialized.push_str(&e.serialize());
528 serialized.push('\n');
529 }
530
531 let parts: Vec<&str> = serialized.split("\n\n").collect();
533 assert!(parts.len() >= 2, "Should have at least 2 entries");
534
535 let entry1 = SessionEntry::deserialize_line(parts[0]).unwrap();
536 assert_eq!(entry1.uris.len(), 1);
537 assert_eq!(entry1.uris[0], "http://a.com/1.bin");
538 assert_eq!(entry1.options.get("split").unwrap(), "4");
539
540 let entry2 = SessionEntry::deserialize_line(parts[1]).unwrap();
541 assert_eq!(entry2.uris.len(), 2);
542 assert!(entry2.paused);
543 }
544
545 #[test]
546 fn test_deserialize_empty_file() {
547 let entry = SessionEntry::deserialize_line("").unwrap();
548 assert!(entry.uris.is_empty());
549
550 let entry = SessionEntry::deserialize_line("\n\n\n").unwrap();
551 assert!(entry.uris.is_empty());
552 }
553
554 #[test]
555 fn test_deserialize_skip_comments_and_blanks() {
556 let input = r#"# This is a comment
557# Another comment
558
559http://example.com/file
560 GID=abc123
561 dir=/downloads
562"#;
563 let entry = SessionEntry::deserialize_line(input).unwrap();
564 assert_eq!(entry.uris.len(), 1);
566 assert_eq!(entry.uris[0], "http://example.com/file");
567 }
568
569 #[test]
570 fn test_deserialize_options_parsing() {
571 let input = r#"http://example.com/file.zip
572 GID=1
573 split=4
574 max-connection-per-server=2
575 dir=C:\Users\test\Downloads
576 out=file.zip
577"#;
578 let entry = SessionEntry::deserialize_line(input).unwrap();
579 assert_eq!(entry.options.get("split").unwrap(), "4");
580 assert_eq!(entry.options.get("max-connection-per-server").unwrap(), "2");
581 assert_eq!(
582 entry.options.get("dir").unwrap(),
583 "C:\\Users\\test\\Downloads"
584 );
585 assert_eq!(entry.options.get("out").unwrap(), "file.zip");
586 }
587
588 #[test]
589 fn test_pause_flag_serialization() {
590 let input = r#"http://example.com/pause.me
591 GID=42
592 PAUSE=true
593"#;
594 let entry = SessionEntry::deserialize_line(input).unwrap();
595 assert!(entry.paused);
596
597 let text = entry.serialize();
598 assert!(text.contains("PAUSE=true"));
599 }
600
601 #[test]
602 fn test_serialize_tab_separated_uris() {
603 let entry = SessionEntry::new(
604 99,
605 vec![
606 "http://mirror1.com/f".to_string(),
607 "http://mirror2.com/f".to_string(),
608 "http://mirror3.com/f".to_string(),
609 ],
610 );
611 let text = entry.serialize();
612 let uri_line = text.lines().next().unwrap();
613 assert_eq!(
614 uri_line.matches('\t').count(),
615 2,
616 "3 URIs should have 2 tab separators"
617 );
618 }
619
620 #[test]
623 fn test_serialize_new_fields() {
624 let mut entry = SessionEntry::new(1, vec!["http://example.com/file.bin".to_string()]);
625 entry.total_length = 1024 * 1024; entry.completed_length = 512 * 1024; entry.upload_length = 1024;
628 entry.download_speed = 2048;
629 entry.status = "active".to_string();
630 entry.error_code = None;
631
632 let text = entry.serialize();
633
634 assert!(
636 text.contains("TOTAL_LENGTH=1048576"),
637 "Should contain TOTAL_LENGTH"
638 );
639 assert!(
640 text.contains("COMPLETED_LENGTH=524288"),
641 "Should contain COMPLETED_LENGTH"
642 );
643 assert!(
644 text.contains("UPLOAD_LENGTH=1024"),
645 "Should contain UPLOAD_LENGTH"
646 );
647 assert!(
648 text.contains("DOWNLOAD_SPEED=2048"),
649 "Should contain DOWNLOAD_SPEED"
650 );
651 assert!(text.contains("STATUS=active"), "Should contain STATUS");
652 }
653
654 #[test]
655 fn test_deserialize_with_all_fields() {
656 let input = r#"http://example.com/bigfile.zip
657 GID=1
658 TOTAL_LENGTH=10485760
659 COMPLETED_LENGTH=5242880
660 UPLOAD_LENGTH=2048
661 DOWNLOAD_SPEED=4096
662 STATUS=active
663 ERROR_CODE=
664 BITFIELD=
665 NUM_PIECES=0
666 PIECE_LENGTH=0
667 INFO_HASH=
668 RESUME_OFFSET=5242880
669"#;
670
671 let entry = SessionEntry::deserialize_line(input).unwrap();
672
673 assert_eq!(entry.total_length, 10485760);
674 assert_eq!(entry.completed_length, 5242880);
675 assert_eq!(entry.upload_length, 2048);
676 assert_eq!(entry.download_speed, 4096);
677 assert_eq!(entry.status, "active");
678 assert_eq!(entry.error_code, None);
679 assert_eq!(entry.resume_offset, Some(5242880));
680 }
681
682 #[test]
683 fn test_deserialize_user_options() {
684 let input = r#"http://example.com/file.zip
686 GID=1
687 split=4
688 dir=/downloads
689 TOTAL_LENGTH=1000
690"#;
691
692 let entry = SessionEntry::deserialize_line(input).unwrap();
693
694 assert_eq!(entry.total_length, 1000);
696
697 assert_eq!(entry.options.get("split").unwrap(), "4");
699 assert_eq!(entry.options.get("dir").unwrap(), "/downloads");
700 }
701
702 #[test]
703 fn test_bitfield_roundtrip() {
704 let mut entry =
705 SessionEntry::new(1, vec!["http://example.com/torrent.torrent".to_string()]);
706
707 entry.bitfield = Some(vec![0xFF, 0xF0, 0x0F]);
709 entry.num_pieces = Some(24); entry.piece_length = Some(262144); let text = entry.serialize();
713
714 assert!(
716 text.contains("BITFIELD=fff00f"),
717 "bitfield should be encoded as hex string"
718 );
719
720 let restored = SessionEntry::deserialize_line(&text).unwrap();
722 assert_eq!(
723 restored.bitfield,
724 Some(vec![0xFF, 0xF0, 0x0F]),
725 "bitfield should be restored correctly"
726 );
727 assert_eq!(restored.num_pieces, Some(24));
728 assert_eq!(restored.piece_length, Some(262144));
729 }
730
731 #[test]
732 fn test_empty_bitfield_serialized_as_empty() {
733 let entry = SessionEntry::new(1, vec!["http://example.com/file.zip".to_string()]);
734 let text = entry.serialize();
737
738 assert!(
740 text.contains("BITFIELD=\n"),
741 "None bitfield should be serialized as empty value"
742 );
743
744 let restored = SessionEntry::deserialize_line(&text).unwrap();
746 assert_eq!(
747 restored.bitfield, None,
748 "Empty bitfield should restore to None"
749 );
750 }
751
752 #[test]
753 fn test_default_session_entry_has_zero_progress() {
754 let entry = SessionEntry::new(99, vec!["http://test.com/f".to_string()]);
755
756 assert_eq!(entry.total_length, 0);
758 assert_eq!(entry.completed_length, 0);
759 assert_eq!(entry.upload_length, 0);
760 assert_eq!(entry.download_speed, 0);
761 assert_eq!(entry.status, "active", "Default status should be 'active'");
762 assert_eq!(entry.error_code, None);
763 assert_eq!(entry.bitfield, None);
764 assert_eq!(entry.num_pieces, None);
765 assert_eq!(entry.piece_length, None);
766 assert_eq!(entry.info_hash_hex, None);
767 assert_eq!(entry.resume_offset, None);
768 }
769
770 #[test]
771 fn test_status_field_values() {
772 let statuses = ["active", "waiting", "paused", "error"];
773
774 for status in statuses {
775 let mut entry = SessionEntry::new(1, vec!["http://example.com/f".to_string()]);
776 entry.status = status.to_string();
777
778 let text = entry.serialize();
779 assert!(
780 text.contains(&format!("STATUS={}", status)),
781 "Status '{}' should be serialized correctly",
782 status
783 );
784
785 let restored = SessionEntry::deserialize_line(&text).unwrap();
787 assert_eq!(
788 restored.status, status,
789 "Status '{}' should be deserialized correctly",
790 status
791 );
792 }
793 }
794
795 #[test]
796 fn test_resume_offset_for_http_ftp() {
797 let mut entry = SessionEntry::new(1, vec!["http://example.com/large-file.iso".to_string()]);
798
799 entry.total_length = 1073741824; entry.completed_length = 536870912; entry.resume_offset = Some(536870912); entry.status = "paused".to_string();
804
805 let text = entry.serialize();
806
807 assert!(
809 text.contains("RESUME_OFFSET=536870912"),
810 "resume offset should be serialized correctly"
811 );
812
813 let restored = SessionEntry::deserialize_line(&text).unwrap();
815 assert_eq!(
816 restored.resume_offset,
817 Some(536870912),
818 "resume offset should be restored correctly"
819 );
820 assert_eq!(restored.status, "paused");
821 }
822
823 #[test]
824 fn test_bt_specific_fields_only_when_present() {
825 let mut entry = SessionEntry::new(1, vec!["magnet:?xt=urn:btih:abc123".to_string()]);
827
828 let text_without_bt = entry.serialize();
830 let restored_without_bt = SessionEntry::deserialize_line(&text_without_bt).unwrap();
831
832 assert_eq!(restored_without_bt.bitfield, None);
833 assert_eq!(restored_without_bt.num_pieces, None);
834 assert_eq!(restored_without_bt.piece_length, None);
835 assert_eq!(restored_without_bt.info_hash_hex, None);
836
837 entry.bitfield = Some(vec![0xAA, 0xBB]);
839 entry.num_pieces = Some(16);
840 entry.piece_length = Some(524288);
841 entry.info_hash_hex = Some("abc123def456".to_string());
842
843 let text_with_bt = entry.serialize();
844 let restored_with_bt = SessionEntry::deserialize_line(&text_with_bt).unwrap();
845
846 assert_eq!(restored_with_bt.bitfield, Some(vec![0xAA, 0xBB]));
847 assert_eq!(restored_with_bt.num_pieces, Some(16));
848 assert_eq!(restored_with_bt.piece_length, Some(524288));
849 assert_eq!(
850 restored_with_bt.info_hash_hex,
851 Some("abc123def456".to_string())
852 );
853 }
854
855 #[test]
856 fn test_download_options_to_map_all_fields() {
857 let opts = DownloadOptions {
859 split: Some(8),
860 max_connection_per_server: Some(4),
861 max_download_limit: Some(102400),
862 max_upload_limit: Some(51200),
863 dir: Some("/downloads".to_string()),
864 out: Some("file.bin".to_string()),
865 seed_time: Some(3600),
866 seed_ratio: Some(2.0),
867 file_allocation: Some("trunc".to_string()),
869 mmap_threshold: Some(128 * 1024 * 1024),
870 secure_falloc: true,
871 checksum: Some(("sha256".to_string(), "abc123".to_string())),
873 cookie_file: Some("/tmp/cookies.txt".to_string()),
875 cookies: Some("key=value".to_string()),
876 bt_force_encrypt: true,
878 bt_require_crypto: true,
879 enable_dht: false,
880 dht_listen_port: Some(6881),
881 dht_entry_point: Some(vec!["router.bittorrent.com:6881".to_string()]),
882 enable_public_trackers: false,
883 bt_piece_selection_strategy: "sequential".to_string(),
884 bt_endgame_threshold: 10,
885 bt_max_upload_slots: Some(4),
886 bt_optimistic_unchoke_interval: Some(30),
887 bt_snubbed_timeout: Some(60),
888 bt_prioritize_piece: "head".to_string(),
889 enable_utp: true,
890 utp_listen_port: Some(6882),
891 max_retries: 5,
893 retry_wait: 3,
894 dht_file_path: Some("/tmp/dht.dat".to_string()),
896 http_proxy: Some("http://proxy:8080".to_string()),
898 all_proxy: Some("socks5://proxy:1080".to_string()),
899 https_proxy: Some("http://proxy:8443".to_string()),
900 ftp_proxy: Some("http://proxy:8021".to_string()),
901 no_proxy: Some("localhost,127.0.0.1".to_string()),
902 header: vec!["X-Custom: foo".to_string(), "X-Other: bar".to_string()],
904 user_agent: Some("aria2-rust/1.0".to_string()),
905 referer: Some("http://example.com".to_string()),
906 };
907
908 let map = download_options_to_map(&opts);
909
910 assert_eq!(map.get("file-allocation").unwrap(), "trunc");
912 assert_eq!(map.get("mmap-threshold").unwrap(), "134217728");
913 assert_eq!(map.get("secure-falloc").unwrap(), "true");
914
915 assert_eq!(map.get("checksum").unwrap(), "sha256=abc123");
917
918 assert_eq!(map.get("cookie-file").unwrap(), "/tmp/cookies.txt");
920 assert_eq!(map.get("cookies").unwrap(), "key=value");
921
922 assert_eq!(map.get("bt-force-encrypt").unwrap(), "true");
924 assert_eq!(map.get("bt-require-crypto").unwrap(), "true");
925 assert_eq!(map.get("enable-dht").unwrap(), "false");
926 assert_eq!(map.get("dht-listen-port").unwrap(), "6881");
927 assert_eq!(
928 map.get("dht-entry-point").unwrap(),
929 "router.bittorrent.com:6881"
930 );
931 assert_eq!(map.get("enable-public-trackers").unwrap(), "false");
932 assert_eq!(
933 map.get("bt-piece-selection-strategy").unwrap(),
934 "sequential"
935 );
936 assert_eq!(map.get("bt-endgame-threshold").unwrap(), "10");
937 assert_eq!(map.get("bt-max-upload-slots").unwrap(), "4");
938 assert_eq!(map.get("bt-optimistic-unchoke-interval").unwrap(), "30");
939 assert_eq!(map.get("bt-snubbed-timeout").unwrap(), "60");
940 assert_eq!(map.get("bt-prioritize-piece").unwrap(), "head");
941 assert_eq!(map.get("enable-utp").unwrap(), "true");
942 assert_eq!(map.get("utp-listen-port").unwrap(), "6882");
943
944 assert_eq!(map.get("max-retries").unwrap(), "5");
946 assert_eq!(map.get("retry-wait").unwrap(), "3");
947
948 assert_eq!(map.get("dht-file-path").unwrap(), "/tmp/dht.dat");
950
951 assert_eq!(map.get("http-proxy").unwrap(), "http://proxy:8080");
953 assert_eq!(map.get("all-proxy").unwrap(), "socks5://proxy:1080");
954 assert_eq!(map.get("https-proxy").unwrap(), "http://proxy:8443");
955 assert_eq!(map.get("ftp-proxy").unwrap(), "http://proxy:8021");
956 assert_eq!(map.get("no-proxy").unwrap(), "localhost,127.0.0.1");
957
958 assert_eq!(map.get("header").unwrap(), "X-Custom: foo,X-Other: bar");
960 assert_eq!(map.get("user-agent").unwrap(), "aria2-rust/1.0");
961 assert_eq!(map.get("referer").unwrap(), "http://example.com");
962 }
963
964 #[test]
965 fn test_download_options_to_map_defaults_excluded() {
966 let opts = DownloadOptions::default();
971 let map = download_options_to_map(&opts);
972
973 assert!(!map.contains_key("secure-falloc"));
975 assert!(!map.contains_key("file-allocation"));
977 assert!(!map.contains_key("enable-dht"));
979 assert!(!map.contains_key("enable-public-trackers"));
980 }
981}