mod ado_net;
mod ado_parser;
mod jdbc;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(15);
const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
use super::AuthMethod;
use crate::EncryptionLevel;
use ado_net::*;
use jdbc::*;
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
use secrecy::SecretString;
#[derive(Clone, Debug)]
pub struct Config {
pub(crate) host: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) database: Option<String>,
pub(crate) instance_name: Option<String>,
pub(crate) application_name: Option<String>,
pub(crate) encryption: EncryptionLevel,
pub(crate) trust: TrustConfig,
pub(crate) auth: AuthMethod,
pub(crate) readonly: bool,
pub(crate) packet_size: Option<u32>,
pub(crate) hostname_in_certificate: Option<String>,
pub(crate) client_name: Option<String>,
pub(crate) multi_subnet_failover: bool,
pub(crate) handshake_timeout: Option<Duration>,
pub(crate) command_timeout: Option<Duration>,
pub(crate) lossy_utf16_decoding: bool,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) client_cert: Option<ClientCertificate>,
}
#[derive(Clone)]
pub(crate) struct TrustConfig {
pub(crate) source: RootSource,
pub(crate) extra_cas: Vec<ExtraCa>,
pub(crate) bypass: bool,
}
impl Default for TrustConfig {
fn default() -> Self {
Self {
source: RootSource::Native,
extra_cas: Vec::new(),
bypass: false,
}
}
}
impl TrustConfig {
#[cfg(test)]
pub(crate) fn is_default(&self) -> bool {
matches!(self.source, RootSource::Native) && self.extra_cas.is_empty() && !self.bypass
}
}
impl std::fmt::Debug for TrustConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustConfig")
.field("source", &self.source)
.field("extra_cas", &self.extra_cas)
.field("bypass", &self.bypass)
.finish()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum RootSource {
#[default]
Native,
#[cfg(feature = "rustls-webpki-roots")]
WebpkiRoots,
}
#[derive(Clone)]
#[cfg_attr(
not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)),
allow(dead_code)
)]
pub(crate) enum ExtraCa {
File(PathBuf),
Bundle(Vec<u8>),
}
impl std::fmt::Debug for ExtraCa {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtraCa::File(path) => f.debug_tuple("File").field(path).finish(),
ExtraCa::Bundle(bytes) => f.debug_struct("Bundle").field("len", &bytes.len()).finish(),
}
}
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[derive(Clone, Debug)]
pub(crate) struct ClientCertificate {
pub(crate) source: ClientCertSource,
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[derive(Clone, Debug)]
pub(crate) enum ClientCertSource {
#[cfg_attr(not(any(feature = "rustls", feature = "native-tls")), allow(dead_code))]
CertAndKey { cert: PathBuf, key: PathBuf },
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
Pkcs12 {
path: PathBuf,
password: SecretString,
},
}
impl Default for Config {
fn default() -> Self {
Self {
host: None,
port: None,
database: None,
instance_name: None,
application_name: None,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
encryption: EncryptionLevel::Required,
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
encryption: EncryptionLevel::NotSupported,
trust: TrustConfig::default(),
auth: AuthMethod::None,
readonly: false,
packet_size: None,
hostname_in_certificate: None,
client_name: None,
multi_subnet_failover: false,
handshake_timeout: Some(DEFAULT_HANDSHAKE_TIMEOUT),
command_timeout: Some(DEFAULT_COMMAND_TIMEOUT),
lossy_utf16_decoding: false,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
client_cert: None,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> ConfigBuilder {
ConfigBuilder {
inner: Self::default(),
}
}
pub fn host(&mut self, host: impl ToString) {
self.host = Some(host.to_string());
}
pub fn port(&mut self, port: u16) {
self.port = Some(port);
}
pub fn database(&mut self, database: impl ToString) {
self.database = Some(database.to_string())
}
pub fn instance_name(&mut self, name: impl ToString) {
self.instance_name = Some(name.to_string());
}
pub fn application_name(&mut self, name: impl ToString) {
self.application_name = Some(name.to_string());
}
pub fn packet_size(&mut self, size: u32) {
self.packet_size = Some(size);
}
pub fn get_packet_size(&self) -> Option<u32> {
self.packet_size
}
pub fn encryption(&mut self, encryption: EncryptionLevel) {
self.encryption = encryption;
}
pub fn trust_cert(&mut self) {
if !self.trust.extra_cas.is_empty() || !matches!(self.trust.source, RootSource::Native) {
panic!(
"'trust_cert' and 'trust_cert_ca'/'trust_cert_ca_bundle'/'trust_webpki_roots' \
are mutual exclusive! Only use one."
)
}
self.trust.bypass = true;
}
pub fn trust_cert_ca(&mut self, path: impl ToString) {
if self.trust.bypass {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
}
self.trust
.extra_cas
.push(ExtraCa::File(PathBuf::from(path.to_string())));
}
pub fn trust_cert_ca_bundle(&mut self, bundle: impl Into<Vec<u8>>) {
if self.trust.bypass {
panic!("'trust_cert' and 'trust_cert_ca_bundle' are mutual exclusive! Only use one.")
}
self.trust.extra_cas.push(ExtraCa::Bundle(bundle.into()));
}
#[cfg(feature = "rustls-webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-webpki-roots")))]
pub fn trust_webpki_roots(&mut self) {
if self.trust.bypass {
panic!("'trust_cert' and 'trust_webpki_roots' are mutual exclusive! Only use one.")
}
self.trust.source = RootSource::WebpkiRoots;
}
pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
self.hostname_in_certificate = Some(hostname.to_string());
}
pub fn client_name(&mut self, name: impl ToString) {
self.client_name = Some(name.to_string());
}
pub fn authentication(&mut self, auth: AuthMethod) {
self.auth = auth;
}
pub fn readonly(&mut self, readonly: bool) {
self.readonly = readonly;
}
pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) {
self.multi_subnet_failover = multi_subnet_failover;
}
pub fn get_multi_subnet_failover(&self) -> bool {
self.multi_subnet_failover
}
pub fn handshake_timeout(&mut self, timeout: Option<Duration>) {
self.handshake_timeout = timeout;
}
pub fn get_handshake_timeout(&self) -> Option<Duration> {
self.handshake_timeout
}
pub fn command_timeout(&mut self, timeout: Option<Duration>) {
self.command_timeout = timeout;
}
pub fn get_command_timeout(&self) -> Option<Duration> {
self.command_timeout
}
pub fn lossy_utf16_decoding(&mut self, lossy: bool) {
self.lossy_utf16_decoding = lossy;
}
pub fn get_lossy_utf16_decoding(&self) -> bool {
self.lossy_utf16_decoding
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))
)]
pub fn client_certificate(&mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) {
self.client_cert = Some(ClientCertificate {
source: ClientCertSource::CertAndKey {
cert: cert.into(),
key: key.into(),
},
});
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
)]
pub fn client_certificate_pkcs12(
&mut self,
path: impl Into<PathBuf>,
password: impl Into<String>,
) {
self.client_cert = Some(ClientCertificate {
source: ClientCertSource::Pkcs12 {
path: path.into(),
password: crate::client::auth::secret_from_string(password.into()),
},
});
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) fn get_client_certificate(&self) -> Option<&ClientCertificate> {
self.client_cert.as_ref()
}
pub(crate) fn get_host(&self) -> &str {
self.host
.as_deref()
.filter(|v| v != &".")
.unwrap_or("localhost")
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) fn get_hostname_in_certificate(&self) -> &str {
self.hostname_in_certificate
.as_deref()
.unwrap_or_else(|| self.get_host())
}
pub(crate) fn get_port(&self) -> u16 {
match (self.port, self.instance_name.as_ref()) {
(Some(port), _) => port,
(None, Some(_)) => 1434,
(None, None) => 1433,
}
}
pub fn get_addr(&self) -> String {
format!("{}:{}", self.get_host(), self.get_port())
}
pub fn from_ado_string(s: &str) -> crate::Result<Self> {
let ado: AdoNetConfig = s.parse()?;
Self::from_config_string(ado)
}
pub fn from_jdbc_string(s: &str) -> crate::Result<Self> {
let jdbc: JdbcConfig = s.parse()?;
Self::from_config_string(jdbc)
}
fn from_config_string(s: impl ConfigString) -> crate::Result<Self> {
let mut builder = Self::new();
let server = s.server()?;
if let Some(host) = server.host {
builder.host(host);
}
if let Some(port) = server.port {
builder.port(port);
}
if let Some(instance) = server.instance {
builder.instance_name(instance);
}
builder.authentication(s.authentication()?);
if let Some(database) = s.database() {
builder.database(database);
}
if let Some(name) = s.application_name() {
builder.application_name(name);
}
let trust_cert = s.trust_cert()?;
let trust_cert_ca = s.trust_cert_ca();
if trust_cert && trust_cert_ca.is_some() {
return Err(crate::Error::Conversion(
"'TrustServerCertificate' and 'TrustServerCertificateCA' are \
mutually exclusive; specify only one"
.into(),
));
}
if trust_cert {
builder.trust_cert();
}
if let Some(ca) = trust_cert_ca {
builder.trust_cert_ca(ca);
}
if let Some(hostname_in_cert) = s.hostname_in_certificate() {
builder.hostname_in_certificate(hostname_in_cert);
}
builder.encryption(s.encrypt()?);
builder.readonly(s.readonly());
if let Some(client_name) = s.client_name() {
builder.client_name(client_name);
}
builder.multi_subnet_failover(s.multi_subnet_failover()?);
Ok(builder)
}
}
#[derive(Clone, Debug)]
pub struct ConfigBuilder {
inner: Config,
}
impl ConfigBuilder {
pub fn host(mut self, host: impl ToString) -> Self {
self.inner.host = Some(host.to_string());
self
}
pub fn port(mut self, port: u16) -> Self {
self.inner.port = Some(port);
self
}
pub fn database(mut self, database: impl ToString) -> Self {
self.inner.database = Some(database.to_string());
self
}
pub fn instance_name(mut self, name: impl ToString) -> Self {
self.inner.instance_name = Some(name.to_string());
self
}
pub fn application_name(mut self, name: impl ToString) -> Self {
self.inner.application_name = Some(name.to_string());
self
}
pub fn encryption(mut self, encryption: EncryptionLevel) -> Self {
self.inner.encryption = encryption;
self
}
pub fn trust_cert(mut self) -> Self {
self.inner.trust_cert();
self
}
pub fn trust_cert_ca(mut self, path: impl ToString) -> Self {
self.inner.trust_cert_ca(path);
self
}
pub fn trust_cert_ca_bundle(mut self, bundle: impl Into<Vec<u8>>) -> Self {
self.inner.trust_cert_ca_bundle(bundle);
self
}
#[cfg(feature = "rustls-webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-webpki-roots")))]
pub fn trust_webpki_roots(mut self) -> Self {
self.inner.trust_webpki_roots();
self
}
pub fn authentication(mut self, auth: AuthMethod) -> Self {
self.inner.auth = auth;
self
}
pub fn readonly(mut self, readonly: bool) -> Self {
self.inner.readonly = readonly;
self
}
pub fn handshake_timeout(mut self, timeout: Option<Duration>) -> Self {
self.inner.handshake_timeout = timeout;
self
}
pub fn command_timeout(mut self, timeout: Option<Duration>) -> Self {
self.inner.command_timeout = timeout;
self
}
pub fn lossy_utf16_decoding(mut self, lossy: bool) -> Self {
self.inner.lossy_utf16_decoding = lossy;
self
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))
)]
pub fn client_certificate(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
self.inner.client_certificate(cert, key);
self
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
)]
pub fn client_certificate_pkcs12(
mut self,
path: impl Into<PathBuf>,
password: impl Into<String>,
) -> Self {
self.inner.client_certificate_pkcs12(path, password);
self
}
pub fn build(self) -> Config {
self.inner
}
}
impl From<Config> for ConfigBuilder {
fn from(config: Config) -> Self {
ConfigBuilder { inner: config }
}
}
impl From<ConfigBuilder> for Config {
fn from(builder: ConfigBuilder) -> Self {
builder.inner
}
}
pub(crate) struct ServerDefinition {
host: Option<String>,
port: Option<u16>,
instance: Option<String>,
}
fn connection_string_error(
err: connection_string::Error,
quoting: &str,
docs: &str,
) -> crate::Error {
let err = err.to_string();
let detail = err.strip_prefix("Conversion error: ").unwrap_or(&err);
hinted_conversion_error(detail, quoting, docs)
}
fn hinted_conversion_error(detail: &str, quoting: &str, docs: &str) -> crate::Error {
crate::Error::Conversion(
format!(
"{detail}. hint: if a value such as a password contains a special \
character (for example `;`, `=` or `{{`), it must be quoted — \
{quoting}. Do not URL-encode the value (`%24` is sent literally, \
not decoded to `$`). Non-ASCII values are not accepted by the \
connection-string parser; build the `Config` programmatically \
(see `{docs}`) to pass any value without escaping."
)
.into(),
)
}
pub(crate) trait ConfigString {
fn dict(&self) -> &HashMap<String, String>;
fn server(&self) -> crate::Result<ServerDefinition>;
fn authentication(&self) -> crate::Result<AuthMethod> {
let user = self
.dict()
.get("uid")
.or_else(|| self.dict().get("username"))
.or_else(|| self.dict().get("user"))
.or_else(|| self.dict().get("user id"))
.map(|s| s.as_str());
let pw = self
.dict()
.get("password")
.or_else(|| self.dict().get("pwd"))
.map(|s| s.as_str());
match self
.dict()
.get("integratedsecurity")
.or_else(|| self.dict().get("integrated security"))
{
#[cfg(all(windows, feature = "winauth"))]
Some(val) if Self::is_sspi_or_truthy(val)? => match (user, pw) {
(None, None) => Ok(AuthMethod::Integrated),
_ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
},
#[cfg(all(unix, feature = "sspi-rs"))]
Some(val) if Self::is_sspi_or_truthy(val)? => match (user, pw) {
(Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)),
#[cfg(feature = "integrated-auth-gssapi")]
(None, None) => Ok(AuthMethod::Integrated),
_ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
},
#[cfg(all(
feature = "integrated-auth-gssapi",
not(all(unix, feature = "sspi-rs"))
))]
Some(val) if Self::is_sspi_or_truthy(val)? => Ok(AuthMethod::Integrated),
_ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))),
}
}
fn database(&self) -> Option<String> {
self.dict()
.get("database")
.or_else(|| self.dict().get("initial catalog"))
.or_else(|| self.dict().get("databasename"))
.map(|db| db.to_string())
}
fn application_name(&self) -> Option<String> {
self.dict()
.get("application name")
.or_else(|| self.dict().get("applicationname"))
.map(|name| name.to_string())
}
fn trust_cert(&self) -> crate::Result<bool> {
self.dict()
.get("trustservercertificate")
.map(Self::parse_bool)
.unwrap_or(Ok(false))
}
fn trust_cert_ca(&self) -> Option<String> {
self.dict()
.get("trustservercertificateca")
.map(|ca| ca.to_string())
}
fn hostname_in_certificate(&self) -> Option<String> {
self.dict()
.get("hostnameincertificate")
.or_else(|| self.dict().get("hostname in certificate"))
.map(|host| host.to_string())
}
fn client_name(&self) -> Option<String> {
self.dict()
.get("workstationid")
.or_else(|| self.dict().get("workstation id"))
.map(|name| name.to_string())
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
fn encrypt(&self) -> crate::Result<EncryptionLevel> {
self.dict()
.get("encrypt")
.map(|val| match Self::parse_bool(val) {
Ok(true) => Ok(EncryptionLevel::Required),
Ok(false) => Ok(EncryptionLevel::Off),
Err(_) if val == "DANGER_PLAINTEXT" => Ok(EncryptionLevel::NotSupported),
Err(_) if val.eq_ignore_ascii_case("strict") && cfg!(feature = "tds80") => {
Ok(EncryptionLevel::Strict)
}
Err(_) if val.eq_ignore_ascii_case("strict") => Err(crate::Error::Conversion(
"encrypt=strict requires the crate's `tds80` feature to be enabled".into(),
)),
Err(e) => Err(e),
})
.unwrap_or(Ok(EncryptionLevel::Required))
}
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
fn encrypt(&self) -> crate::Result<EncryptionLevel> {
let Some(val) = self.dict().get("encrypt") else {
return Ok(EncryptionLevel::NotSupported);
};
match Self::parse_bool(val) {
Ok(false) => Ok(EncryptionLevel::NotSupported),
Err(_) if val == "DANGER_PLAINTEXT" => Ok(EncryptionLevel::NotSupported),
Ok(true) => Err(Self::tls_backend_missing()),
Err(_) if val.eq_ignore_ascii_case("strict") => Err(Self::tls_backend_missing()),
Err(e) => Err(e),
}
}
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
fn tls_backend_missing() -> crate::Error {
crate::Error::Tls(
"encryption was requested (`encrypt=...`) but the crate was compiled without a TLS \
backend; enable one of the `native-tls`, `rustls` or `vendored-openssl` features."
.to_string(),
)
}
fn parse_bool<T: AsRef<str>>(v: T) -> crate::Result<bool> {
match v.as_ref().trim().to_lowercase().as_str() {
"true" | "yes" => Ok(true),
"false" | "no" => Ok(false),
_ => Err(crate::Error::Conversion(
"Connection string: Not a valid boolean".into(),
)),
}
}
#[cfg(any(
all(windows, feature = "winauth"),
all(unix, feature = "sspi-rs"),
feature = "integrated-auth-gssapi"
))]
fn is_sspi_or_truthy<T: AsRef<str>>(v: T) -> crate::Result<bool> {
let v = v.as_ref();
Ok(v.eq_ignore_ascii_case("sspi") || Self::parse_bool(v)?)
}
fn readonly(&self) -> bool {
self.dict()
.get("applicationintent")
.filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
.is_some()
}
fn multi_subnet_failover(&self) -> crate::Result<bool> {
self.dict()
.get("multisubnetfailover")
.map(Self::parse_bool)
.unwrap_or(Ok(false))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
use secrecy::ExposeSecret;
#[test]
fn config_debug_redacts_connection_password() {
let config = Config::builder()
.authentication(AuthMethod::sql_server("SA", "conn-str-secret"))
.build();
let dbg = format!("{config:?}");
assert!(
!dbg.contains("conn-str-secret"),
"connection password leaked in Config Debug: {dbg}"
);
assert!(dbg.contains("REDACTED"), "password not redacted: {dbg}");
}
#[test]
fn config_builder_constructs_config() {
let config = Config::builder()
.host("db.example.com")
.port(4433)
.database("northwind")
.application_name("my-app")
.authentication(AuthMethod::sql_server("SA", "secret"))
.readonly(true)
.build();
assert_eq!("db.example.com", config.get_host());
assert_eq!(4433, config.get_port());
assert_eq!("db.example.com:4433", config.get_addr());
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("my-app"), config.application_name.as_deref());
assert!(config.readonly);
assert!(matches!(config.auth, AuthMethod::SqlServer(_)));
assert!(config.trust.is_default());
}
#[test]
fn config_builder_roundtrips_via_from() {
let config = Config::builder().host("localhost").port(1433).build();
let builder: ConfigBuilder = config.into();
let config = builder.database("master").build();
assert_eq!("localhost:1433", config.get_addr());
assert_eq!(Some("master"), config.database.as_deref());
}
#[test]
fn config_from_builder_carries_builder_settings() {
let config: Config = Config::builder().host("db.internal").port(2020).into();
assert_eq!("db.internal", config.get_host());
assert_eq!(2020, config.get_port());
}
#[test]
fn handshake_timeout_defaults_to_fifteen_seconds() {
let config = Config::new();
assert_eq!(
config.get_handshake_timeout(),
Some(Duration::from_secs(15))
);
}
#[test]
fn handshake_timeout_setter_roundtrips() {
let cases = [
Some(Duration::from_millis(1)),
Some(Duration::from_secs(30)),
None,
];
for want in cases {
let mut config = Config::new();
config.handshake_timeout(want);
assert_eq!(config.get_handshake_timeout(), want, "for {want:?}");
}
}
#[test]
fn config_builder_sets_handshake_timeout() {
let config = Config::builder()
.handshake_timeout(Some(Duration::from_secs(3)))
.build();
assert_eq!(config.get_handshake_timeout(), Some(Duration::from_secs(3)));
let config = Config::builder().handshake_timeout(None).build();
assert_eq!(config.get_handshake_timeout(), None);
}
#[test]
fn config_builder_defaults_handshake_timeout() {
let config = Config::builder().build();
assert_eq!(
config.get_handshake_timeout(),
Some(Duration::from_secs(15))
);
}
#[test]
fn command_timeout_defaults_to_thirty_seconds() {
let config = Config::new();
assert_eq!(config.get_command_timeout(), Some(Duration::from_secs(30)));
}
#[test]
fn command_timeout_setter_roundtrips() {
let cases = [
Some(Duration::from_millis(1)),
Some(Duration::from_secs(60)),
None,
];
for want in cases {
let mut config = Config::new();
config.command_timeout(want);
assert_eq!(config.get_command_timeout(), want, "for {want:?}");
}
}
#[test]
fn config_builder_sets_command_timeout() {
let config = Config::builder()
.command_timeout(Some(Duration::from_secs(7)))
.build();
assert_eq!(config.get_command_timeout(), Some(Duration::from_secs(7)));
let config = Config::builder().command_timeout(None).build();
assert_eq!(config.get_command_timeout(), None);
}
#[test]
fn config_builder_defaults_command_timeout() {
let config = Config::builder().build();
assert_eq!(config.get_command_timeout(), Some(Duration::from_secs(30)));
}
#[test]
fn lossy_utf16_decoding_defaults_to_false() {
let config = Config::new();
assert!(!config.get_lossy_utf16_decoding());
}
#[test]
fn lossy_utf16_decoding_setter_roundtrips() {
let mut config = Config::new();
config.lossy_utf16_decoding(true);
assert!(config.get_lossy_utf16_decoding());
config.lossy_utf16_decoding(false);
assert!(!config.get_lossy_utf16_decoding());
}
#[test]
fn config_builder_sets_lossy_utf16_decoding() {
let config = Config::builder().lossy_utf16_decoding(true).build();
assert!(config.get_lossy_utf16_decoding());
}
#[test]
fn config_builder_defaults_lossy_utf16_decoding() {
let config = Config::builder().build();
assert!(!config.get_lossy_utf16_decoding());
}
#[test]
fn timeouts_are_independent() {
let mut config = Config::new();
config.handshake_timeout(Some(Duration::from_secs(1)));
config.command_timeout(Some(Duration::from_secs(2)));
assert_eq!(config.get_handshake_timeout(), Some(Duration::from_secs(1)));
assert_eq!(config.get_command_timeout(), Some(Duration::from_secs(2)));
}
#[test]
fn get_packet_size_reflects_the_set_value() {
let mut config = Config::new();
assert_eq!(config.get_packet_size(), None);
config.packet_size(8192);
assert_eq!(config.get_packet_size(), Some(8192));
}
#[test]
fn from_jdbc_string_parses_host_and_port() {
let config =
Config::from_jdbc_string("jdbc:sqlserver://db.example.com:2345").expect("valid jdbc");
assert_eq!("db.example.com", config.get_host());
assert_eq!(2345, config.get_port());
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn get_hostname_in_certificate_falls_back_to_host() {
let mut config = Config::new();
config.host("real.host");
assert_eq!(config.get_hostname_in_certificate(), "real.host");
config.hostname_in_certificate("cert.host");
assert_eq!(config.get_hostname_in_certificate(), "cert.host");
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn client_certificate_sets_cert_and_key_source() {
let mut config = Config::new();
assert!(config.get_client_certificate().is_none());
config.client_certificate("/tmp/client.pem", "/tmp/client.key");
let cert = config
.get_client_certificate()
.expect("client certificate should be set");
match &cert.source {
ClientCertSource::CertAndKey { cert, key } => {
assert_eq!(cert, &PathBuf::from("/tmp/client.pem"));
assert_eq!(key, &PathBuf::from("/tmp/client.key"));
}
#[allow(unreachable_patterns)]
other => panic!("expected CertAndKey source, got {other:?}"),
}
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn config_builder_sets_client_certificate() {
let config = Config::builder()
.host("localhost")
.client_certificate("cert.der", "key.der")
.build();
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::CertAndKey { cert, key } => {
assert_eq!(cert, &PathBuf::from("cert.der"));
assert_eq!(key, &PathBuf::from("key.der"));
}
#[allow(unreachable_patterns)]
other => panic!("expected CertAndKey source, got {other:?}"),
}
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn client_certificate_pkcs12_sets_bundle_source() {
let mut config = Config::new();
config.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t");
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::Pkcs12 { path, password } => {
assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
assert_eq!(password.expose_secret(), "s3cr3t");
}
other => panic!("expected Pkcs12 source, got {other:?}"),
}
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn client_certificate_debug_redacts_pkcs12_password() {
let mut config = Config::new();
config.client_certificate_pkcs12("/tmp/identity.pfx", "topsecret");
let dbg = format!("{:?}", config.get_client_certificate().unwrap());
assert!(dbg.contains("REDACTED"), "password not redacted: {dbg}");
assert!(!dbg.contains("topsecret"), "password leaked: {dbg}");
}
#[cfg(all(unix, feature = "sspi-rs"))]
#[test]
fn ado_integrated_security_sspi_with_credentials_uses_windows_ntlm() {
let config = Config::from_ado_string(
"server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=DOMAIN\\user;pwd=secret",
)
.unwrap();
match config.auth {
AuthMethod::Windows(auth) => {
assert_eq!("user", auth.user);
assert_eq!(Some("DOMAIN"), auth.domain.as_deref());
}
other => panic!("expected Windows NTLM auth, got {other:?}"),
}
}
#[test]
fn config_direct_setters_populate_fields() {
let mut config = Config::new();
config.database("northwind");
config.instance_name("SQLEXPRESS");
config.client_name("workstation-7");
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert_eq!(Some("workstation-7"), config.client_name.as_deref());
}
#[test]
fn get_port_defaults_without_port_or_instance() {
let config = Config::new();
assert_eq!(1433, config.get_port());
}
#[test]
fn get_port_uses_sql_browser_port_for_named_instance() {
let mut config = Config::new();
config.instance_name("SQLEXPRESS");
assert_eq!(1434, config.get_port());
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_after_trust_cert_ca_panics() {
let mut config = Config::new();
config.trust_cert_ca("/tmp/ca.crt");
config.trust_cert();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_ca_after_trust_cert_panics() {
let mut config = Config::new();
config.trust_cert();
config.trust_cert_ca("/tmp/ca.crt");
}
#[test]
fn trust_cert_ca_sets_ca_location() {
let mut config = Config::new();
config.trust_cert_ca("/tmp/ca.crt");
assert!(matches!(
config.trust.extra_cas.as_slice(),
[ExtraCa::File(_)]
));
assert!(!config.trust.bypass);
assert!(matches!(config.trust.source, RootSource::Native));
}
#[test]
fn trust_cert_ca_accumulates_across_calls() {
let mut config = Config::new();
config.trust_cert_ca("/tmp/a.crt");
config.trust_cert_ca("/tmp/b.crt");
config.trust_cert_ca_bundle(b"----- not really a cert -----".to_vec());
assert_eq!(config.trust.extra_cas.len(), 3);
match &config.trust.extra_cas[0] {
ExtraCa::File(p) => assert_eq!(p, &PathBuf::from("/tmp/a.crt")),
other => panic!("expected File, got {other:?}"),
}
match &config.trust.extra_cas[1] {
ExtraCa::File(p) => assert_eq!(p, &PathBuf::from("/tmp/b.crt")),
other => panic!("expected File, got {other:?}"),
}
assert!(matches!(config.trust.extra_cas[2], ExtraCa::Bundle(_)));
}
#[test]
fn trust_cert_ca_bundle_pushes_bundle() {
let mut config = Config::new();
config.trust_cert_ca_bundle(vec![1u8, 2, 3]);
match config.trust.extra_cas.as_slice() {
[ExtraCa::Bundle(bytes)] => assert_eq!(bytes, &[1, 2, 3]),
other => panic!("expected a single Bundle, got {other:?}"),
}
}
#[test]
fn trust_config_debug_redacts_bundle_bytes() {
let mut config = Config::new();
config.trust_cert_ca_bundle(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let dbg = format!("{:?}", config.trust);
assert!(dbg.contains("Bundle { len: 4 }"), "got: {dbg}");
assert!(
!dbg.contains("222") && !dbg.contains("0xde"),
"bytes leaked: {dbg}"
);
}
#[cfg(feature = "rustls-webpki-roots")]
#[test]
fn trust_webpki_roots_sets_source_and_is_mutually_exclusive_with_extras() {
let mut config = Config::new();
config.trust_webpki_roots();
assert!(matches!(config.trust.source, RootSource::WebpkiRoots));
config.trust_cert_ca("/tmp/extra.crt");
assert_eq!(config.trust.extra_cas.len(), 1);
}
#[cfg(feature = "rustls-webpki-roots")]
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_after_trust_webpki_roots_panics() {
let mut config = Config::new();
config.trust_webpki_roots();
config.trust_cert();
}
#[cfg(feature = "rustls-webpki-roots")]
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_webpki_roots_after_trust_cert_panics() {
let mut config = Config::new();
config.trust_cert();
config.trust_webpki_roots();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_after_trust_cert_ca_bundle_panics() {
let mut config = Config::new();
config.trust_cert_ca_bundle(vec![1, 2, 3]);
config.trust_cert();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_ca_bundle_after_trust_cert_panics() {
let mut config = Config::new();
config.trust_cert();
config.trust_cert_ca_bundle(vec![1, 2, 3]);
}
#[test]
fn trust_cert_sets_bypass() {
let mut config = Config::new();
assert!(config.trust.is_default());
config.trust_cert();
assert!(config.trust.bypass);
}
#[test]
fn config_builder_covers_all_setters() {
let config = Config::builder()
.host("localhost")
.instance_name("SQLEXPRESS")
.encryption(EncryptionLevel::Off)
.trust_cert_ca("/tmp/ca.crt")
.build();
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert!(matches!(config.encryption, EncryptionLevel::Off));
assert!(matches!(
config.trust.extra_cas.as_slice(),
[ExtraCa::File(_)]
));
}
#[test]
fn config_builder_trust_cert_sets_bypass() {
let config = Config::builder().trust_cert().build();
assert!(config.trust.bypass);
}
#[test]
fn config_builder_trust_cert_ca_bundle_accumulates() {
let config = Config::builder()
.trust_cert_ca("/tmp/a.crt")
.trust_cert_ca_bundle(vec![1, 2, 3])
.build();
assert_eq!(config.trust.extra_cas.len(), 2);
}
#[cfg(feature = "rustls-webpki-roots")]
#[test]
fn config_builder_trust_webpki_roots_sets_source() {
let config = Config::builder().trust_webpki_roots().build();
assert!(matches!(config.trust.source, RootSource::WebpkiRoots));
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn config_builder_trust_cert_after_ca_panics() {
Config::builder().trust_cert_ca("/tmp/ca.crt").trust_cert();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn config_builder_trust_cert_ca_after_trust_cert_panics() {
Config::builder().trust_cert().trust_cert_ca("/tmp/ca.crt");
}
#[test]
fn from_ado_string_populates_optional_fields() {
let config = Config::from_ado_string(
"server=tcp:my-server.com\\SQLEXPRESS;database=northwind;\
HostNameInCertificate=cert.host;WorkstationID=ws-1",
)
.expect("valid ado string");
assert_eq!("my-server.com", config.get_host());
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("cert.host"), config.hostname_in_certificate.as_deref());
assert_eq!(Some("ws-1"), config.client_name.as_deref());
}
#[test]
fn from_ado_string_trust_cert_ca_populates_extra_cas() {
let config = Config::from_ado_string(
"server=tcp:localhost,1433;TrustServerCertificateCA=/tmp/ca.crt",
)
.expect("valid ado string");
match config.trust.extra_cas.as_slice() {
[ExtraCa::File(p)] => assert_eq!(p, &PathBuf::from("/tmp/ca.crt")),
other => panic!("expected a single File CA, got {other:?}"),
}
assert!(!config.trust.bypass);
}
#[test]
fn from_ado_string_trust_cert_populates_bypass() {
let config =
Config::from_ado_string("server=tcp:localhost,1433;TrustServerCertificate=true")
.expect("valid ado string");
assert!(config.trust.bypass);
}
#[test]
fn from_ado_string_trust_cert_and_ca_conflict_errors() {
let err = Config::from_ado_string(
"server=tcp:localhost,1433;TrustServerCertificate=true;\
TrustServerCertificateCA=/tmp/ca.crt",
)
.expect_err("conflicting trust settings must error");
match err {
crate::Error::Conversion(msg) => {
assert!(msg.contains("mutually exclusive"), "got: {msg}")
}
other => panic!("expected Conversion error, got {other:?}"),
}
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn client_cert_source_debug_formats_cert_and_key() {
let mut config = Config::new();
config.client_certificate("/tmp/client.pem", "/tmp/client.key");
let dbg = format!("{:?}", config.get_client_certificate().unwrap().source);
assert!(dbg.contains("CertAndKey"));
assert!(dbg.contains("client.pem"));
assert!(dbg.contains("client.key"));
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn config_builder_sets_pkcs12_client_certificate() {
let config = Config::builder()
.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t")
.build();
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::Pkcs12 { path, password } => {
assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
assert_eq!(password.expose_secret(), "s3cr3t");
}
other => panic!("expected Pkcs12 source, got {other:?}"),
}
}
#[cfg(all(unix, feature = "sspi-rs"))]
#[test]
fn ado_integrated_security_sspi_with_partial_credentials_uses_windows() {
let config = Config::from_ado_string(
"server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=onlyuser",
)
.unwrap();
match config.auth {
AuthMethod::Windows(auth) => {
assert_eq!("onlyuser", auth.user);
}
other => panic!("expected Windows auth, got {other:?}"),
}
}
}