use std::path::PathBuf;
use std::time::Duration;
use crate::errors::*;
use crate::http_client::HeaderMap;
use crate::{get_target, DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE};
#[derive(Clone, Debug, Default)]
pub(crate) struct RequestConfig {
pub(crate) timeout: Option<Duration>,
pub(crate) headers: HeaderMap,
pub(crate) retries: u32,
pub(crate) client: crate::http_client::ClientOverride,
pub(crate) header_error: Option<String>,
}
impl RequestConfig {
pub(crate) fn insert_header<N, V>(&mut self, name: N, value: V)
where
N: ::core::convert::TryInto<crate::http_client::header::HeaderName>,
V: ::core::convert::TryInto<crate::http_client::header::HeaderValue>,
{
let name = match name.try_into() {
Ok(n) => n,
Err(_) => {
if self.header_error.is_none() {
self.header_error =
Some("invalid HTTP header name passed to `request_header`".to_string());
}
return;
}
};
let value = match value.try_into() {
Ok(v) => v,
Err(_) => {
if self.header_error.is_none() {
self.header_error =
Some("invalid HTTP header value passed to `request_header`".to_string());
}
return;
}
};
self.headers.insert(name, value);
}
pub(crate) fn check(&self) -> Result<()> {
match &self.header_error {
Some(msg) => Err(Error::Config(msg.clone())),
None => Ok(()),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct CommonBuilderConfig {
pub request: RequestConfig,
pub target: Option<String>,
pub asset_identifier: Option<String>,
pub bin_name: Option<String>,
pub bin_install_path: Option<PathBuf>,
pub bin_path_in_archive: Option<String>,
pub(crate) bin_path_in_archive_auto: bool,
pub show_download_progress: bool,
pub show_output: bool,
pub no_confirm: bool,
pub current_version: Option<String>,
pub release_tag: Option<String>,
pub progress_template: String,
pub progress_chars: String,
pub auth_token: Option<String>,
pub progress_callback: Option<crate::ProgressCallback>,
pub verify: Option<crate::VerifyCallback>,
pub asset_matcher: Option<crate::AssetMatcher>,
#[cfg(feature = "checksums")]
pub checksum: Option<crate::Checksum>,
#[cfg(feature = "signatures")]
pub verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>,
}
impl Default for CommonBuilderConfig {
fn default() -> Self {
Self {
request: RequestConfig::default(),
target: None,
asset_identifier: None,
bin_name: None,
bin_install_path: None,
bin_path_in_archive: None,
bin_path_in_archive_auto: false,
show_download_progress: false,
show_output: true,
no_confirm: false,
current_version: None,
release_tag: None,
progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(),
progress_chars: DEFAULT_PROGRESS_CHARS.to_string(),
auth_token: None,
progress_callback: None,
verify: None,
asset_matcher: None,
#[cfg(feature = "checksums")]
checksum: None,
#[cfg(feature = "signatures")]
verifying_keys: vec![],
}
}
}
impl CommonBuilderConfig {
pub(crate) fn build(&self) -> Result<CommonConfig> {
self.request.check()?;
Ok(CommonConfig {
request: self.request.clone(),
target: self
.target
.clone()
.unwrap_or_else(|| get_target().to_owned()),
asset_identifier: self.asset_identifier.clone(),
current_version: self
.current_version
.clone()
.ok_or_else(|| Error::Config("`current_version` required (call `.current_version(...)`)" .to_string()))?,
release_tag: self.release_tag.clone(),
bin_name: self
.bin_name
.clone()
.ok_or_else(|| Error::Config("`bin_name` required (call `.bin_name(...)`)" .to_string()))?,
bin_install_path: match &self.bin_install_path {
Some(p) => p.clone(),
None => std::env::current_exe()?,
},
bin_path_in_archive: self
.bin_path_in_archive
.clone()
.ok_or_else(|| Error::Config("`bin_path_in_archive` required (call `.bin_name(...)` or `.bin_path_in_archive(...)`)" .to_string()))?,
show_download_progress: self.show_download_progress,
show_output: self.show_output,
no_confirm: self.no_confirm,
progress_template: self.progress_template.clone(),
progress_chars: self.progress_chars.clone(),
auth_token: self.auth_token.clone(),
progress_callback: self.progress_callback.clone(),
verify: self.verify.clone(),
asset_matcher: self.asset_matcher.clone(),
#[cfg(feature = "checksums")]
checksum: self.checksum.clone(),
#[cfg(feature = "signatures")]
verifying_keys: self.verifying_keys.clone(),
})
}
}
#[derive(Debug)]
pub(crate) struct CommonConfig {
pub request: RequestConfig,
pub target: String,
pub asset_identifier: Option<String>,
pub current_version: String,
pub release_tag: Option<String>,
pub bin_name: String,
pub bin_install_path: PathBuf,
pub bin_path_in_archive: String,
pub show_download_progress: bool,
pub show_output: bool,
pub no_confirm: bool,
pub progress_template: String,
pub progress_chars: String,
pub auth_token: Option<String>,
pub progress_callback: Option<crate::ProgressCallback>,
pub verify: Option<crate::VerifyCallback>,
pub asset_matcher: Option<crate::AssetMatcher>,
#[cfg(feature = "checksums")]
pub checksum: Option<crate::Checksum>,
#[cfg(feature = "signatures")]
pub verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>,
}
#[cfg(test)]
mod tests {
use super::{CommonBuilderConfig, RequestConfig};
#[test]
fn insert_header_records_invalid_value_error() {
let mut req = RequestConfig::default();
req.insert_header("x-ok", "bad\nvalue");
assert!(
req.headers.get("x-ok").is_none(),
"an invalid value must not be inserted"
);
let err = req
.check()
.expect_err("invalid value must surface from check()");
match err {
crate::errors::Error::Config(msg) => {
assert!(
msg.contains("value"),
"value-conversion error should mention the value, got: {}",
msg
);
}
other => panic!("expected Error::Config, got {:?}", other),
}
}
#[test]
fn insert_header_records_invalid_name_error() {
let mut req = RequestConfig::default();
req.insert_header("inva lid", "ok");
assert!(req.headers.get("inva lid").is_none());
match req
.check()
.expect_err("invalid name must surface from check()")
{
crate::errors::Error::Config(msg) => assert!(msg.contains("name")),
other => panic!("expected Error::Config, got {:?}", other),
}
}
#[test]
fn insert_header_first_error_wins() {
let mut req = RequestConfig::default();
req.insert_header("bad name", "ok"); req.insert_header("x-ok", "bad\nvalue"); match req.check().expect_err("an error is recorded") {
crate::errors::Error::Config(msg) => assert!(
msg.contains("name"),
"the first (name) error must win, got: {}",
msg
),
other => panic!("expected Error::Config, got {:?}", other),
}
}
#[test]
fn insert_header_valid_then_invalid_still_keeps_valid_header() {
let mut req = RequestConfig::default();
req.insert_header("x-good", "value");
req.insert_header("x-bad", "bad\nvalue");
assert_eq!(req.headers.get("x-good").unwrap(), "value");
assert!(req.check().is_err());
}
#[test]
fn check_is_ok_when_no_error_recorded() {
let mut req = RequestConfig::default();
req.insert_header("x-fine", "ok");
assert!(req.check().is_ok());
assert_eq!(req.headers.get("x-fine").unwrap(), "ok");
}
#[test]
fn build_requires_current_version_bin_name_and_archive_path() {
assert!(CommonBuilderConfig::default().build().is_err());
let cfg = CommonBuilderConfig {
current_version: Some("0.1.0".to_string()),
..Default::default()
};
assert!(cfg.build().is_err());
let cfg = CommonBuilderConfig {
current_version: Some("0.1.0".to_string()),
bin_name: Some("app".to_string()),
bin_path_in_archive: Some("app".to_string()),
..Default::default()
};
let built = cfg.build().expect("all required fields present");
assert_eq!(built.current_version, "0.1.0");
assert_eq!(built.bin_name, "app");
}
#[test]
fn build_resolves_target_and_install_path_defaults() {
let base = CommonBuilderConfig {
current_version: Some("0.1.0".to_string()),
bin_name: Some("app".to_string()),
bin_path_in_archive: Some("app".to_string()),
..Default::default()
};
let built = base.clone().build().unwrap();
assert_eq!(built.target.as_str(), crate::get_target());
assert!(!built.bin_install_path.as_os_str().is_empty());
let with_target = CommonBuilderConfig {
target: Some("custom-target".to_string()),
..base
};
assert_eq!(with_target.build().unwrap().target, "custom-target");
}
#[test]
fn build_error_message_names_the_setter_for_current_version() {
let err = CommonBuilderConfig::default().build().unwrap_err();
match err {
crate::errors::Error::Config(msg) => {
assert!(
msg.contains("current_version") && msg.contains("current_version("),
"error message must name the setter, got: {}",
msg
);
}
other => panic!("expected Error::Config, got {:?}", other),
}
}
#[test]
fn build_error_message_names_the_setter_for_bin_name() {
let err = CommonBuilderConfig {
current_version: Some("0.1.0".to_string()),
..Default::default()
}
.build()
.unwrap_err();
match err {
crate::errors::Error::Config(msg) => {
assert!(
msg.contains("bin_name") && msg.contains("bin_name("),
"error message must name the setter, got: {}",
msg
);
}
other => panic!("expected Error::Config, got {:?}", other),
}
}
}