From 4390e3195f3a973c3730d69bf3cad444caafc2f3 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:29:30 -0700 Subject: [PATCH 01/42] feat: TDS 8.0 strict encryption, hostname_in_certificate, client_name (#412, #340, #414, #224) Adapted from upstream #413 (author @olback) onto the #419 rustls-0.23 stack: - EncryptionLevel::Strict (TDS 8.0): TLS handshake before prelogin, ALPN 'tds/8.0' advertised on all three TLS backends. New tds80 feature (in default), with compile_error if enabled without a TLS backend. - Config::hostname_in_certificate(): validate the server cert against a specified name instead of the host (#340). - Config::client_name() + default login hostname (workstation id) from the local machine name (#414). - Connection-string parsing for HostNameInCertificate / WorkstationID and encrypt=strict. - Deps: async-native-tls 0.4->0.5 (request_alpns), libc (unix hostname). 142 lib tests pass; clippy --features=all -D warnings clean. --- Cargo.toml | 9 +- src/client/config.rs | 66 ++++++++++++- src/client/config/ado_net.rs | 47 +++++++++ src/client/connection.rs | 103 ++++++++++++++------ src/client/tls_stream.rs | 8 ++ src/client/tls_stream/native_tls_stream.rs | 8 +- src/client/tls_stream/opentls_tls_stream.rs | 11 ++- src/client/tls_stream/rustls_tls_stream.rs | 15 ++- src/lib.rs | 10 ++ src/tds.rs | 31 ++++++ src/tds/codec/login.rs | 57 +++++++++++ src/tds/codec/pre_login.rs | 5 +- 12 files changed, 334 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 173d75522..466668c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,9 +58,10 @@ winauth = { version = "0.0.4", optional = true } [target.'cfg(unix)'.dependencies] libgssapi = { version = "0.8.1", optional = true, default-features = false } +libc = "0.2" [dependencies.async-native-tls] -version = "0.4" +version = "0.5" features = ["runtime-async-std"] optional = true @@ -184,6 +185,7 @@ all = [ "chrono", "time", "tds73", + "tds80", "sql-browser-async-std", "sql-browser-tokio", "sql-browser-smol", @@ -192,8 +194,11 @@ all = [ "bigdecimal", "native-tls", ] -default = ["tds73", "winauth", "native-tls"] +default = ["tds80", "winauth", "native-tls"] tds73 = [] +# Enables TDS 8.0 support, including the `Strict` encryption level (TLS before +# the TDS prelogin, TDS 8.0 "strict" mode). Requires a TLS backend. +tds80 = ["tds73"] docs = [] sql-browser-async-std = ["async-std"] sql-browser-tokio = ["tokio", "tokio-util"] diff --git a/src/client/config.rs b/src/client/config.rs index 3d6994f0f..4d286094b 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -33,6 +33,8 @@ pub struct Config { pub(crate) auth: AuthMethod, pub(crate) readonly: bool, pub(crate) packet_size: Option, + pub(crate) hostname_in_certificate: Option, + pub(crate) client_name: Option, } #[derive(Clone, Debug)] @@ -67,6 +69,8 @@ impl Default for Config { auth: AuthMethod::None, readonly: false, packet_size: None, + hostname_in_certificate: None, + client_name: None, } } } @@ -175,6 +179,28 @@ impl Config { } } + /// Sets the hostname that the server certificate is validated against, + /// instead of the value given to [`host`]. + /// + /// This is useful when connecting through an IP address, a tunnel, or a + /// load balancer whose certificate carries a different subject/SAN than the + /// address used to reach it (see issue #340). + /// + /// - Defaults to the value of [`host`]. + /// + /// [`host`]: Config::host + pub fn hostname_in_certificate(&mut self, hostname: impl ToString) { + self.hostname_in_certificate = Some(hostname.to_string()); + } + + /// Sets the client / workstation name reported to the server in the login + /// record (queryable with `HOST_NAME()`). + /// + /// - Defaults to the local workstation id (the machine hostname). + pub fn client_name(&mut self, name: impl ToString) { + self.client_name = Some(name.to_string()); + } + /// Sets the authentication method. /// /// - Defaults to `None`. @@ -196,6 +222,17 @@ impl Config { .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()) { // A user-defined port, we must use that. @@ -228,8 +265,10 @@ impl Config { /// |`database`|``|The name of the database.| /// |`TrustServerCertificate`|`true`,`false`,`yes`,`no`|Specifies whether the driver trusts the server certificate when connecting using TLS. Cannot be used toghether with `TrustServerCertificateCA`| /// |`TrustServerCertificateCA`|``|Path to a `pem`, `crt` or `der` certificate file. Cannot be used together with `TrustServerCertificate`| - /// |`encrypt`|`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication.| + /// |`encrypt`|`strict`,`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication. `strict` (TDS 8.0) requires the `tds80` feature.| /// |`Application Name`, `ApplicationName`|``|Sets the application name for the connection.| + /// |`HostNameInCertificate`, `HostName In Certificate`|``|The hostname the server certificate is validated against. Defaults to `server`.| + /// |`WorkstationID`, `Workstation ID`|``|The client / workstation name reported to the server.| /// /// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings pub fn from_ado_string(s: &str) -> crate::Result { @@ -283,10 +322,18 @@ impl Config { 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); + } + Ok(builder) } } @@ -364,6 +411,20 @@ pub(crate) trait ConfigString { .map(|ca| ca.to_string()) } + fn hostname_in_certificate(&self) -> Option { + self.dict() + .get("hostnameincertificate") + .or_else(|| self.dict().get("hostname in certificate")) + .map(|host| host.to_string()) + } + + fn client_name(&self) -> Option { + self.dict() + .get("workstationid") + .or_else(|| self.dict().get("workstation id")) + .map(|name| name.to_string()) + } + #[cfg(any( feature = "rustls", feature = "native-tls", @@ -376,6 +437,9 @@ pub(crate) trait ConfigString { 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(e) => Err(e), }) .unwrap_or(Ok(EncryptionLevel::Off)) diff --git a/src/client/config/ado_net.rs b/src/client/config/ado_net.rs index 018f92da7..b452bd416 100644 --- a/src/client/config/ado_net.rs +++ b/src/client/config/ado_net.rs @@ -470,6 +470,53 @@ mod tests { Ok(()) } + #[test] + #[cfg(feature = "tds80")] + fn encryption_parsing_strict() -> crate::Result<()> { + let test_str = "encrypt=strict"; + let ado: AdoNetConfig = test_str.parse()?; + + assert_eq!(EncryptionLevel::Strict, ado.encrypt()?); + + Ok(()) + } + + #[test] + fn client_name_parsing() -> crate::Result<()> { + let test_str = "workstationid=meow"; + let ado: AdoNetConfig = test_str.parse()?; + + assert_eq!(Some("meow".into()), ado.client_name()); + + let test_str = "Workstation ID=meow"; + let ado: AdoNetConfig = test_str.parse()?; + + assert_eq!(Some("meow".into()), ado.client_name()); + + Ok(()) + } + + #[test] + fn hostname_in_certificate_parsing() -> crate::Result<()> { + let test_str = "HostNameInCertificate=foo.example.com"; + let ado: AdoNetConfig = test_str.parse()?; + + assert_eq!( + Some("foo.example.com".into()), + ado.hostname_in_certificate() + ); + + let test_str = "HostName In Certificate=foo.example.com"; + let ado: AdoNetConfig = test_str.parse()?; + + assert_eq!( + Some("foo.example.com".into()), + ado.hostname_in_certificate() + ); + + Ok(()) + } + #[test] fn application_name_parsing() -> crate::Result<()> { let test_str = "Application Name=meow"; diff --git a/src/client/connection.rs b/src/client/connection.rs index 14ce262ae..bd0cbc015 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -80,6 +80,34 @@ impl Connection { context }; + // In TDS 8.0 "strict" mode the TLS handshake happens *before* the + // prelogin, so we wrap the stream in TLS up front. In every other mode + // the connection starts in the clear and TLS (if any) is negotiated + // during the prelogin. + #[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" + ))] + let transport = match config.encryption { + EncryptionLevel::Strict => { + event!(Level::DEBUG, "Performing a TLS handshake (TDS 8.0 strict)"); + let mut pre_login_stream = TlsPreloginWrapper::new(tcp_stream); + // No prelogin framing is used for the strict handshake; pass the + // raw TLS bytes straight through. + pre_login_stream.handshake_complete(); + let stream = create_tls_stream(&config, pre_login_stream).await?; + event!(Level::DEBUG, "TLS handshake successful"); + Framed::new(MaybeTlsStream::Tls(stream), PacketCodec) + } + _ => Framed::new(MaybeTlsStream::Raw(tcp_stream), PacketCodec), + }; + + #[cfg(not(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" + )))] let transport = Framed::new(MaybeTlsStream::Raw(tcp_stream), PacketCodec); let mut connection = Self { @@ -106,6 +134,7 @@ impl Connection { config.database, config.host, config.application_name, + config.client_name, config.readonly, config.packet_size, prelogin, @@ -333,6 +362,7 @@ impl Connection { db: Option, server_name: Option, application_name: Option, + client_name: Option, readonly: bool, packet_size: Option, prelogin: PreloginMessage, @@ -351,6 +381,10 @@ impl Connection { login_message.app_name(app_name); } + if let Some(client_name) = client_name { + login_message.hostname(client_name); + } + login_message.readonly(readonly); if let Some(size) = packet_size { @@ -495,37 +529,50 @@ impl Connection { config: &Config, encryption: EncryptionLevel, ) -> crate::Result { - if encryption != EncryptionLevel::NotSupported { - event!(Level::DEBUG, "Performing a TLS handshake"); - - let Self { - transport, context, .. - } = self; - let mut stream = match transport.into_inner() { - MaybeTlsStream::Raw(tcp) => { - create_tls_stream(config, TlsPreloginWrapper::new(tcp)).await? - } - _ => unreachable!(), - }; + match encryption { + EncryptionLevel::NotSupported => { + event!( + Level::WARN, + "TLS encryption is not enabled. All traffic including the login credentials are not encrypted." + ); - stream.get_mut().handshake_complete(); - event!(Level::DEBUG, "TLS handshake successful"); + Ok(self) + } + // In strict mode the handshake already happened before the prelogin, + // so the transport is already a TLS stream. Nothing to do here. + EncryptionLevel::Strict => { + event!( + Level::TRACE, + "Already in a TLS stream (TDS 8.0 strict), skipping handshake." + ); - let transport = Framed::new(MaybeTlsStream::Tls(stream), PacketCodec); + Ok(self) + } + EncryptionLevel::Off | EncryptionLevel::On | EncryptionLevel::Required => { + event!(Level::DEBUG, "Performing a TLS handshake"); + + let Self { + transport, context, .. + } = self; + let mut stream = match transport.into_inner() { + MaybeTlsStream::Raw(tcp) => { + create_tls_stream(config, TlsPreloginWrapper::new(tcp)).await? + } + _ => unreachable!(), + }; - Ok(Self { - transport, - context, - flushed: false, - buf: BytesMut::new(), - }) - } else { - event!( - Level::WARN, - "TLS encryption is not enabled. All traffic including the login credentials are not encrypted." - ); + stream.get_mut().handshake_complete(); + event!(Level::DEBUG, "TLS handshake successful"); - Ok(self) + let transport = Framed::new(MaybeTlsStream::Tls(stream), PacketCodec); + + Ok(Self { + transport, + context, + flushed: false, + buf: BytesMut::new(), + }) + } } } @@ -560,7 +607,7 @@ impl Connection { feature = "vendored-openssl" )))] fn check_tls_backend_available(encryption: EncryptionLevel) -> crate::Result<()> { - if let EncryptionLevel::On | EncryptionLevel::Required = encryption { + if let EncryptionLevel::On | EncryptionLevel::Required | EncryptionLevel::Strict = encryption { return Err(crate::Error::Tls( "TLS encryption was requested but the crate was compiled without a TLS backend. \ Enable one of the `native-tls`, `rustls` or `vendored-openssl` features." diff --git a/src/client/tls_stream.rs b/src/client/tls_stream.rs index 9eba1060f..007b3c1d1 100644 --- a/src/client/tls_stream.rs +++ b/src/client/tls_stream.rs @@ -1,6 +1,14 @@ use crate::Config; use futures_util::io::{AsyncRead, AsyncWrite}; +/// ALPN protocol name advertised for TDS 8.0 ("strict") encryption. +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] +pub(crate) const TDS_ALPN_PROTOCOL_NAME: &str = "tds/8.0"; + #[cfg(feature = "native-tls")] mod native_tls_stream; diff --git a/src/client/tls_stream/native_tls_stream.rs b/src/client/tls_stream/native_tls_stream.rs index 73cd10595..8ae24a27f 100644 --- a/src/client/tls_stream/native_tls_stream.rs +++ b/src/client/tls_stream/native_tls_stream.rs @@ -14,6 +14,10 @@ pub(crate) async fn create_tls_stream( ) -> crate::Result> { let mut builder = TlsConnector::new(); + if matches!(config.encryption, crate::EncryptionLevel::Strict) { + builder = builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]); + } + match &config.trust { TrustConfig::CaCertificateLocation(path) => { if let Ok(buf) = fs::read(path) { @@ -56,5 +60,7 @@ pub(crate) async fn create_tls_stream( } } - Ok(builder.connect(config.get_host(), stream).await?) + Ok(builder + .connect(config.get_hostname_in_certificate(), stream) + .await?) } diff --git a/src/client/tls_stream/opentls_tls_stream.rs b/src/client/tls_stream/opentls_tls_stream.rs index fa8009a65..7a5ea9f04 100644 --- a/src/client/tls_stream/opentls_tls_stream.rs +++ b/src/client/tls_stream/opentls_tls_stream.rs @@ -14,6 +14,13 @@ pub(crate) async fn create_tls_stream( ) -> crate::Result> { let mut builder = TlsConnector::new(); + if matches!(config.encryption, crate::EncryptionLevel::Strict) { + event!( + Level::WARN, + "OpenTLS does not support ALPN, so the TDS 8.0 ALPN protocol will not be requested. SQL Server will assume TDS 8.0." + ); + } + match &config.trust { TrustConfig::CaCertificateLocation(path) => { if let Ok(buf) = fs::read(path) { @@ -56,5 +63,7 @@ pub(crate) async fn create_tls_stream( } } - Ok(builder.connect(config.get_host(), stream).await?) + Ok(builder + .connect(config.get_hostname_in_certificate(), stream) + .await?) } diff --git a/src/client/tls_stream/rustls_tls_stream.rs b/src/client/tls_stream/rustls_tls_stream.rs index 88871ba07..e79646902 100644 --- a/src/client/tls_stream/rustls_tls_stream.rs +++ b/src/client/tls_stream/rustls_tls_stream.rs @@ -87,7 +87,10 @@ impl ServerCertVerifier for NoCertVerifier { } fn get_server_name(config: &Config) -> crate::Result> { - match (ServerName::try_from(config.get_host()), &config.trust) { + match ( + ServerName::try_from(config.get_hostname_in_certificate()), + &config.trust, + ) { (Ok(sn), _) => Ok(sn.to_owned()), (Err(_), TrustConfig::TrustAll) => { Ok(ServerName::try_from("placeholder.domain.com").unwrap()) @@ -105,7 +108,7 @@ impl TlsStream { .with_safe_default_protocol_versions() .map_err(|e| crate::Error::Tls(e.to_string()))?; - let client_config = match &config.trust { + let mut client_config = match &config.trust { TrustConfig::CaCertificateLocation(path) => { if let Ok(buf) = fs::read(path) { let cert = match path.extension() { @@ -173,6 +176,14 @@ impl TlsStream { } }; + // TDS 8.0 "strict" mode advertises the `tds/8.0` ALPN protocol so the + // server knows to speak TDS directly over the TLS stream. + if matches!(config.encryption, crate::EncryptionLevel::Strict) { + client_config + .alpn_protocols + .push(super::TDS_ALPN_PROTOCOL_NAME.as_bytes().to_vec()); + } + let connector = TlsConnector::from(Arc::new(client_config)); let tls_stream = connector diff --git a/src/lib.rs b/src/lib.rs index 1115a5e2a..5a1495f84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,6 +250,16 @@ #![doc(test(attr(deny(rust_2018_idioms, warnings))))] #![doc(test(attr(allow(unused_extern_crates, unused_variables))))] +#[cfg(all( + feature = "tds80", + not(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" + )) +))] +compile_error!("The `tds80` feature requires one of the TLS features to be enabled."); + #[cfg(feature = "bigdecimal")] pub(crate) extern crate bigdecimal_ as bigdecimal; diff --git a/src/tds.rs b/src/tds.rs index f4b6f9253..95cb556f6 100644 --- a/src/tds.rs +++ b/src/tds.rs @@ -25,6 +25,37 @@ uint_enum! { NotSupported = 2, /// Encrypt everything and fail if not possible Required = 3, + /// Start encryption before the TDS prelogin (TDS 8.0 "strict" mode) and + /// encrypt everything, failing if not possible. + Strict = 4, } } + +impl EncryptionLevel { + /// The value sent on the wire in the prelogin `ENCRYPTION` option. + /// + /// `Strict` (TDS 8.0) is negotiated out-of-band via a TLS handshake before + /// the prelogin, so when a prelogin is emitted at all it advertises the + /// classic `Required` value. + pub(crate) fn as_wire_value(&self) -> u8 { + match self { + EncryptionLevel::Strict => EncryptionLevel::Required as u8, + other => *other as u8, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encryption_level_as_wire_value() { + assert_eq!(EncryptionLevel::Off.as_wire_value(), 0); + assert_eq!(EncryptionLevel::On.as_wire_value(), 1); + assert_eq!(EncryptionLevel::NotSupported.as_wire_value(), 2); + assert_eq!(EncryptionLevel::Required.as_wire_value(), 3); + assert_eq!(EncryptionLevel::Strict.as_wire_value(), 3); + } +} diff --git a/src/tds/codec/login.rs b/src/tds/codec/login.rs index 1a55af5e4..9a4a7f255 100644 --- a/src/tds/codec/login.rs +++ b/src/tds/codec/login.rs @@ -179,10 +179,62 @@ impl<'a> LoginMessage<'a> { option_flags_2: OptionFlag2::InitLangFatal | OptionFlag2::OdbcDriver, option_flags_3: BitFlags::from_flag(OptionFlag3::UnknownCollationHandling), app_name: "tiberius".into(), + hostname: Self::get_hostname(), ..Default::default() } } + /// Best-effort local workstation id (machine hostname), used as the default + /// login `hostname`. Returns an empty string if it cannot be determined. + fn get_hostname() -> Cow<'static, str> { + #[cfg(windows)] + fn get_computer_name() -> io::Result { + extern "system" { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamew + fn GetComputerNameW(lpBuffer: *mut u16, nSize: *mut u32) -> i32; + } + + // MAX_COMPUTERNAME_LENGTH is 15, plus 1 for the null terminator. + let mut buffer = [0u16; 15 + 1]; + let mut size = buffer.len() as u32; + let result = unsafe { GetComputerNameW(buffer.as_mut_ptr(), &mut size) }; + if result == 0 { + let lerr = io::Error::last_os_error(); + tracing::error!("GetComputerNameW failed: {lerr}"); + Err(lerr) + } else { + Ok(String::from_utf16_lossy(&buffer[..size as usize])) + } + } + + #[cfg(target_family = "unix")] + fn get_computer_name() -> io::Result { + // POSIX gethostname() may or may not null-terminate on truncation, + // so we split on the first NUL (falling back to the whole buffer). + let mut buffer = [0u8; 255 + 1]; + let result = unsafe { + libc::gethostname(buffer.as_mut_ptr() as *mut _, buffer.len() as libc::size_t) + }; + if result != 0 { + let lerr = io::Error::last_os_error(); + tracing::error!("gethostname failed: {lerr}"); + Err(lerr) + } else { + match buffer.split(|b| *b == 0).next() { + Some(hostname) => Ok(String::from_utf8_lossy(hostname).into_owned()), + None => Ok(String::from_utf8_lossy(&buffer).into_owned()), + } + } + } + + #[cfg(not(any(windows, target_family = "unix")))] + fn get_computer_name() -> io::Result { + Ok(String::new()) + } + + get_computer_name().map(Cow::Owned).unwrap_or_default() + } + #[cfg(any(all(unix, feature = "integrated-auth-gssapi"), windows))] pub fn integrated_security(&mut self, bytes: Option>) { if bytes.is_some() { @@ -206,6 +258,11 @@ impl<'a> LoginMessage<'a> { self.server_name = server_name.into(); } + /// Sets the client / workstation name reported to the server. + pub fn hostname(&mut self, hostname: impl Into>) { + self.hostname = hostname.into(); + } + pub fn user_name(&mut self, user_name: impl Into>) { self.username = user_name.into(); } diff --git a/src/tds/codec/pre_login.rs b/src/tds/codec/pre_login.rs index a21f0bce6..57a3ea9fb 100644 --- a/src/tds/codec/pre_login.rs +++ b/src/tds/codec/pre_login.rs @@ -74,6 +74,9 @@ impl PreloginMessage { "Server does not allow the requested encryption level.".into(), )) } + // In TDS 8.0 "strict" mode encryption is established before the + // prelogin, so there is nothing to negotiate here. + (EncryptionLevel::Strict, _) => EncryptionLevel::Strict, (_, _) => EncryptionLevel::On, }; @@ -114,7 +117,7 @@ impl Encode for PreloginMessage { // encryption fields.push((PRELOGIN_ENCRYPTION, 0x01)); // encryption - data_cursor.write_u8(self.encryption as u8)?; + data_cursor.write_u8(self.encryption.as_wire_value())?; // threadid fields.push((PRELOGIN_THREADID, 0x04)); // thread id From 65e3b84d31a92332b016e1d7d92a22bb547d1291 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:45:04 -0700 Subject: [PATCH 02/42] docs: use docsrs cfg instead of a nightly-only docs feature Fixes the docs.rs build (doc_status:false on 0.13.0-alpha.1). The crate gated #![feature(doc_cfg)] behind a 'docs' cargo feature, which failed to build. Switch all #[doc(cfg(...))] annotations to the standard #[cfg_attr(docsrs, ...)] pattern, set docs.rs to build with rustdoc-args=[--cfg docsrs], drop the unused 'docs' feature, and declare docsrs via [lints.rust] check-cfg so clippy -D warnings stays clean. Normal builds no longer require nightly. --- Cargo.toml | 9 +++++++-- src/client/auth.rs | 10 +++++----- src/error.rs | 4 ++-- src/lib.rs | 2 +- src/tds/codec/column_data.rs | 8 ++++---- src/tds/numeric.rs | 4 ++-- src/tds/time.rs | 30 +++++++++++++++--------------- src/tds/time/chrono.rs | 2 +- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 466668c59..97ed9560e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,7 +178,13 @@ chrono = "0.4.38" indoc = "1.0.7" [package.metadata.docs.rs] -features = ["all", "docs"] +features = ["all"] +# docs.rs builds on nightly with this cfg set, enabling #[doc(cfg(...))] +# annotations (feature(doc_cfg)) without requiring nightly for normal builds. +rustdoc-args = ["--cfg", "docsrs"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs)'] } [features] all = [ @@ -199,7 +205,6 @@ tds73 = [] # Enables TDS 8.0 support, including the `Strict` encryption level (TLS before # the TDS prelogin, TDS 8.0 "strict" mode). Requires a TLS backend. tds80 = ["tds73"] -docs = [] sql-browser-async-std = ["async-std"] sql-browser-tokio = ["tokio", "tokio-util"] sql-browser-smol = ["async-io", "async-net", "futures-lite"] diff --git a/src/client/auth.rs b/src/client/auth.rs index 3abf42df8..8e99f708f 100644 --- a/src/client/auth.rs +++ b/src/client/auth.rs @@ -24,7 +24,7 @@ impl Debug for SqlServerAuth { #[derive(Clone, PartialEq, Eq)] #[cfg(any(all(windows, feature = "winauth"), doc))] -#[cfg_attr(feature = "docs", doc(all(windows, feature = "winauth")))] +#[cfg_attr(docsrs, doc(all(windows, feature = "winauth")))] pub struct WindowsAuth { pub(crate) user: String, pub(crate) password: String, @@ -32,7 +32,7 @@ pub struct WindowsAuth { } #[cfg(any(all(windows, feature = "winauth"), doc))] -#[cfg_attr(feature = "docs", doc(all(windows, feature = "winauth")))] +#[cfg_attr(docsrs, doc(all(windows, feature = "winauth")))] impl Debug for WindowsAuth { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WindowsAuth") @@ -50,7 +50,7 @@ pub enum AuthMethod { SqlServer(SqlServerAuth), /// Authenticate with Windows credentials. #[cfg(any(all(windows, feature = "winauth"), doc))] - #[cfg_attr(feature = "docs", doc(cfg(all(windows, feature = "winauth"))))] + #[cfg_attr(docsrs, doc(cfg(all(windows, feature = "winauth"))))] Windows(WindowsAuth), /// Authenticate as the currently logged in user. On Windows uses SSPI and /// Kerberos on Unix platforms. @@ -60,7 +60,7 @@ pub enum AuthMethod { doc ))] #[cfg_attr( - feature = "docs", + docsrs, doc(cfg(any(windows, all(unix, feature = "integrated-auth-gssapi")))) )] Integrated, @@ -82,7 +82,7 @@ impl AuthMethod { /// Construct a new Windows authentication configuration. #[cfg(any(all(windows, feature = "winauth"), doc))] - #[cfg_attr(feature = "docs", doc(cfg(all(windows, feature = "winauth"))))] + #[cfg_attr(docsrs, doc(cfg(all(windows, feature = "winauth"))))] pub fn windows(user: impl AsRef, password: impl ToString) -> Self { let (domain, user) = match user.as_ref().find('\\') { Some(idx) => (Some(&user.as_ref()[..idx]), &user.as_ref()[idx + 1..]), diff --git a/src/error.rs b/src/error.rs index 504ca8c18..fb7cc1c2e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,7 +42,7 @@ pub enum Error { Tls(String), #[cfg(any(all(unix, feature = "integrated-auth-gssapi"), doc))] #[cfg_attr( - feature = "docs", + docsrs, doc(cfg(all(unix, feature = "integrated-auth-gssapi"))) )] /// An error from the GSSAPI library. @@ -149,7 +149,7 @@ impl From for Error { #[cfg(all(unix, feature = "integrated-auth-gssapi"))] #[cfg_attr( - feature = "docs", + docsrs, doc(cfg(all(unix, feature = "integrated-auth-gssapi"))) )] impl From for Error { diff --git a/src/lib.rs b/src/lib.rs index 5a1495f84..d27cf1d32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -243,7 +243,7 @@ //! [`time`]: time/index.html //! [ways of authentication]: enum.AuthMethod.html //! [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings -#![cfg_attr(feature = "docs", feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![recursion_limit = "512"] #![warn(missing_docs)] #![warn(missing_debug_implementations, rust_2018_idioms)] diff --git a/src/tds/codec/column_data.rs b/src/tds/codec/column_data.rs index 054d10a2e..76191fd3c 100644 --- a/src/tds/codec/column_data.rs +++ b/src/tds/codec/column_data.rs @@ -68,19 +68,19 @@ pub enum ColumnData<'a> { /// A small DateTime value. SmallDateTime(Option), #[cfg(feature = "tds73")] - #[cfg_attr(feature = "docs", doc(cfg(feature = "tds73")))] + #[cfg_attr(docsrs, doc(cfg(feature = "tds73")))] /// Time value. Time(Option