1use hex;
7use hmac::{Hmac, Mac};
8use log::{info, warn};
9use reqwest::{Client, multipart};
10use serde::Serialize;
11use sha2::Sha256;
12use std::borrow::Cow;
13use std::collections::HashMap;
14use std::fs::File;
15use std::io::{Read, Seek, SeekFrom};
16use std::sync::Arc;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18use url::Url;
19
20use crate::{VeracodeConfig, VeracodeError};
21
22type HmacSha256 = Hmac<Sha256>;
24
25const INVALID_URL_MSG: &str = "Invalid URL";
27const INVALID_API_KEY_MSG: &str = "Invalid API key format - must be hex string";
28const INVALID_NONCE_MSG: &str = "Invalid nonce format";
29const HMAC_CREATION_FAILED_MSG: &str = "Failed to create HMAC";
30
31#[derive(Clone)]
36pub struct VeracodeClient {
37 config: VeracodeConfig,
38 client: Client,
39}
40
41impl VeracodeClient {
42 fn build_url_with_params(&self, endpoint: &str, query_params: &[(&str, &str)]) -> String {
44 let estimated_capacity =
46 self.config.base_url.len() + endpoint.len() + query_params.len() * 32; let mut url = String::with_capacity(estimated_capacity);
49 url.push_str(&self.config.base_url);
50 url.push_str(endpoint);
51
52 if !query_params.is_empty() {
53 url.push('?');
54 for (i, (key, value)) in query_params.iter().enumerate() {
55 if i > 0 {
56 url.push('&');
57 }
58 url.push_str(&urlencoding::encode(key));
59 url.push('=');
60 url.push_str(&urlencoding::encode(value));
61 }
62 }
63
64 url
65 }
66
67 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
77 let mut client_builder = Client::builder();
78
79 if !config.validate_certificates {
81 client_builder = client_builder
82 .danger_accept_invalid_certs(true)
83 .danger_accept_invalid_hostnames(true);
84 }
85
86 client_builder = client_builder
88 .connect_timeout(Duration::from_secs(config.connect_timeout))
89 .timeout(Duration::from_secs(config.request_timeout));
90
91 let client = client_builder.build().map_err(VeracodeError::Http)?;
92 Ok(Self { config, client })
93 }
94
95 #[must_use]
97 pub fn base_url(&self) -> &str {
98 &self.config.base_url
99 }
100
101 #[must_use]
103 pub fn config(&self) -> &VeracodeConfig {
104 &self.config
105 }
106
107 #[must_use]
109 pub fn client(&self) -> &Client {
110 &self.client
111 }
112
113 async fn execute_with_retry<F>(
129 &self,
130 request_builder: F,
131 operation_name: Cow<'_, str>,
132 ) -> Result<reqwest::Response, VeracodeError>
133 where
134 F: Fn() -> reqwest::RequestBuilder,
135 {
136 let retry_config = &self.config.retry_config;
137 let start_time = Instant::now();
138 let mut total_delay = std::time::Duration::from_millis(0);
139
140 if retry_config.max_attempts == 0 {
142 return match request_builder().send().await {
143 Ok(response) => Ok(response),
144 Err(e) => Err(VeracodeError::Http(e)),
145 };
146 }
147
148 let mut last_error = None;
149 let mut rate_limit_attempts = 0;
150
151 for attempt in 1..=retry_config.max_attempts + 1 {
152 match request_builder().send().await {
154 Ok(response) => {
155 if response.status().as_u16() == 429 {
157 let retry_after_seconds = response
159 .headers()
160 .get("retry-after")
161 .and_then(|h| h.to_str().ok())
162 .and_then(|s| s.parse::<u64>().ok());
163
164 let message = "HTTP 429: Rate limit exceeded".to_string();
165 let veracode_error = VeracodeError::RateLimited {
166 retry_after_seconds,
167 message,
168 };
169
170 rate_limit_attempts += 1;
172
173 if attempt > retry_config.max_attempts
175 || rate_limit_attempts > retry_config.rate_limit_max_attempts
176 {
177 last_error = Some(veracode_error);
178 break;
179 }
180
181 let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
183 total_delay += delay;
184
185 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
187 let msg = format!(
188 "{} exceeded maximum total retry time of {}ms after {} attempts",
189 operation_name, retry_config.max_total_delay_ms, attempt
190 );
191 last_error = Some(VeracodeError::RetryExhausted(msg));
192 break;
193 }
194
195 let wait_time = match retry_after_seconds {
197 Some(seconds) => format!("{seconds}s (from Retry-After header)"),
198 None => format!("{}s (until next minute window)", delay.as_secs()),
199 };
200 warn!(
201 "🚦 {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
202 );
203
204 tokio::time::sleep(delay).await;
206 last_error = Some(veracode_error);
207 continue;
208 }
209
210 if attempt > 1 {
211 info!("✅ {operation_name} succeeded on attempt {attempt}");
213 }
214 return Ok(response);
215 }
216 Err(e) => {
217 let veracode_error = VeracodeError::Http(e);
219
220 if attempt > retry_config.max_attempts
222 || !retry_config.is_retryable_error(&veracode_error)
223 {
224 last_error = Some(veracode_error);
225 break;
226 }
227
228 let delay = retry_config.calculate_delay(attempt);
230 total_delay += delay;
231
232 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
234 let msg = format!(
236 "{} exceeded maximum total retry time of {}ms after {} attempts",
237 operation_name, retry_config.max_total_delay_ms, attempt
238 );
239 last_error = Some(VeracodeError::RetryExhausted(msg));
240 break;
241 }
242
243 warn!(
245 "⚠️ {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
246 delay.as_millis()
247 );
248
249 tokio::time::sleep(delay).await;
251 last_error = Some(veracode_error);
252 }
253 }
254 }
255
256 match last_error {
258 Some(error) => {
259 let elapsed = start_time.elapsed();
260 match error {
261 VeracodeError::RetryExhausted(_) => Err(error),
262 _ => {
263 let msg = format!(
264 "{} failed after {} attempts over {}ms: {}",
265 operation_name,
266 retry_config.max_attempts + 1,
267 elapsed.as_millis(),
268 error
269 );
270 Err(VeracodeError::RetryExhausted(msg))
271 }
272 }
273 }
274 None => {
275 let msg = format!(
276 "{} failed after {} attempts with unknown error",
277 operation_name,
278 retry_config.max_attempts + 1
279 );
280 Err(VeracodeError::RetryExhausted(msg))
281 }
282 }
283 }
284
285 fn generate_hmac_signature(
287 &self,
288 method: &str,
289 url: &str,
290 timestamp: u64,
291 nonce: &str,
292 ) -> Result<String, VeracodeError> {
293 let url_parsed = Url::parse(url)
294 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
295
296 let path_and_query = match url_parsed.query() {
297 Some(query) => format!("{}?{}", url_parsed.path(), query),
298 None => url_parsed.path().to_string(),
299 };
300
301 let host = url_parsed.host_str().unwrap_or("");
302
303 let data = format!(
306 "id={}&host={}&url={}&method={}",
307 self.config.credentials.expose_api_id(),
308 host,
309 path_and_query,
310 method
311 );
312
313 let timestamp_str = timestamp.to_string();
314 let ver_str = "vcode_request_version_1";
315
316 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
318 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
319
320 let nonce_bytes = hex::decode(nonce)
321 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
322
323 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
325 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
326 mac1.update(&nonce_bytes);
327 let hashed_nonce = mac1.finalize().into_bytes();
328
329 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
331 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
332 mac2.update(timestamp_str.as_bytes());
333 let hashed_timestamp = mac2.finalize().into_bytes();
334
335 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
337 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
338 mac3.update(ver_str.as_bytes());
339 let hashed_ver_str = mac3.finalize().into_bytes();
340
341 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
343 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
344 mac4.update(data.as_bytes());
345 let signature = mac4.finalize().into_bytes();
346
347 Ok(hex::encode(signature).to_lowercase())
349 }
350
351 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
353 let timestamp = SystemTime::now()
354 .duration_since(UNIX_EPOCH)
355 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
356 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
360 let nonce = hex::encode(nonce_bytes);
361
362 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
363
364 Ok(format!(
365 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
366 self.config.credentials.expose_api_id(),
367 timestamp,
368 nonce,
369 signature
370 ))
371 }
372
373 pub async fn get(
384 &self,
385 endpoint: &str,
386 query_params: Option<&[(String, String)]>,
387 ) -> Result<reqwest::Response, VeracodeError> {
388 let param_count = query_params.map_or(0, |p| p.len());
390 let estimated_capacity = self.config.base_url.len() + endpoint.len() + param_count * 32;
391 let mut url = String::with_capacity(estimated_capacity);
392 url.push_str(&self.config.base_url);
393 url.push_str(endpoint);
394
395 if let Some(params) = query_params
396 && !params.is_empty()
397 {
398 url.push('?');
399 for (i, (key, value)) in params.iter().enumerate() {
400 if i > 0 {
401 url.push('&');
402 }
403 url.push_str(key);
404 url.push('=');
405 url.push_str(value);
406 }
407 }
408
409 let request_builder = || {
411 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
413 return self.client.get("invalid://url");
414 };
415
416 self.client
417 .get(&url)
418 .header("Authorization", auth_header)
419 .header("Content-Type", "application/json")
420 };
421
422 let operation_name = if endpoint.len() < 50 {
424 Cow::Owned(format!("GET {endpoint}"))
425 } else {
426 Cow::Borrowed("GET [long endpoint]")
427 };
428 self.execute_with_retry(request_builder, operation_name)
429 .await
430 }
431
432 pub async fn post<T: Serialize>(
443 &self,
444 endpoint: &str,
445 body: Option<&T>,
446 ) -> Result<reqwest::Response, VeracodeError> {
447 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
448 url.push_str(&self.config.base_url);
449 url.push_str(endpoint);
450
451 let serialized_body = if let Some(body) = body {
453 Some(serde_json::to_string(body)?)
454 } else {
455 None
456 };
457
458 let request_builder = || {
460 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
462 return self.client.post("invalid://url");
463 };
464
465 let mut request = self
466 .client
467 .post(&url)
468 .header("Authorization", auth_header)
469 .header("Content-Type", "application/json");
470
471 if let Some(ref body_str) = serialized_body {
472 request = request.body(body_str.clone());
473 }
474
475 request
476 };
477
478 let operation_name = if endpoint.len() < 50 {
479 Cow::Owned(format!("POST {endpoint}"))
480 } else {
481 Cow::Borrowed("POST [long endpoint]")
482 };
483 self.execute_with_retry(request_builder, operation_name)
484 .await
485 }
486
487 pub async fn put<T: Serialize>(
498 &self,
499 endpoint: &str,
500 body: Option<&T>,
501 ) -> Result<reqwest::Response, VeracodeError> {
502 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
503 url.push_str(&self.config.base_url);
504 url.push_str(endpoint);
505
506 let serialized_body = if let Some(body) = body {
508 Some(serde_json::to_string(body)?)
509 } else {
510 None
511 };
512
513 let request_builder = || {
515 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
517 return self.client.put("invalid://url");
518 };
519
520 let mut request = self
521 .client
522 .put(&url)
523 .header("Authorization", auth_header)
524 .header("Content-Type", "application/json");
525
526 if let Some(ref body_str) = serialized_body {
527 request = request.body(body_str.clone());
528 }
529
530 request
531 };
532
533 let operation_name = if endpoint.len() < 50 {
534 Cow::Owned(format!("PUT {endpoint}"))
535 } else {
536 Cow::Borrowed("PUT [long endpoint]")
537 };
538 self.execute_with_retry(request_builder, operation_name)
539 .await
540 }
541
542 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
552 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
553 url.push_str(&self.config.base_url);
554 url.push_str(endpoint);
555
556 let request_builder = || {
558 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
560 return self.client.delete("invalid://url");
561 };
562
563 self.client
564 .delete(&url)
565 .header("Authorization", auth_header)
566 .header("Content-Type", "application/json")
567 };
568
569 let operation_name = if endpoint.len() < 50 {
570 Cow::Owned(format!("DELETE {endpoint}"))
571 } else {
572 Cow::Borrowed("DELETE [long endpoint]")
573 };
574 self.execute_with_retry(request_builder, operation_name)
575 .await
576 }
577
578 pub async fn handle_response(
590 response: reqwest::Response,
591 ) -> Result<reqwest::Response, VeracodeError> {
592 if !response.status().is_success() {
593 let status = response.status();
594 let error_text = response.text().await?;
595 return Err(VeracodeError::InvalidResponse(format!(
596 "HTTP {status}: {error_text}"
597 )));
598 }
599 Ok(response)
600 }
601
602 pub async fn get_with_query(
615 &self,
616 endpoint: &str,
617 query_params: Option<Vec<(String, String)>>,
618 ) -> Result<reqwest::Response, VeracodeError> {
619 let query_slice = query_params.as_deref();
620 let response = self.get(endpoint, query_slice).await?;
621 Self::handle_response(response).await
622 }
623
624 pub async fn post_with_response<T: Serialize>(
635 &self,
636 endpoint: &str,
637 body: Option<&T>,
638 ) -> Result<reqwest::Response, VeracodeError> {
639 let response = self.post(endpoint, body).await?;
640 Self::handle_response(response).await
641 }
642
643 pub async fn put_with_response<T: Serialize>(
654 &self,
655 endpoint: &str,
656 body: Option<&T>,
657 ) -> Result<reqwest::Response, VeracodeError> {
658 let response = self.put(endpoint, body).await?;
659 Self::handle_response(response).await
660 }
661
662 pub async fn delete_with_response(
672 &self,
673 endpoint: &str,
674 ) -> Result<reqwest::Response, VeracodeError> {
675 let response = self.delete(endpoint).await?;
676 Self::handle_response(response).await
677 }
678
679 pub async fn get_paginated(
694 &self,
695 endpoint: &str,
696 base_query_params: Option<Vec<(String, String)>>,
697 page_size: Option<u32>,
698 ) -> Result<String, VeracodeError> {
699 let size = page_size.unwrap_or(500);
700 let mut page = 0;
701 let mut all_items = Vec::new();
702 let mut page_info = None;
703
704 loop {
705 let mut query_params = base_query_params.clone().unwrap_or_default();
706 query_params.push(("page".to_string(), page.to_string()));
707 query_params.push(("size".to_string(), size.to_string()));
708
709 let response = self.get_with_query(endpoint, Some(query_params)).await?;
710 let response_text = response.text().await?;
711
712 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
714 if let Some(embedded) = json_value.get("_embedded") {
716 if let Some(items_array) =
717 embedded.as_object().and_then(|obj| obj.values().next())
718 && let Some(items) = items_array.as_array()
719 {
720 if items.is_empty() {
721 break; }
723 all_items.extend(items.clone());
724 }
725 } else if let Some(items) = json_value.as_array() {
726 if items.is_empty() {
728 break;
729 }
730 all_items.extend(items.clone());
731 } else {
732 return Ok(response_text);
734 }
735
736 if let Some(page_obj) = json_value.get("page") {
738 page_info = Some(page_obj.clone());
739 if let (Some(current), Some(total)) = (
740 page_obj.get("number").and_then(|n| n.as_u64()),
741 page_obj.get("totalPages").and_then(|n| n.as_u64()),
742 ) && current + 1 >= total
743 {
744 break; }
746 }
747 } else {
748 return Ok(response_text);
750 }
751
752 page += 1;
753
754 if page > 100 {
756 break;
757 }
758 }
759
760 let combined_response = if let Some(page_info) = page_info {
762 serde_json::json!({
764 "_embedded": {
765 "roles": all_items },
767 "page": page_info
768 })
769 } else {
770 serde_json::Value::Array(all_items)
772 };
773
774 Ok(combined_response.to_string())
775 }
776
777 pub async fn get_with_params(
788 &self,
789 endpoint: &str,
790 params: &[(&str, &str)],
791 ) -> Result<reqwest::Response, VeracodeError> {
792 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
793 url.push_str(&self.config.base_url);
794 url.push_str(endpoint);
795 let mut request_url =
796 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
797
798 if !params.is_empty() {
800 let mut query_pairs = request_url.query_pairs_mut();
801 for (key, value) in params {
802 query_pairs.append_pair(key, value);
803 }
804 }
805
806 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
807
808 let response = self
809 .client
810 .get(request_url)
811 .header("Authorization", auth_header)
812 .header("User-Agent", "Veracode Rust Client")
813 .send()
814 .await?;
815
816 Ok(response)
817 }
818
819 pub async fn post_form(
830 &self,
831 endpoint: &str,
832 params: &[(&str, &str)],
833 ) -> Result<reqwest::Response, VeracodeError> {
834 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
835 url.push_str(&self.config.base_url);
836 url.push_str(endpoint);
837
838 let form_data: Vec<(&str, &str)> = params.to_vec();
840
841 let auth_header = self.generate_auth_header("POST", &url)?;
842
843 let response = self
844 .client
845 .post(&url)
846 .header("Authorization", auth_header)
847 .header("User-Agent", "Veracode Rust Client")
848 .form(&form_data)
849 .send()
850 .await?;
851
852 Ok(response)
853 }
854
855 pub async fn upload_file_multipart(
869 &self,
870 endpoint: &str,
871 params: HashMap<&str, &str>,
872 file_field_name: &str,
873 filename: &str,
874 file_data: Vec<u8>,
875 ) -> Result<reqwest::Response, VeracodeError> {
876 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
877 url.push_str(&self.config.base_url);
878 url.push_str(endpoint);
879
880 let mut form = multipart::Form::new();
882
883 for (key, value) in params {
885 form = form.text(key.to_string(), value.to_string());
886 }
887
888 let part = multipart::Part::bytes(file_data)
890 .file_name(filename.to_string())
891 .mime_str("application/octet-stream")
892 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
893
894 form = form.part(file_field_name.to_string(), part);
895
896 let auth_header = self.generate_auth_header("POST", &url)?;
897
898 let response = self
899 .client
900 .post(&url)
901 .header("Authorization", auth_header)
902 .header("User-Agent", "Veracode Rust Client")
903 .multipart(form)
904 .send()
905 .await?;
906
907 Ok(response)
908 }
909
910 pub async fn upload_file_multipart_put(
924 &self,
925 url: &str,
926 file_field_name: &str,
927 filename: &str,
928 file_data: Vec<u8>,
929 additional_headers: Option<HashMap<&str, &str>>,
930 ) -> Result<reqwest::Response, VeracodeError> {
931 let part = multipart::Part::bytes(file_data)
933 .file_name(filename.to_string())
934 .mime_str("application/octet-stream")
935 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
936
937 let form = multipart::Form::new().part(file_field_name.to_string(), part);
938
939 let auth_header = self.generate_auth_header("PUT", url)?;
940
941 let mut request = self
942 .client
943 .put(url)
944 .header("Authorization", auth_header)
945 .header("User-Agent", "Veracode Rust Client")
946 .multipart(form);
947
948 if let Some(headers) = additional_headers {
950 for (key, value) in headers {
951 request = request.header(key, value);
952 }
953 }
954
955 let response = request.send().await?;
956 Ok(response)
957 }
958
959 pub async fn upload_file_with_query_params(
979 &self,
980 endpoint: &str,
981 query_params: &[(&str, &str)],
982 file_field_name: &str,
983 filename: &str,
984 file_data: Vec<u8>,
985 ) -> Result<reqwest::Response, VeracodeError> {
986 let url = self.build_url_with_params(endpoint, query_params);
988
989 let file_data_arc = Arc::new(file_data);
991
992 let filename_cow: Cow<str> = if filename.len() < 128 {
994 Cow::Borrowed(filename)
995 } else {
996 Cow::Owned(filename.to_string())
997 };
998
999 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1000 Cow::Borrowed(file_field_name)
1001 } else {
1002 Cow::Owned(file_field_name.to_string())
1003 };
1004
1005 let request_builder = || {
1007 let file_data_clone = Arc::clone(&file_data_arc);
1009
1010 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1012 .file_name(filename_cow.to_string())
1013 .mime_str("application/octet-stream")
1014 else {
1015 return self.client.post("invalid://url");
1016 };
1017
1018 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1019
1020 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1022 return self.client.post("invalid://url");
1023 };
1024
1025 self.client
1026 .post(&url)
1027 .header("Authorization", auth_header)
1028 .header("User-Agent", "Veracode Rust Client")
1029 .multipart(form)
1030 };
1031
1032 let operation_name: Cow<str> = if endpoint.len() < 50 {
1034 Cow::Owned(format!("File Upload POST {endpoint}"))
1035 } else {
1036 Cow::Borrowed("File Upload POST [long endpoint]")
1037 };
1038
1039 self.execute_with_retry(request_builder, operation_name)
1040 .await
1041 }
1042
1043 pub async fn post_with_query_params(
1057 &self,
1058 endpoint: &str,
1059 query_params: &[(&str, &str)],
1060 ) -> Result<reqwest::Response, VeracodeError> {
1061 let url = self.build_url_with_params(endpoint, query_params);
1063
1064 let auth_header = self.generate_auth_header("POST", &url)?;
1065
1066 let response = self
1067 .client
1068 .post(&url)
1069 .header("Authorization", auth_header)
1070 .header("User-Agent", "Veracode Rust Client")
1071 .send()
1072 .await?;
1073
1074 Ok(response)
1075 }
1076
1077 pub async fn get_with_query_params(
1091 &self,
1092 endpoint: &str,
1093 query_params: &[(&str, &str)],
1094 ) -> Result<reqwest::Response, VeracodeError> {
1095 let url = self.build_url_with_params(endpoint, query_params);
1097
1098 let auth_header = self.generate_auth_header("GET", &url)?;
1099
1100 let response = self
1101 .client
1102 .get(&url)
1103 .header("Authorization", auth_header)
1104 .header("User-Agent", "Veracode Rust Client")
1105 .send()
1106 .await?;
1107
1108 Ok(response)
1109 }
1110
1111 pub async fn upload_large_file_chunked<F>(
1128 &self,
1129 endpoint: &str,
1130 query_params: &[(&str, &str)],
1131 file_path: &str,
1132 content_type: Option<&str>,
1133 progress_callback: Option<F>,
1134 ) -> Result<reqwest::Response, VeracodeError>
1135 where
1136 F: Fn(u64, u64, f64) + Send + Sync,
1137 {
1138 let url = self.build_url_with_params(endpoint, query_params);
1140
1141 let mut file = File::open(file_path)
1143 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1144
1145 let file_size = file
1146 .metadata()
1147 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1148 .len();
1149
1150 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1153 return Err(VeracodeError::InvalidConfig(format!(
1154 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1155 )));
1156 }
1157
1158 file.seek(SeekFrom::Start(0))
1160 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1161
1162 let mut file_data = Vec::with_capacity(file_size as usize);
1163 file.read_to_end(&mut file_data)
1164 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1165
1166 let file_data_arc = Arc::new(file_data);
1168 let content_type_cow: Cow<str> =
1169 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1170 if ct.len() < 64 {
1171 Cow::Borrowed(ct)
1172 } else {
1173 Cow::Owned(ct.to_string())
1174 }
1175 });
1176
1177 let request_builder = || {
1179 let file_data_clone = Arc::clone(&file_data_arc);
1181
1182 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1184 return self.client.post("invalid://url");
1185 };
1186
1187 self.client
1188 .post(&url)
1189 .header("Authorization", auth_header)
1190 .header("User-Agent", "Veracode Rust Client")
1191 .header("Content-Type", content_type_cow.as_ref())
1192 .header("Content-Length", file_size.to_string())
1193 .body((*file_data_clone).clone())
1194 };
1195
1196 if let Some(callback) = progress_callback {
1198 callback(file_size, file_size, 100.0);
1199 }
1200
1201 let operation_name: Cow<str> = if endpoint.len() < 50 {
1203 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1204 } else {
1205 Cow::Borrowed("Large File Upload POST [long endpoint]")
1206 };
1207
1208 self.execute_with_retry(request_builder, operation_name)
1209 .await
1210 }
1211
1212 pub async fn upload_file_binary(
1231 &self,
1232 endpoint: &str,
1233 query_params: &[(&str, &str)],
1234 file_data: Vec<u8>,
1235 content_type: &str,
1236 ) -> Result<reqwest::Response, VeracodeError> {
1237 let url = self.build_url_with_params(endpoint, query_params);
1239
1240 let file_data_arc = Arc::new(file_data);
1242 let file_size = file_data_arc.len();
1243
1244 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1246 Cow::Borrowed(content_type)
1247 } else {
1248 Cow::Owned(content_type.to_string())
1249 };
1250
1251 let request_builder = || {
1253 let file_data_clone = Arc::clone(&file_data_arc);
1255
1256 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1258 return self.client.post("invalid://url");
1259 };
1260
1261 self.client
1262 .post(&url)
1263 .header("Authorization", auth_header)
1264 .header("User-Agent", "Veracode Rust Client")
1265 .header("Content-Type", content_type_cow.as_ref())
1266 .header("Content-Length", file_size.to_string())
1267 .body((*file_data_clone).clone())
1268 };
1269
1270 let operation_name: Cow<str> = if endpoint.len() < 50 {
1272 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1273 } else {
1274 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1275 };
1276
1277 self.execute_with_retry(request_builder, operation_name)
1278 .await
1279 }
1280}