use std::sync::OnceLock;
use anyhow::{Result, ensure};
pub const USER_AGENT: &str = concat!("versatiles/", env!("CARGO_PKG_VERSION"), " (+https://versatiles.org)");
static USER_AGENT_WITH_PRODUCT: OnceLock<String> = OnceLock::new();
pub fn set_product(name: &str, version: &str, url: Option<&str>) -> Result<()> {
ensure!(is_token(name), "product name {name:?} is not a valid User-Agent token");
ensure!(
is_token(version),
"product version {version:?} is not a valid User-Agent token"
);
if let Some(url) = url {
ensure!(!url.is_empty(), "product url is empty");
ensure!(
!url.chars().any(|c| c.is_whitespace() || c.is_control() || c == ')'),
"product url {url:?} would not survive being written into a User-Agent comment"
);
}
let comment = url.map(|url| format!(" (+{url})")).unwrap_or_default();
let header = format!("{USER_AGENT} {name}/{version}{comment}");
if USER_AGENT_WITH_PRODUCT.set(header).is_err() {
log::debug!(
"ignoring set_product({name:?}, {version:?}): the User-Agent already reads {:?}",
user_agent()
);
}
Ok(())
}
#[must_use]
pub fn user_agent() -> &'static str {
USER_AGENT_WITH_PRODUCT.get().map_or(USER_AGENT, String::as_str)
}
fn is_token(text: &str) -> bool {
!text.is_empty()
&& text
.chars()
.all(|c| c.is_ascii_alphanumeric() || "!#$%&'*+-.^_`|~".contains(c))
}
mod data_reader;
mod data_reader_blob;
mod data_reader_file;
mod data_reader_http;
#[cfg(feature = "ssh2")]
mod data_reader_sftp;
mod data_writer;
mod data_writer_blob;
mod data_writer_file;
#[cfg(feature = "ssh2")]
mod data_writer_sftp;
mod network_reader;
#[cfg(feature = "ssh2")]
mod network_writer;
mod retry;
#[cfg(feature = "ssh2")]
mod sftp_pool;
#[cfg(feature = "ssh2")]
pub mod sftp_utils;
#[cfg(feature = "ssh2")]
mod sftp_wrappers;
#[cfg(all(feature = "ssh2", test, unix))]
pub(crate) mod test_sftp_server;
#[cfg(feature = "ssh2")]
pub use sftp_wrappers::*;
mod value_reader;
mod value_reader_blob;
mod value_reader_file;
mod value_reader_slice;
mod value_writer;
mod value_writer_blob;
mod value_writer_file;
pub use data_reader::*;
pub use data_reader_blob::*;
pub use data_reader_file::*;
pub use data_reader_http::*;
#[cfg(feature = "ssh2")]
pub use data_reader_sftp::*;
pub use data_writer::*;
pub use data_writer_blob::*;
pub use data_writer_file::*;
#[cfg(feature = "ssh2")]
pub use data_writer_sftp::*;
pub use value_reader::*;
pub use value_reader_blob::*;
pub use value_reader_file::*;
pub use value_reader_slice::*;
pub use value_writer::*;
pub use value_writer_blob::*;
pub use value_writer_file::*;
#[cfg(test)]
mod tests {
use super::{USER_AGENT, is_token, set_product, user_agent};
#[test]
fn user_agent_has_expected_shape() {
assert!(USER_AGENT.starts_with("versatiles/"), "got: {USER_AGENT}");
assert!(USER_AGENT.contains(env!("CARGO_PKG_VERSION")), "got: {USER_AGENT}");
assert!(USER_AGENT.contains("(+https://versatiles.org)"), "got: {USER_AGENT}");
}
#[test]
fn product_token_is_appended_once() {
assert_eq!(user_agent(), USER_AGENT);
assert!(set_product("VersaTiles Studio", "0.4.0", None).is_err());
assert!(set_product("VersaTiles-Studio", "0 4 0", None).is_err());
assert!(set_product("VersaTiles-Studio", "0.4.0", Some("https://x.org/a)b")).is_err());
assert!(set_product("VersaTiles-Studio", "0.4.0", Some("")).is_err());
assert_eq!(user_agent(), USER_AGENT, "a rejected call must not set anything");
set_product("VersaTiles-Studio", "0.4.0", Some("https://versatiles.org/studio")).unwrap();
assert_eq!(
user_agent(),
format!("{USER_AGENT} VersaTiles-Studio/0.4.0 (+https://versatiles.org/studio)")
);
set_product("Something-Else", "9.9.9", None).unwrap();
assert!(user_agent().ends_with("VersaTiles-Studio/0.4.0 (+https://versatiles.org/studio)"));
}
#[test]
fn tokens_are_rfc9110_tokens() {
assert!(is_token("VersaTiles-Studio"));
assert!(is_token("0.4.0-beta.1"));
assert!(is_token("a"));
assert!(!is_token(""));
assert!(!is_token("has space"));
assert!(!is_token("has/slash"));
assert!(!is_token("has(paren"));
}
}