1use std::fmt::{Debug, Formatter};
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use crate::{MDB_CLIENT_ERROR_CONTEXT, get_config_path};
9use malwaredb_api::{
10 GetAPIKeyResponse, GetUserInfoResponse, Labels, PartialHashSearchType, Report, SearchRequest,
11 SearchRequestParameters, SearchResponse, SearchType, ServerInfo, ServerResponse,
12 SimilarSamplesResponse, Sources, SupportedFileTypes, YaraSearchRequest,
13 YaraSearchRequestResponse, YaraSearchResponse, digest::HashType,
14};
15use malwaredb_types::exec::pe32::EXE;
16
17use anyhow::{Context, Result, bail, ensure};
18use base64::Engine;
19use base64::engine::general_purpose;
20use fuzzyhash::FuzzyHash;
21use malwaredb_lzjd2::lzjd::LzDigest;
22use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24use tlsh_fixed::TlshBuilder;
25use tracing::{error, info, trace, warn};
26use uuid::Uuid;
27use zeroize::{Zeroize, ZeroizeOnDrop};
28
29#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
31#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
32pub struct MdbClient {
33 pub url: String,
35
36 api_key: String,
38
39 #[zeroize(skip)]
41 #[serde(skip)]
42 client: reqwest::blocking::Client,
43
44 #[cfg(target_os = "macos")]
46 #[zeroize(skip)]
47 #[serde(skip)]
48 cert: Option<crate::macos::CertificateData>,
49}
50
51impl MdbClient {
52 pub fn new(url: String, api_key: String, cert_path: Option<PathBuf>) -> Result<Self> {
63 let mut url = url;
64 let url = if url.ends_with('/') {
65 url.pop();
66 url
67 } else {
68 url
69 };
70
71 let cert = if let Some(path) = cert_path {
72 Some((crate::path_load_cert(&path)?, path))
73 } else {
74 None
75 };
76
77 let builder = reqwest::blocking::ClientBuilder::new()
78 .gzip(true)
79 .zstd(true)
80 .use_rustls_tls()
81 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
82
83 let client = if let Some(((_cert_type, cert), _path)) = &cert {
84 builder.add_root_certificate(cert.clone()).build()
85 } else {
86 builder.build()
87 }?;
88
89 #[cfg(target_os = "macos")]
90 let cert = if let Some(((cert_type, _cert), cert_path)) = &cert {
91 Some(crate::macos::CertificateData {
92 cert_type: *cert_type,
93 cert_bytes: std::fs::read(cert_path)?,
94 })
95 } else {
96 None
97 };
98
99 Ok(Self {
100 url,
101 api_key,
102 client,
103
104 #[cfg(target_os = "macos")]
105 cert,
106 })
107 }
108
109 pub fn anonymous(url: String, save: bool, cert_path: Option<PathBuf>) -> Result<Self> {
115 let mut url = url;
116 let url = if url.ends_with('/') {
117 url.pop();
118 url
119 } else {
120 url
121 };
122
123 let builder = reqwest::blocking::ClientBuilder::new()
124 .gzip(true)
125 .zstd(true)
126 .use_rustls_tls()
127 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
128
129 let cert = if let Some(path) = cert_path {
130 Some((crate::path_load_cert(&path)?, path))
131 } else {
132 None
133 };
134
135 let client = if let Some(((_cert_type, cert), _cert_path)) = &cert {
136 builder.add_root_certificate(cert.clone()).build()
137 } else {
138 builder.build()
139 }?;
140
141 let info = client
142 .get(format!("{url}{}", malwaredb_api::USER_INFO_URL))
143 .send()?
144 .json::<ServerResponse<GetUserInfoResponse>>()
145 .context(MDB_CLIENT_ERROR_CONTEXT)?;
146
147 if let ServerResponse::Success(info) = info {
148 ensure!(info.id > 0);
149 } else {
150 bail!("Anonymous access is not allowed");
151 }
152
153 #[cfg(target_os = "macos")]
154 let cert = if let Some(((cert_type, _cert), cert_path)) = &cert {
155 Some(crate::macos::CertificateData {
156 cert_type: *cert_type,
157 cert_bytes: std::fs::read(cert_path)?,
158 })
159 } else {
160 None
161 };
162
163 let client = Self {
164 url,
165 api_key: "anonymous".to_string(),
166 client,
167
168 #[cfg(target_os = "macos")]
169 cert,
170 };
171
172 let server_info = client.server_info()?;
173 if server_info.mdb_version > *crate::MDB_VERSION_SEMVER {
174 warn!(
175 "Server version {:?} is newer than client {:?}, consider updating.",
176 server_info.mdb_version,
177 crate::MDB_VERSION_SEMVER
178 );
179 }
180
181 if save && let Err(e) = client.save() {
182 error!("Anonymous connection successful but failed to save config: {e}");
183 bail!("Anonymous connection successful but failed to save config: {e}");
184 }
185
186 Ok(client)
187 }
188
189 pub fn login(
200 url: String,
201 username: String,
202 password: String,
203 save: bool,
204 cert_path: Option<PathBuf>,
205 ) -> Result<Self> {
206 let mut url = url;
207 let url = if url.ends_with('/') {
208 url.pop();
209 url
210 } else {
211 url
212 };
213
214 let api_request = malwaredb_api::GetAPIKeyRequest {
215 user: username,
216 password,
217 };
218
219 let builder = reqwest::blocking::ClientBuilder::new()
220 .gzip(true)
221 .zstd(true)
222 .use_rustls_tls()
223 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
224
225 let cert = if let Some(path) = cert_path {
226 Some((crate::path_load_cert(&path)?, path))
227 } else {
228 None
229 };
230
231 let client = if let Some(((_cert_type, cert), _path)) = &cert {
232 builder.add_root_certificate(cert.clone()).build()
233 } else {
234 builder.build()
235 }?;
236
237 let res = client
238 .post(format!("{url}{}", malwaredb_api::USER_LOGIN_URL))
239 .json(&api_request)
240 .send()?
241 .json::<ServerResponse<GetAPIKeyResponse>>()
242 .context(MDB_CLIENT_ERROR_CONTEXT)?;
243
244 let res = match res {
245 ServerResponse::Success(res) => res,
246 ServerResponse::Error(err) => return Err(err.into()),
247 };
248
249 #[cfg(target_os = "macos")]
250 let cert = if let Some(((cert_type, _cert), cert_path)) = &cert {
251 Some(crate::macos::CertificateData {
252 cert_type: *cert_type,
253 cert_bytes: std::fs::read(cert_path)?,
254 })
255 } else {
256 None
257 };
258
259 let client = MdbClient {
260 url,
261 api_key: res.key.clone(),
262 client,
263
264 #[cfg(target_os = "macos")]
265 cert,
266 };
267
268 let server_info = client.server_info()?;
269 if server_info.mdb_version > *crate::MDB_VERSION_SEMVER {
270 warn!(
271 "Server version {:?} is newer than client {:?}, consider updating.",
272 server_info.mdb_version,
273 crate::MDB_VERSION_SEMVER
274 );
275 }
276
277 if save && let Err(e) = client.save() {
278 error!("Login successful but failed to save config: {e}");
279 bail!("Login successful but failed to save config: {e}");
280 }
281 Ok(client)
282 }
283
284 pub fn reset_key(&self) -> Result<()> {
290 let response = self
291 .client
292 .get(format!("{}{}", self.url, malwaredb_api::USER_LOGOUT_URL))
293 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
294 .send()
295 .context(MDB_CLIENT_ERROR_CONTEXT)?;
296 if !response.status().is_success() {
297 bail!("failed to reset API key, was it correct?");
298 }
299 Ok(())
300 }
301
302 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
309 let name = path.as_ref().display();
310 let config =
311 std::fs::read_to_string(&path).context(format!("failed to read config file {name}"))?;
312 let cfg: MdbClient =
313 toml::from_str(&config).context(format!("failed to parse config file {name}"))?;
314 Ok(cfg)
315 }
316
317 pub fn load() -> Result<Self> {
330 #[cfg(target_os = "macos")]
331 {
332 if let Ok((api_key, url, cert)) = crate::macos::retrieve_credentials() {
333 let builder = reqwest::blocking::ClientBuilder::new()
334 .gzip(true)
335 .zstd(true)
336 .use_rustls_tls()
337 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
338
339 let client = if let Some(cert) = &cert {
340 builder.add_root_certificate(cert.as_cert()?).build()
341 } else {
342 builder.build()
343 }?;
344
345 return Ok(Self {
346 url,
347 api_key,
348 client,
349 cert,
350 });
351 }
352 }
353
354 let path = get_config_path(false)?;
355 if path.exists() {
356 return Self::from_file(path);
357 }
358 bail!("config file not found")
359 }
360
361 pub fn save(&self) -> Result<()> {
369 #[cfg(target_os = "macos")]
370 {
371 if crate::macos::save_credentials(&self.url, &self.api_key, self.cert.clone()).is_ok() {
372 return Ok(());
373 }
374 }
375
376 let toml = toml::to_string(self)?;
377 let path = get_config_path(true)?;
378
379 let mut options = OpenOptions::new();
380 options
381 .write(true)
382 .create(true)
383 .append(false)
384 .truncate(false);
385
386 #[cfg(target_family = "unix")]
387 {
388 use std::os::unix::fs::OpenOptionsExt;
389
390 options.mode(0o600);
391 }
392
393 let mut file = options.open(&path)?;
394 write!(file, "{toml}").context(format!("failed to write mdb config to {}", path.display()))
395 }
396
397 pub fn delete(&self) -> Result<()> {
404 #[cfg(target_os = "macos")]
405 crate::macos::clear_credentials();
406
407 let path = get_config_path(false)?;
408 if path.exists() {
409 std::fs::remove_file(&path)
410 .context(format!("failed to delete client config file {}", path.display()))?;
411 }
412 Ok(())
413 }
414
415 pub fn server_info(&self) -> Result<ServerInfo> {
423 let response = self
424 .client
425 .get(format!("{}{}", self.url, malwaredb_api::SERVER_INFO_URL))
426 .send()?
427 .json::<ServerResponse<ServerInfo>>()
428 .context(MDB_CLIENT_ERROR_CONTEXT)?;
429
430 match response {
431 ServerResponse::Success(info) => Ok(info),
432 ServerResponse::Error(e) => Err(e.into()),
433 }
434 }
435
436 pub fn supported_types(&self) -> Result<SupportedFileTypes> {
442 let response = self
443 .client
444 .get(format!("{}{}", self.url, malwaredb_api::SUPPORTED_FILE_TYPES_URL))
445 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
446 .send()?
447 .json::<ServerResponse<SupportedFileTypes>>()
448 .context(MDB_CLIENT_ERROR_CONTEXT)?;
449
450 match response {
451 ServerResponse::Success(types) => Ok(types),
452 ServerResponse::Error(e) => Err(e.into()),
453 }
454 }
455
456 pub fn whoami(&self) -> Result<GetUserInfoResponse> {
463 let response = self
464 .client
465 .get(format!("{}{}", self.url, malwaredb_api::USER_INFO_URL))
466 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
467 .send()?
468 .json::<ServerResponse<GetUserInfoResponse>>()
469 .context(MDB_CLIENT_ERROR_CONTEXT)?;
470
471 match response {
472 ServerResponse::Success(info) => Ok(info),
473 ServerResponse::Error(e) => Err(e.into()),
474 }
475 }
476
477 pub fn labels(&self) -> Result<Labels> {
484 let response = self
485 .client
486 .get(format!("{}{}", self.url, malwaredb_api::LIST_LABELS_URL))
487 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
488 .send()?
489 .json::<ServerResponse<Labels>>()
490 .context(MDB_CLIENT_ERROR_CONTEXT)?;
491
492 match response {
493 ServerResponse::Success(labels) => Ok(labels),
494 ServerResponse::Error(e) => Err(e.into()),
495 }
496 }
497
498 pub fn sources(&self) -> Result<Sources> {
505 let response = self
506 .client
507 .get(format!("{}{}", self.url, malwaredb_api::LIST_SOURCES_URL))
508 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
509 .send()?
510 .json::<ServerResponse<Sources>>()
511 .context(MDB_CLIENT_ERROR_CONTEXT)?;
512
513 match response {
514 ServerResponse::Success(sources) => Ok(sources),
515 ServerResponse::Error(e) => Err(e.into()),
516 }
517 }
518
519 pub fn submit(
526 &self,
527 contents: impl AsRef<[u8]>,
528 file_name: String,
529 source_id: u32,
530 ) -> Result<bool> {
531 let mut hasher = Sha256::new();
532 hasher.update(&contents);
533 let result = hasher.finalize();
534
535 let encoded = general_purpose::STANDARD.encode(contents);
536
537 let payload = malwaredb_api::NewSampleB64 {
538 file_name,
539 source_id,
540 file_contents_b64: encoded,
541 sha256: hex::encode(result),
542 };
543
544 match self
545 .client
546 .post(format!("{}{}", self.url, malwaredb_api::UPLOAD_SAMPLE_JSON_URL))
547 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
548 .json(&payload)
549 .send()
550 {
551 Ok(res) => {
552 if !res.status().is_success() {
553 info!("Code {} sending {}", res.status(), payload.file_name);
554 }
555 Ok(res.status().is_success())
556 }
557 Err(e) => {
558 let status: String = e
559 .status()
560 .map(|s| s.as_str().to_string())
561 .unwrap_or_default();
562 error!("Error{status} sending {}: {e}", payload.file_name);
563 bail!(e.to_string())
564 }
565 }
566 }
567
568 pub fn submit_as_cbor(
576 &self,
577 contents: impl AsRef<[u8]>,
578 file_name: String,
579 source_id: u32,
580 ) -> Result<bool> {
581 let mut hasher = Sha256::new();
582 hasher.update(&contents);
583 let result = hasher.finalize();
584
585 let payload = malwaredb_api::NewSampleBytes {
586 file_name,
587 source_id,
588 file_contents: contents.as_ref().to_vec(),
589 sha256: hex::encode(result),
590 };
591
592 let mut bytes = Vec::with_capacity(payload.file_contents.len());
593 ciborium::ser::into_writer(&payload, &mut bytes)?;
594
595 match self
596 .client
597 .post(format!("{}{}", self.url, malwaredb_api::UPLOAD_SAMPLE_CBOR_URL))
598 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
599 .header("content-type", "application/cbor")
600 .body(bytes)
601 .send()
602 {
603 Ok(res) => {
604 if !res.status().is_success() {
605 info!("Code {} sending {}", res.status(), payload.file_name);
606 }
607 Ok(res.status().is_success())
608 }
609 Err(e) => {
610 let status: String = e
611 .status()
612 .map(|s| s.as_str().to_string())
613 .unwrap_or_default();
614 error!("Error{status} sending {}: {e}", payload.file_name);
615 bail!(e.to_string())
616 }
617 }
618 }
619
620 pub fn partial_search(
626 &self,
627 partial_hash: Option<(PartialHashSearchType, String)>,
628 name: Option<String>,
629 response: PartialHashSearchType,
630 limit: u32,
631 ) -> Result<SearchResponse> {
632 let query = SearchRequest {
633 search: SearchType::Search(SearchRequestParameters {
634 partial_hash,
635 file_name: name,
636 response,
637 limit,
638 labels: None,
639 file_type: None,
640 magic: None,
641 }),
642 };
643
644 self.do_search_request(&query)
645 }
646
647 #[allow(clippy::too_many_arguments)]
653 pub fn partial_search_labels_type(
654 &self,
655 partial_hash: Option<(PartialHashSearchType, String)>,
656 name: Option<String>,
657 response: PartialHashSearchType,
658 labels: Option<Vec<String>>,
659 file_type: Option<String>,
660 magic: Option<String>,
661 limit: u32,
662 ) -> Result<SearchResponse> {
663 let query = SearchRequest {
664 search: SearchType::Search(SearchRequestParameters {
665 partial_hash,
666 file_name: name,
667 response,
668 limit,
669 file_type,
670 magic,
671 labels,
672 }),
673 };
674
675 self.do_search_request(&query)
676 }
677
678 pub fn next_page_search(&self, response: &SearchResponse) -> Result<SearchResponse> {
684 if let Some(uuid) = response.pagination {
685 let request = SearchRequest {
686 search: SearchType::Continuation(uuid),
687 };
688 return self.do_search_request(&request);
689 }
690
691 bail!("Pagination not available")
692 }
693
694 fn do_search_request(&self, query: &SearchRequest) -> Result<SearchResponse> {
695 ensure!(
696 query.is_valid(),
697 "Query isn't valid: hash isn't hexidecimal or both the hashes and file name are empty"
698 );
699
700 let response = self
701 .client
702 .post(format!("{}{}", self.url, malwaredb_api::SEARCH_URL))
703 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
704 .json(query)
705 .send()?
706 .json::<ServerResponse<SearchResponse>>()
707 .context(MDB_CLIENT_ERROR_CONTEXT)?;
708
709 match response {
710 ServerResponse::Success(search) => Ok(search),
711 ServerResponse::Error(e) => Err(e.into()),
712 }
713 }
714
715 pub fn retrieve(&self, hash: &str, cart: bool) -> Result<Vec<u8>> {
722 let api_endpoint = if cart {
723 format!("{}{hash}", malwaredb_api::DOWNLOAD_SAMPLE_CART_URL)
724 } else {
725 format!("{}{hash}", malwaredb_api::DOWNLOAD_SAMPLE_URL)
726 };
727
728 let res = self
729 .client
730 .get(format!("{}{api_endpoint}", self.url))
731 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
732 .send()?;
733
734 if !res.status().is_success() {
735 bail!("Received code {}", res.status());
736 }
737
738 let content_digest = res.headers().get("content-digest").map(ToOwned::to_owned);
739 let body = res.bytes()?;
740 let bytes = body.to_vec();
741
742 if let Some(digest) = content_digest {
744 let hash = HashType::from_content_digest_header(digest.to_str()?)?;
745 if hash.verify(&bytes) {
746 trace!("Hash verified for sample {hash}");
747 } else {
748 error!("Hash mismatch for sample {hash}");
749 }
750 } else {
751 warn!("No content digest header received for sample {hash}");
752 }
753
754 Ok(bytes)
755 }
756
757 pub fn report(&self, hash: &str) -> Result<Report> {
764 let response = self
765 .client
766 .get(format!("{}{}/{hash}", self.url, malwaredb_api::SAMPLE_REPORT_URL))
767 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
768 .send()?
769 .json::<ServerResponse<Report>>()
770 .context(MDB_CLIENT_ERROR_CONTEXT)?;
771
772 match response {
773 ServerResponse::Success(report) => Ok(report),
774 ServerResponse::Error(e) => Err(e.into()),
775 }
776 }
777
778 pub fn similar(&self, contents: &[u8]) -> Result<SimilarSamplesResponse> {
786 let mut hashes = vec![];
787 let ssdeep_hash = FuzzyHash::new(contents);
788
789 let lzjd_str = LzDigest::from(contents).to_string();
790 hashes.push((malwaredb_api::SimilarityHashType::LZJD, lzjd_str));
791 hashes.push((malwaredb_api::SimilarityHashType::SSDeep, ssdeep_hash.to_string()));
792
793 let mut builder = TlshBuilder::new(
794 tlsh_fixed::BucketKind::Bucket256,
795 tlsh_fixed::ChecksumKind::ThreeByte,
796 tlsh_fixed::Version::Version4,
797 );
798
799 builder.update(contents);
800 if let Ok(hasher) = builder.build() {
801 hashes.push((malwaredb_api::SimilarityHashType::TLSH, hasher.hash()));
802 }
803
804 if let Ok(exe) = EXE::from(contents)
805 && let Some(imports) = exe.imports
806 {
807 hashes
808 .push((malwaredb_api::SimilarityHashType::ImportHash, hex::encode(imports.hash())));
809 hashes.push((malwaredb_api::SimilarityHashType::FuzzyImportHash, imports.fuzzy_hash()));
810 }
811
812 let request = malwaredb_api::SimilarSamplesRequest { hashes };
813
814 let response = self
815 .client
816 .post(format!("{}{}", self.url, malwaredb_api::SIMILAR_SAMPLES_URL))
817 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
818 .json(&request)
819 .send()?
820 .json::<ServerResponse<SimilarSamplesResponse>>()
821 .context(MDB_CLIENT_ERROR_CONTEXT)?;
822
823 match response {
824 ServerResponse::Success(similar) => Ok(similar),
825 ServerResponse::Error(e) => Err(e.into()),
826 }
827 }
828
829 pub fn yara_search(&self, yara: &str) -> Result<YaraSearchRequestResponse> {
835 let yara = YaraSearchRequest {
836 rules: vec![yara.to_string()],
837 response: PartialHashSearchType::SHA256,
838 };
839
840 let response = self
841 .client
842 .post(format!("{}{}", self.url, malwaredb_api::YARA_SEARCH_URL))
843 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
844 .json(&yara)
845 .send()?
846 .json::<ServerResponse<YaraSearchRequestResponse>>()?;
847
848 match response {
849 ServerResponse::Success(similar) => Ok(similar),
850 ServerResponse::Error(e) => Err(e.into()),
851 }
852 }
853
854 pub fn yara_result(&self, uuid: Uuid) -> Result<YaraSearchResponse> {
860 let response = self
861 .client
862 .get(format!("{}{}/{uuid}", self.url, malwaredb_api::YARA_SEARCH_URL))
863 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
864 .send()?
865 .json::<ServerResponse<YaraSearchResponse>>()?;
866
867 match response {
868 ServerResponse::Success(sources) => Ok(sources),
869 ServerResponse::Error(e) => Err(e.into()),
870 }
871 }
872}
873
874impl Debug for MdbClient {
875 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
876 use crate::MDB_VERSION;
877
878 writeln!(f, "MDB Client v{MDB_VERSION}: {}", self.url)
879 }
880}
881
882#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
897pub struct IterableHashSearchResult<'a> {
898 pub response: SearchResponse,
900
901 client: &'a MdbClient,
903}
904
905impl<'a> IterableHashSearchResult<'a> {
906 #[must_use]
908 pub fn from(response: SearchResponse, client: &'a MdbClient) -> Self {
909 Self { response, client }
910 }
911}
912
913impl Iterator for IterableHashSearchResult<'_> {
914 type Item = String;
915
916 fn next(&mut self) -> Option<Self::Item> {
917 if let Some(hash) = self.response.hashes.pop() {
918 Some(hash)
919 } else if let Some(uuid) = self.response.pagination {
920 let request = SearchRequest {
921 search: SearchType::Continuation(uuid),
922 };
923
924 self.response = match self.client.do_search_request(&request) {
925 Ok(response) => response,
926 Err(e) => {
927 warn!("Failed to continue search: {e}");
928 return None;
929 }
930 };
931
932 self.response.hashes.pop()
933 } else {
934 None
935 }
936 }
937}
938
939#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
941pub struct IterableSampleSearchResult<'a> {
942 pub response: SearchResponse,
944
945 client: &'a MdbClient,
947}
948
949impl<'a> IterableSampleSearchResult<'a> {
950 #[must_use]
952 pub fn from(response: SearchResponse, client: &'a MdbClient) -> Self {
953 Self { response, client }
954 }
955}
956
957impl Iterator for IterableSampleSearchResult<'_> {
958 type Item = Vec<u8>;
959
960 fn next(&mut self) -> Option<Self::Item> {
961 if let Some(hash) = self.response.hashes.pop() {
962 let binary = match self.client.retrieve(&hash, false) {
963 Ok(binary) => binary,
964 Err(e) => {
965 error!("Failed to download {hash}: {e}");
966 return None;
967 }
968 };
969 Some(binary)
970 } else if let Some(uuid) = self.response.pagination {
971 let request = SearchRequest {
972 search: SearchType::Continuation(uuid),
973 };
974
975 self.response = match self.client.do_search_request(&request) {
976 Ok(response) => response,
977 Err(e) => {
978 warn!("Failed to continue search: {e}");
979 return None;
980 }
981 };
982
983 if let Some(hash) = self.response.hashes.pop() {
984 let binary = match self.client.retrieve(&hash, false) {
985 Ok(binary) => binary,
986 Err(e) => {
987 error!("Failed to download {hash}: {e}");
988 return None;
989 }
990 };
991 Some(binary)
992 } else {
993 None
994 }
995 } else {
996 None
997 }
998 }
999}