Skip to main content

tds_driver/client/
config.rs

1mod ado_net;
2mod jdbc;
3
4use std::path::PathBuf;
5use std::time::Duration;
6
7use super::AuthMethod;
8use crate::EncryptionLevel;
9use ado_net::*;
10use jdbc::*;
11
12/// SqlClient's `Connect Timeout` default.
13const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
14
15#[derive(Clone, Debug)]
16/// The `Config` struct contains all configuration information
17/// required for connecting to the database with a [`Client`]. It also provides
18/// the server address when connecting to a `TcpStream` via the
19/// [`get_addr`] method.
20///
21/// When using an [ADO.NET connection string], it can be
22/// constructed using the [`from_ado_string`] function.
23///
24/// Alternatively, a [`ConfigBuilder`] can be used for an ergonomic,
25/// chainable construction. Create one via [`builder`], call its
26/// setter methods and finalize it with [`build`].
27///
28/// [`Client`]: struct.Client.html
29/// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings
30/// [`from_ado_string`]: struct.Config.html#method.from_ado_string
31/// [`get_addr`]: struct.Config.html#method.get_addr
32/// [`ConfigBuilder`]: struct.ConfigBuilder.html
33/// [`builder`]: struct.Config.html#method.builder
34/// [`build`]: struct.ConfigBuilder.html#method.build
35pub struct Config {
36    pub(crate) host: Option<String>,
37    pub(crate) port: Option<u16>,
38    pub(crate) database: Option<String>,
39    pub(crate) instance_name: Option<String>,
40    pub(crate) application_name: Option<String>,
41    pub(crate) encryption: EncryptionLevel,
42    pub(crate) trust: TrustConfig,
43    pub(crate) auth: AuthMethod,
44    pub(crate) readonly: bool,
45    pub(crate) packet_size: Option<u32>,
46    pub(crate) hostname_in_certificate: Option<String>,
47    pub(crate) client_name: Option<String>,
48    pub(crate) multi_subnet_failover: bool,
49    pub(crate) connect_timeout: Option<Duration>,
50    pub(crate) server_spn: Option<String>,
51    #[cfg(any(
52        feature = "rustls",
53        feature = "native-tls",
54        feature = "vendored-openssl"
55    ))]
56    pub(crate) client_cert: Option<ClientCertificate>,
57}
58
59#[derive(Clone, Debug)]
60pub(crate) enum TrustConfig {
61    #[allow(dead_code)]
62    CaCertificateLocation(PathBuf),
63    TrustAll,
64    Default,
65}
66
67/// A client certificate and its private key, presented to the server during the
68/// TLS handshake to authenticate the *client* (mutual TLS / TDS 8.0
69/// `ENCRYPT_CLIENT_CERT`).
70///
71/// Construct one indirectly via [`Config::client_certificate`] (PEM/DER
72/// certificate + private-key files) or [`Config::client_certificate_pkcs12`]
73/// (a PKCS#12 / PFX bundle, `native-tls` and `vendored-openssl` only).
74#[cfg(any(
75    feature = "rustls",
76    feature = "native-tls",
77    feature = "vendored-openssl"
78))]
79#[derive(Clone, Debug)]
80pub(crate) struct ClientCertificate {
81    pub(crate) source: ClientCertSource,
82}
83
84#[cfg(any(
85    feature = "rustls",
86    feature = "native-tls",
87    feature = "vendored-openssl"
88))]
89#[derive(Clone)]
90pub(crate) enum ClientCertSource {
91    /// A certificate file and a separate private-key file. Both may be PEM
92    /// (`.pem`/`.crt` for the certificate, `.pem`/`.key` for the key) or DER
93    /// (`.der`); the concrete format is detected from the file extension by the
94    /// active TLS backend.
95    CertAndKey { cert: PathBuf, key: PathBuf },
96    /// A PKCS#12 / PFX bundle path together with its decryption password. Only
97    /// supported by the `native-tls` and `vendored-openssl` backends.
98    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
99    Pkcs12 {
100        path: PathBuf,
101        password: zeroize::Zeroizing<String>,
102    },
103}
104
105// Manual `Debug` so the PKCS#12 password is never printed.
106#[cfg(any(
107    feature = "rustls",
108    feature = "native-tls",
109    feature = "vendored-openssl"
110))]
111impl std::fmt::Debug for ClientCertSource {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            ClientCertSource::CertAndKey { cert, key } => f
115                .debug_struct("CertAndKey")
116                .field("cert", cert)
117                .field("key", key)
118                .finish(),
119            #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
120            ClientCertSource::Pkcs12 { path, .. } => f
121                .debug_struct("Pkcs12")
122                .field("path", path)
123                .field("password", &"<redacted>")
124                .finish(),
125        }
126    }
127}
128
129impl Default for Config {
130    fn default() -> Self {
131        Self {
132            host: None,
133            port: None,
134            database: None,
135            instance_name: None,
136            application_name: None,
137            #[cfg(any(
138                feature = "rustls",
139                feature = "native-tls",
140                feature = "vendored-openssl"
141            ))]
142            encryption: EncryptionLevel::Required,
143            #[cfg(not(any(
144                feature = "rustls",
145                feature = "native-tls",
146                feature = "vendored-openssl"
147            )))]
148            encryption: EncryptionLevel::NotSupported,
149            trust: TrustConfig::Default,
150            auth: AuthMethod::None,
151            readonly: false,
152            packet_size: None,
153            hostname_in_certificate: None,
154            client_name: None,
155            multi_subnet_failover: false,
156            connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
157            server_spn: None,
158            #[cfg(any(
159                feature = "rustls",
160                feature = "native-tls",
161                feature = "vendored-openssl"
162            ))]
163            client_cert: None,
164        }
165    }
166}
167
168// `AuthMethod::AADToken` holds a plain `String` (public API); zeroize it
169// whenever a `Config` goes, including a connect future dropped by a timeout.
170impl Drop for Config {
171    fn drop(&mut self) {
172        if let AuthMethod::AADToken(token) = &mut self.auth {
173            zeroize::Zeroize::zeroize(token);
174        }
175    }
176}
177
178impl Config {
179    /// Create a new `Config` with the default settings.
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Create a new [`ConfigBuilder`] initialized with the default settings.
185    ///
186    /// This provides an ergonomic, chainable alternative to constructing a
187    /// [`Config`] via its individual setter methods.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// # use tds_driver::{Config, AuthMethod};
193    /// let config = Config::builder()
194    ///     .host("localhost")
195    ///     .port(1433)
196    ///     .database("master")
197    ///     .authentication(AuthMethod::sql_server("SA", "<password>"))
198    ///     .build();
199    ///
200    /// assert_eq!("localhost:1433", config.get_addr());
201    /// ```
202    ///
203    /// [`ConfigBuilder`]: struct.ConfigBuilder.html
204    /// [`Config`]: struct.Config.html
205    pub fn builder() -> ConfigBuilder {
206        ConfigBuilder {
207            inner: Self::default(),
208        }
209    }
210
211    /// A host or ip address to connect to.
212    ///
213    /// - Defaults to `localhost`.
214    pub fn host(&mut self, host: impl ToString) {
215        self.host = Some(host.to_string());
216    }
217
218    /// The server port.
219    ///
220    /// - Defaults to `1433`.
221    pub fn port(&mut self, port: u16) {
222        self.port = Some(port);
223    }
224
225    /// The database to connect to.
226    ///
227    /// - Defaults to `master`.
228    pub fn database(&mut self, database: impl ToString) {
229        self.database = Some(database.to_string())
230    }
231
232    /// The instance name as defined in the SQL Browser. Only available on
233    /// Windows platforms.
234    ///
235    /// If specified, the port is replaced with the value returned from the
236    /// browser.
237    ///
238    /// - Defaults to no name specified.
239    pub fn instance_name(&mut self, name: impl ToString) {
240        self.instance_name = Some(name.to_string());
241    }
242
243    /// Sets the application name to the connection, queryable with the
244    /// `APP_NAME()` command.
245    ///
246    /// - Defaults to `tds-driver`.
247    pub fn application_name(&mut self, name: impl ToString) {
248        self.application_name = Some(name.to_string());
249    }
250
251    /// Sets the TDS packet size for the connection.
252    ///
253    /// Larger packet sizes can improve bulk insert performance by reducing
254    /// the number of network round-trips. Valid values are 512 to 32767.
255    /// The server may negotiate a different size.
256    ///
257    /// - Defaults to 4096 bytes.
258    pub fn packet_size(&mut self, size: u32) {
259        self.packet_size = Some(size);
260    }
261
262    /// Gets the configured packet size, if set.
263    pub fn get_packet_size(&self) -> Option<u32> {
264        self.packet_size
265    }
266
267    /// Set the preferred encryption level.
268    ///
269    /// - With `tls` feature, defaults to `Required`.
270    /// - Without `tls` feature, defaults to `NotSupported`.
271    pub fn encryption(&mut self, encryption: EncryptionLevel) {
272        self.encryption = encryption;
273    }
274
275    /// If set, the server certificate will not be validated and it is accepted
276    /// as-is.
277    ///
278    /// On production setting, the certificate should be added to the local key
279    /// storage (or use `trust_cert_ca` instead), using this setting is potentially dangerous.
280    ///
281    /// # Panics
282    /// Will panic in case `trust_cert_ca` was called before.
283    ///
284    /// - Defaults to `default`, meaning server certificate is validated against system-truststore.
285    pub fn trust_cert(&mut self) {
286        if let TrustConfig::CaCertificateLocation(_) = &self.trust {
287            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
288        }
289        self.trust = TrustConfig::TrustAll;
290    }
291
292    /// If set, the server certificate will be validated against the given CA certificate in
293    /// in addition to the system-truststore.
294    /// Useful when using self-signed certificates on the server without having to disable the
295    /// trust-chain.
296    ///
297    /// # Panics
298    /// Will panic in case `trust_cert` was called before.
299    ///
300    /// - Defaults to validating the server certificate is validated against system's certificate storage.
301    pub fn trust_cert_ca(&mut self, path: impl ToString) {
302        if let TrustConfig::TrustAll = &self.trust {
303            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
304        } else {
305            self.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
306        }
307    }
308
309    /// Sets the hostname that the server certificate is validated against,
310    /// instead of the value given to [`host`].
311    ///
312    /// This is useful when connecting through an IP address, a tunnel, or a
313    /// load balancer whose certificate carries a different subject/SAN than the
314    /// address used to reach it (see issue #340).
315    ///
316    /// - Defaults to the value of [`host`].
317    ///
318    /// [`host`]: Config::host
319    pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
320        self.hostname_in_certificate = Some(hostname.to_string());
321    }
322
323    /// Sets the client / workstation name reported to the server in the login
324    /// record (queryable with `HOST_NAME()`).
325    ///
326    /// - Defaults to the local workstation id (the machine hostname).
327    pub fn client_name(&mut self, name: impl ToString) {
328        self.client_name = Some(name.to_string());
329    }
330
331    /// Sets the authentication method.
332    ///
333    /// - Defaults to `None`.
334    pub fn authentication(&mut self, auth: AuthMethod) {
335        self.auth = auth;
336    }
337
338    /// Sets ApplicationIntent readonly.
339    ///
340    /// - Defaults to `false`.
341    pub fn readonly(&mut self, readnoly: bool) {
342        self.readonly = readnoly;
343    }
344
345    /// Enable multi-subnet failover.
346    ///
347    /// When enabled and the server host name resolves to more than one IP
348    /// address (for example, an Always On availability group listener spread
349    /// across subnets), connections are attempted to all resolved addresses in
350    /// parallel and the first one to succeed is used. This mirrors the ADO.NET
351    /// `MultiSubnetFailover` connection-string keyword.
352    ///
353    /// - Defaults to `false`.
354    pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) {
355        self.multi_subnet_failover = multi_subnet_failover;
356    }
357
358    /// Returns whether multi-subnet failover is enabled.
359    pub fn get_multi_subnet_failover(&self) -> bool {
360        self.multi_subnet_failover
361    }
362
363    /// How long establishing a connection may take: TCP connect, the SQL
364    /// Browser lookup, TLS and login. `None` means no limit.
365    ///
366    /// The crate is runtime-agnostic and owns no timer, so it doesn't enforce
367    /// this itself: wrap the TCP connect and [`Client::connect`] in your
368    /// runtime's timeout.
369    ///
370    /// ```no_run
371    /// # use tds_driver::{Client, Config};
372    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
373    /// # #[tokio::main]
374    /// # async fn main() -> anyhow::Result<()> {
375    /// let config = Config::from_ado_string("server=tcp:db,1433;Connect Timeout=10")?;
376    /// let connect = async {
377    ///     let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
378    ///     tcp.set_nodelay(true)?;
379    ///     Ok::<_, anyhow::Error>(Client::connect(config.clone(), tcp.compat_write()).await?)
380    /// };
381    /// let client = match config.connect_timeout() {
382    ///     Some(limit) => tokio::time::timeout(limit, connect).await??,
383    ///     None => connect.await?,
384    /// };
385    /// # Ok(()) }
386    /// ```
387    ///
388    /// - Defaults to 15 seconds, as in SqlClient. Set from a connection
389    ///   string's `Connect Timeout` (ADO.NET, 0 = no limit) or `loginTimeout`
390    ///   (JDBC, 0 = the 30-second JDBC default).
391    ///
392    /// [`Client::connect`]: crate::Client::connect
393    pub fn connect_timeout(&self) -> Option<Duration> {
394        self.connect_timeout
395    }
396
397    /// Sets the value returned by [`connect_timeout`](Config::connect_timeout).
398    pub fn set_connect_timeout(&mut self, timeout: Option<Duration>) {
399        self.connect_timeout = timeout;
400    }
401
402    /// The service principal name integrated authentication asks a ticket
403    /// for, as given, like SqlClient's `Server SPN`. A blank one is ignored.
404    ///
405    /// - Defaults to `MSSQLSvc/<host>:<port>`, or `MSSQLSvc/<host>:<instance>`
406    ///   for a named instance without a port.
407    pub fn server_spn(&mut self, spn: impl ToString) {
408        self.server_spn = Some(spn.to_string());
409    }
410
411    /// The SPN set with [`server_spn`](Config::server_spn), unless blank:
412    /// SqlClient derives one for a null or white-space `ServerSPN`.
413    pub(crate) fn get_server_spn(&self) -> Option<&str> {
414        self.server_spn
415            .as_deref()
416            .filter(|spn| !spn.trim().is_empty())
417    }
418
419    /// Supplies a client certificate and private key used to authenticate the
420    /// client to the server during the TLS handshake (mutual TLS). This is
421    /// required for TDS 8.0 "strict" connections that use client-certificate
422    /// authentication (`ENCRYPT_CLIENT_CERT`), and may also be used with the
423    /// classic (pre-8.0) TLS handshake when the server requests a client
424    /// certificate.
425    ///
426    /// Both arguments are paths to files:
427    ///
428    /// - `cert`: the client certificate, PEM (`.pem`/`.crt`) or DER (`.der`).
429    /// - `key`: the matching private key, PEM (`.pem`/`.key`) or DER (`.der`,
430    ///   PKCS#8).
431    ///
432    /// Backend support:
433    ///
434    /// - `rustls`: PEM and DER certificate/key files.
435    /// - `native-tls`: PEM certificate + PEM PKCS#8 key only (DER files are
436    ///   rejected at connect time; use [`client_certificate_pkcs12`] for a
437    ///   bundled DER identity).
438    /// - `vendored-openssl` (opentls): does not support separate certificate/key
439    ///   files; use [`client_certificate_pkcs12`] instead.
440    ///
441    /// - Defaults to no client certificate.
442    ///
443    /// [`client_certificate_pkcs12`]: Config::client_certificate_pkcs12
444    #[cfg(any(
445        feature = "rustls",
446        feature = "native-tls",
447        feature = "vendored-openssl"
448    ))]
449    #[cfg_attr(
450        docsrs,
451        doc(cfg(any(
452            feature = "rustls",
453            feature = "native-tls",
454            feature = "vendored-openssl"
455        )))
456    )]
457    pub fn client_certificate(&mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) {
458        self.client_cert = Some(ClientCertificate {
459            source: ClientCertSource::CertAndKey {
460                cert: cert.into(),
461                key: key.into(),
462            },
463        });
464    }
465
466    /// Supplies a client identity from a PKCS#12 / PFX bundle (certificate,
467    /// private key and any chain, encrypted with `password`) used to
468    /// authenticate the client to the server during the TLS handshake (mutual
469    /// TLS).
470    ///
471    /// Only supported by the `native-tls` and `vendored-openssl` backends; the
472    /// `rustls` backend rejects PKCS#12 identities at connect time (supply
473    /// separate PEM/DER files via [`client_certificate`] instead).
474    ///
475    /// - Defaults to no client certificate.
476    ///
477    /// [`client_certificate`]: Config::client_certificate
478    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
479    #[cfg_attr(
480        docsrs,
481        doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
482    )]
483    pub fn client_certificate_pkcs12(
484        &mut self,
485        path: impl Into<PathBuf>,
486        password: impl Into<String>,
487    ) {
488        self.client_cert = Some(ClientCertificate {
489            source: ClientCertSource::Pkcs12 {
490                path: path.into(),
491                password: zeroize::Zeroizing::new(password.into()),
492            },
493        });
494    }
495
496    #[cfg(any(
497        feature = "rustls",
498        feature = "native-tls",
499        feature = "vendored-openssl"
500    ))]
501    pub(crate) fn get_client_certificate(&self) -> Option<&ClientCertificate> {
502        self.client_cert.as_ref()
503    }
504
505    pub(crate) fn get_host(&self) -> &str {
506        self.host
507            .as_deref()
508            .filter(|v| v != &".")
509            .unwrap_or("localhost")
510    }
511
512    #[cfg(any(
513        feature = "rustls",
514        feature = "native-tls",
515        feature = "vendored-openssl"
516    ))]
517    pub(crate) fn get_hostname_in_certificate(&self) -> &str {
518        self.hostname_in_certificate
519            .as_deref()
520            .unwrap_or_else(|| self.get_host())
521    }
522
523    /// The certificate validation to apply. Strict encryption always
524    /// validates: like SqlClient, it ignores `trust_cert`.
525    #[cfg(any(
526        feature = "rustls",
527        feature = "native-tls",
528        feature = "vendored-openssl"
529    ))]
530    pub(crate) fn tls_trust(&self) -> &TrustConfig {
531        static VALIDATE: TrustConfig = TrustConfig::Default;
532
533        match (&self.trust, self.encryption) {
534            (TrustConfig::TrustAll, EncryptionLevel::Strict) => {
535                tracing::warn!(
536                    "TrustServerCertificate is ignored with strict encryption; validating the server certificate."
537                );
538                &VALIDATE
539            }
540            (trust, _) => trust,
541        }
542    }
543
544    pub(crate) fn get_port(&self) -> u16 {
545        match (self.port, self.instance_name.as_ref()) {
546            // A user-defined port, we must use that.
547            (Some(port), _) => port,
548            // If using a named instance, we'll give the default port of SQL
549            // Browser.
550            (None, Some(_)) => 1434,
551            // Otherwise the defaulting to the default SQL Server port.
552            (None, None) => 1433,
553        }
554    }
555
556    /// Get the host address including port
557    pub fn get_addr(&self) -> String {
558        format!("{}:{}", self.get_host(), self.get_port())
559    }
560
561    /// Creates a new `Config` from an [ADO.NET connection string].
562    ///
563    /// # Supported parameters
564    ///
565    /// All parameter keys are handled case-insensitive. The last of a key and
566    /// its synonyms wins, and a key SqlClient doesn't know is an error.
567    ///
568    /// |Parameter|Allowed values|Description|
569    /// |--------|--------|--------|
570    /// |`Data Source`, `server`, `addr`, `address`, `network address`|`<string>`|The name or network address of the instance of SQL Server to which to connect. The port number can be specified after the server name. The correct form of this parameter is either `tcp:host,port` or `tcp:host\\instance`; other protocols (`np:`, `lpc:`, `admin:`) are errors.|
571    /// |`Integrated Security`, `IntegratedSecurity`, `Trusted_Connection`|`sspi`,`true`,`false`,`yes`,`no`|Toggle between Windows/Kerberos authentication and SQL authentication.|
572    /// |`User ID`, `uid`, `user`, `username`|`<string>`|The SQL Server login account.|
573    /// |`password`,`pwd`|`<string>`|The password for the SQL Server account logging on.|
574    /// |`Initial Catalog`, `database`|`<string>`|The name of the database.|
575    /// |`Trust Server Certificate`, `TrustServerCertificate`|`true`,`false`,`yes`,`no`|Specifies whether the driver trusts the server certificate when connecting using TLS. Cannot be used toghether with `TrustServerCertificateCA`|
576    /// |`TrustServerCertificateCA`|`<path>`|Path to a `pem`, `crt` or `der` certificate file. Cannot be used together with `TrustServerCertificate`|
577    /// |`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.|
578    /// |`Application Name`, `app`, `ApplicationName`|`<string>`|Sets the application name for the connection.|
579    /// |`Application Intent`, `ApplicationIntent`|`ReadOnly`,`ReadWrite`|Declares a read-only workload.|
580    /// |`Host Name In Certificate`, `HostNameInCertificate`|`<string>`|The hostname the server certificate is validated against. Defaults to `server`.|
581    /// |`Workstation ID`, `WorkstationID`, `wsid`|`<string>`|The client / workstation name reported to the server.|
582    /// |`Multi Subnet Failover`, `MultiSubnetFailover`|`true`,`false`,`yes`,`no`|When enabled, connections are attempted in parallel to all IP addresses the server resolves to, and the first to succeed is used.|
583    /// |`Connect Timeout`, `Connection Timeout`, `Timeout`|`<seconds>`|See [`Config::connect_timeout`].|
584    /// |`Packet Size`|`512` to `32768`|See [`Config::packet_size`].|
585    /// |`Server SPN`, `ServerSPN`|`<string>`|See [`Config::server_spn`].|
586    ///
587    /// SqlClient's pooling, retry, `Command Timeout`, `Enlist` and similar
588    /// keywords are accepted and ignored. Those this crate can't honour without
589    /// changing the connection are errors: `MultipleActiveResultSets=true`,
590    /// `Failover Partner`, `Column Encryption Setting=Enabled`,
591    /// `AttachDbFilename`, `Current Language`, `Server Certificate`,
592    /// `User Instance=true`, `Replication=true`, Entra ID `Authentication`
593    /// values and `Network Library` other than `dbmssocn`.
594    ///
595    /// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings
596    pub fn from_ado_string(s: &str) -> crate::Result<Self> {
597        let ado: AdoNetConfig = s.parse()?;
598        Self::from_config_string(ado)
599    }
600
601    /// Creates a new `Config` from a [JDBC connection string].
602    ///
603    /// See [`from_ado_string`] method for supported parameters. The URL is
604    /// split as mssql-jdbc splits it: a value containing `;` goes in braces,
605    /// `{like;this}`, with `}}` for a `}`; other values and all names are
606    /// trimmed; the last of a property and its synonyms wins.
607    ///
608    /// [JDBC connection string]: https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15
609    /// [`from_ado_string`]: #method.from_ado_string
610    pub fn from_jdbc_string(s: &str) -> crate::Result<Self> {
611        let jdbc: JdbcConfig = s.parse()?;
612        Self::from_config_string(jdbc)
613    }
614
615    fn from_config_string(s: impl ConfigString) -> crate::Result<Self> {
616        let mut builder = Self::new();
617
618        let server = s.server()?;
619
620        if let Some(host) = server.host {
621            builder.host(host);
622        }
623
624        if let Some(port) = server.port {
625            builder.port(port);
626        }
627
628        if let Some(instance) = server.instance {
629            builder.instance_name(instance);
630        }
631
632        builder.authentication(s.authentication()?);
633
634        if let Some(database) = s.database() {
635            builder.database(database);
636        }
637
638        if let Some(name) = s.application_name() {
639            builder.application_name(name);
640        }
641
642        let encryption = s.encrypt()?;
643        builder.encryption(encryption);
644
645        match (s.trust_cert()?, s.trust_cert_ca()) {
646            // SqlClient ignores TrustServerCertificate under Encrypt=Strict.
647            (true, ca) if encryption == EncryptionLevel::Strict => {
648                tracing::warn!(
649                    "TrustServerCertificate is ignored with Encrypt=Strict; validating the server certificate."
650                );
651                if let Some(ca) = ca {
652                    builder.trust_cert_ca(ca);
653                }
654            }
655            (true, Some(_)) => {
656                return Err(crate::Error::Conversion(
657                    "Connection string: TrustServerCertificate=true and TrustServerCertificateCA are mutually exclusive"
658                        .into(),
659                ))
660            }
661            (true, None) => builder.trust_cert(),
662            (false, Some(ca)) => builder.trust_cert_ca(ca),
663            (false, None) => (),
664        }
665
666        if let Some(hostname_in_cert) = s.hostname_in_certificate() {
667            builder.hostname_in_certificate(hostname_in_cert);
668        }
669
670        builder.readonly(s.readonly());
671
672        if let Some(client_name) = s.client_name() {
673            builder.client_name(client_name);
674        }
675        builder.multi_subnet_failover(s.multi_subnet_failover()?);
676        if let Some(size) = s.packet_size()? {
677            builder.packet_size(size);
678        }
679        builder.set_connect_timeout(s.connect_timeout()?);
680        if let Some(spn) = s.server_spn() {
681            builder.server_spn(spn);
682        }
683
684        Ok(builder)
685    }
686}
687
688/// A builder for [`Config`], providing an ergonomic, chainable way to
689/// construct a connection configuration.
690///
691/// Create a builder with [`Config::builder`], set the desired options by
692/// calling its methods (each returns the builder to allow chaining) and
693/// finalize it with [`build`].
694///
695/// # Example
696///
697/// ```
698/// # use tds_driver::{Config, AuthMethod, EncryptionLevel};
699/// let config = Config::builder()
700///     .host("localhost")
701///     .port(1433)
702///     .database("master")
703///     .encryption(EncryptionLevel::NotSupported)
704///     .authentication(AuthMethod::sql_server("SA", "<password>"))
705///     .build();
706/// ```
707///
708/// [`Config`]: struct.Config.html
709/// [`Config::builder`]: struct.Config.html#method.builder
710/// [`build`]: struct.ConfigBuilder.html#method.build
711#[derive(Clone, Debug)]
712pub struct ConfigBuilder {
713    inner: Config,
714}
715
716impl ConfigBuilder {
717    /// A host or ip address to connect to.
718    ///
719    /// - Defaults to `localhost`.
720    pub fn host(mut self, host: impl ToString) -> Self {
721        self.inner.host = Some(host.to_string());
722        self
723    }
724
725    /// The server port.
726    ///
727    /// - Defaults to `1433`.
728    pub fn port(mut self, port: u16) -> Self {
729        self.inner.port = Some(port);
730        self
731    }
732
733    /// The database to connect to.
734    ///
735    /// - Defaults to `master`.
736    pub fn database(mut self, database: impl ToString) -> Self {
737        self.inner.database = Some(database.to_string());
738        self
739    }
740
741    /// The instance name as defined in the SQL Browser. Only available on
742    /// Windows platforms.
743    ///
744    /// If specified, the port is replaced with the value returned from the
745    /// browser.
746    ///
747    /// - Defaults to no name specified.
748    pub fn instance_name(mut self, name: impl ToString) -> Self {
749        self.inner.instance_name = Some(name.to_string());
750        self
751    }
752
753    /// Sets the application name to the connection, queryable with the
754    /// `APP_NAME()` command.
755    ///
756    /// - Defaults to `tds-driver`.
757    pub fn application_name(mut self, name: impl ToString) -> Self {
758        self.inner.application_name = Some(name.to_string());
759        self
760    }
761
762    /// Set the preferred encryption level.
763    ///
764    /// - With `tls` feature, defaults to `Required`.
765    /// - Without `tls` feature, defaults to `NotSupported`.
766    pub fn encryption(mut self, encryption: EncryptionLevel) -> Self {
767        self.inner.encryption = encryption;
768        self
769    }
770
771    /// If set, the server certificate will not be validated and it is accepted
772    /// as-is.
773    ///
774    /// On production setting, the certificate should be added to the local key
775    /// storage (or use `trust_cert_ca` instead), using this setting is potentially dangerous.
776    ///
777    /// # Panics
778    /// Will panic in case `trust_cert_ca` was called before.
779    ///
780    /// - Defaults to `default`, meaning server certificate is validated against system-truststore.
781    pub fn trust_cert(mut self) -> Self {
782        if let TrustConfig::CaCertificateLocation(_) = &self.inner.trust {
783            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
784        }
785        self.inner.trust = TrustConfig::TrustAll;
786        self
787    }
788
789    /// If set, the server certificate will be validated against the given CA certificate in
790    /// in addition to the system-truststore.
791    /// Useful when using self-signed certificates on the server without having to disable the
792    /// trust-chain.
793    ///
794    /// # Panics
795    /// Will panic in case `trust_cert` was called before.
796    ///
797    /// - Defaults to validating the server certificate is validated against system's certificate storage.
798    pub fn trust_cert_ca(mut self, path: impl ToString) -> Self {
799        if let TrustConfig::TrustAll = &self.inner.trust {
800            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
801        } else {
802            self.inner.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
803        }
804        self
805    }
806
807    /// Sets the authentication method.
808    ///
809    /// - Defaults to `None`.
810    pub fn authentication(mut self, auth: AuthMethod) -> Self {
811        self.inner.auth = auth;
812        self
813    }
814
815    /// Sets ApplicationIntent readonly.
816    ///
817    /// - Defaults to `false`.
818    pub fn readonly(mut self, readonly: bool) -> Self {
819        self.inner.readonly = readonly;
820        self
821    }
822
823    /// Sets the connect timeout; see [`Config::connect_timeout`].
824    pub fn connect_timeout(mut self, timeout: Option<Duration>) -> Self {
825        self.inner.connect_timeout = timeout;
826        self
827    }
828
829    /// Sets the SPN for integrated authentication; see
830    /// [`Config::server_spn`].
831    pub fn server_spn(mut self, spn: impl ToString) -> Self {
832        self.inner.server_spn(spn);
833        self
834    }
835
836    /// Supplies a client certificate and private key for mutual TLS.
837    ///
838    /// See [`Config::client_certificate`] for details and backend support.
839    #[cfg(any(
840        feature = "rustls",
841        feature = "native-tls",
842        feature = "vendored-openssl"
843    ))]
844    #[cfg_attr(
845        docsrs,
846        doc(cfg(any(
847            feature = "rustls",
848            feature = "native-tls",
849            feature = "vendored-openssl"
850        )))
851    )]
852    pub fn client_certificate(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
853        self.inner.client_certificate(cert, key);
854        self
855    }
856
857    /// Supplies a client identity from a PKCS#12 / PFX bundle for mutual TLS.
858    ///
859    /// See [`Config::client_certificate_pkcs12`] for details and backend
860    /// support.
861    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
862    #[cfg_attr(
863        docsrs,
864        doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
865    )]
866    pub fn client_certificate_pkcs12(
867        mut self,
868        path: impl Into<PathBuf>,
869        password: impl Into<String>,
870    ) -> Self {
871        self.inner.client_certificate_pkcs12(path, password);
872        self
873    }
874
875    /// Produces the finalized [`Config`] from this builder.
876    ///
877    /// [`Config`]: struct.Config.html
878    pub fn build(self) -> Config {
879        self.inner
880    }
881}
882
883impl From<Config> for ConfigBuilder {
884    fn from(config: Config) -> Self {
885        ConfigBuilder { inner: config }
886    }
887}
888
889impl From<ConfigBuilder> for Config {
890    fn from(builder: ConfigBuilder) -> Self {
891        builder.inner
892    }
893}
894
895pub(crate) struct ServerDefinition {
896    host: Option<String>,
897    port: Option<u16>,
898    instance: Option<String>,
899}
900
901pub(crate) trait ConfigString {
902    fn get(&self, key: &str) -> Option<&str>;
903
904    /// The value of the first of `keys` present.
905    fn first(&self, keys: &[&str]) -> Option<&str> {
906        keys.iter().find_map(|key| self.get(key))
907    }
908
909    fn server(&self) -> crate::Result<ServerDefinition>;
910
911    fn connect_timeout(&self) -> crate::Result<Option<Duration>>;
912
913    fn packet_size(&self) -> crate::Result<Option<u32>> {
914        Ok(None)
915    }
916
917    /// `sspi`, `true` or `yes`; `false` or `no` (SqlClient's
918    /// `ConvertValueToIntegratedSecurityInternal`).
919    fn integrated_security(&self) -> crate::Result<bool> {
920        match self.first(&["integrated security", "integratedsecurity"]) {
921            Some(v) if v.trim().eq_ignore_ascii_case("sspi") => Ok(true),
922            Some(v) => Self::parse_bool(v),
923            None => Ok(false),
924        }
925    }
926
927    fn authentication(&self) -> crate::Result<AuthMethod> {
928        let user = self.first(&["uid", "username", "user", "user id"]);
929
930        let pw = self.first(&["password", "pwd"]);
931
932        if !self.integrated_security()? {
933            return Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or("")));
934        }
935
936        // Without a backend this used to log in with SQL authentication.
937        #[allow(unused)]
938        let missing = |what: &str| -> crate::Result<AuthMethod> {
939            Err(crate::Error::Conversion(
940                format!("Connection string: Integrated Security needs {what}").into(),
941            ))
942        };
943
944        #[cfg(all(windows, feature = "winauth"))]
945        return match (user, pw) {
946            (None, None) => Ok(AuthMethod::Integrated),
947            _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
948        };
949        #[cfg(all(windows, not(feature = "winauth")))]
950        return missing("the crate's `winauth` feature");
951
952        // `sspi-rs` is NTLM, so it needs a user name; Kerberos takes the
953        // logged-in user's ticket.
954        #[cfg(all(unix, feature = "sspi-rs"))]
955        return match (user, pw) {
956            (Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)),
957            #[cfg(feature = "integrated-auth-gssapi")]
958            (None, None) => Ok(AuthMethod::Integrated),
959            #[cfg(not(feature = "integrated-auth-gssapi"))]
960            (None, None) => missing("a User ID, or the crate's `integrated-auth-gssapi` feature"),
961            _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
962        };
963        #[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))]
964        return Ok(AuthMethod::Integrated);
965        #[cfg(all(
966            unix,
967            not(any(feature = "integrated-auth-gssapi", feature = "sspi-rs"))
968        ))]
969        return missing(
970            "the crate's `integrated-auth-gssapi` (Kerberos) or `sspi-rs` (NTLM) feature",
971        );
972
973        #[cfg(not(any(windows, unix)))]
974        return missing("Windows or Unix");
975    }
976
977    fn database(&self) -> Option<String> {
978        self.first(&["database", "initial catalog", "databasename"])
979            .map(|db| db.to_string())
980    }
981
982    fn application_name(&self) -> Option<String> {
983        self.first(&["application name", "applicationname"])
984            .map(|name| name.to_string())
985    }
986
987    fn trust_cert(&self) -> crate::Result<bool> {
988        self.first(&["trust server certificate", "trustservercertificate"])
989            .map(Self::parse_bool)
990            .unwrap_or(Ok(false))
991    }
992
993    fn trust_cert_ca(&self) -> Option<String> {
994        self.first(&["trustservercertificateca"])
995            .map(|ca| ca.to_string())
996    }
997
998    fn hostname_in_certificate(&self) -> Option<String> {
999        self.first(&[
1000            "host name in certificate",
1001            "hostnameincertificate",
1002            "hostname in certificate",
1003        ])
1004        .map(|host| host.to_string())
1005    }
1006
1007    fn client_name(&self) -> Option<String> {
1008        self.first(&["workstation id", "workstationid"])
1009            .map(|name| name.to_string())
1010    }
1011
1012    /// ADO.NET `Server SPN`, JDBC `serverSpn`.
1013    fn server_spn(&self) -> Option<String> {
1014        self.first(&["server spn", "serverspn"]).map(str::to_owned)
1015    }
1016
1017    #[cfg(any(
1018        feature = "rustls",
1019        feature = "native-tls",
1020        feature = "vendored-openssl"
1021    ))]
1022    fn encrypt(&self) -> crate::Result<EncryptionLevel> {
1023        self.first(&["encrypt"])
1024            .map(|val| {
1025                let val = val.trim();
1026                let is = |s: &str| val.eq_ignore_ascii_case(s);
1027                // SqlClient 5+: true/yes/mandatory, false/no/optional, strict.
1028                if is("true") || is("yes") || is("mandatory") {
1029                    Ok(EncryptionLevel::Required)
1030                } else if is("false") || is("no") || is("optional") {
1031                    Ok(EncryptionLevel::Off)
1032                } else if val == "DANGER_PLAINTEXT" {
1033                    Ok(EncryptionLevel::NotSupported)
1034                } else if is("strict") && cfg!(feature = "tds80") {
1035                    Ok(EncryptionLevel::Strict)
1036                } else if is("strict") {
1037                    Err(crate::Error::Conversion(
1038                        "encrypt=strict requires the crate's `tds80` feature to be enabled".into(),
1039                    ))
1040                } else {
1041                    Err(crate::Error::Conversion(
1042                        "Connection string: Encrypt must be true, false, yes, no, mandatory, optional or strict".into(),
1043                    ))
1044                }
1045            })
1046            // When the `encrypt` keyword is omitted, default to requiring
1047            // encryption — matching `Config::default()` and modern ADO.NET
1048            // (`Encrypt=Mandatory`). Callers who want an unencrypted connection
1049            // must opt out explicitly with `encrypt=false` (or
1050            // `encrypt=DANGER_PLAINTEXT`).
1051            .unwrap_or(Ok(EncryptionLevel::Required))
1052    }
1053
1054    #[cfg(not(any(
1055        feature = "rustls",
1056        feature = "native-tls",
1057        feature = "vendored-openssl"
1058    )))]
1059    fn encrypt(&self) -> crate::Result<EncryptionLevel> {
1060        Ok(EncryptionLevel::NotSupported)
1061    }
1062
1063    fn parse_bool<T: AsRef<str>>(v: T) -> crate::Result<bool> {
1064        match v.as_ref().trim().to_lowercase().as_str() {
1065            "true" | "yes" => Ok(true),
1066            "false" | "no" => Ok(false),
1067            _ => Err(crate::Error::Conversion(
1068                "Connection string: Not a valid boolean".into(),
1069            )),
1070        }
1071    }
1072
1073    fn readonly(&self) -> bool {
1074        self.first(&["application intent", "applicationintent"])
1075            .filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
1076            .is_some()
1077    }
1078
1079    fn multi_subnet_failover(&self) -> crate::Result<bool> {
1080        self.first(&["multi subnet failover", "multisubnetfailover"])
1081            .map(Self::parse_bool)
1082            .unwrap_or(Ok(false))
1083    }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089
1090    #[test]
1091    fn config_builder_constructs_config() {
1092        let config = Config::builder()
1093            .host("db.example.com")
1094            .port(4433)
1095            .database("northwind")
1096            .application_name("my-app")
1097            .authentication(AuthMethod::sql_server("SA", "secret"))
1098            .readonly(true)
1099            .build();
1100
1101        assert_eq!("db.example.com", config.get_host());
1102        assert_eq!(4433, config.get_port());
1103        assert_eq!("db.example.com:4433", config.get_addr());
1104        assert_eq!(Some("northwind"), config.database.as_deref());
1105        assert_eq!(Some("my-app"), config.application_name.as_deref());
1106        assert!(config.readonly);
1107        assert!(matches!(config.auth, AuthMethod::SqlServer(_)));
1108        assert!(matches!(config.trust, TrustConfig::Default));
1109    }
1110
1111    #[test]
1112    fn config_builder_roundtrips_via_from() {
1113        let config = Config::builder().host("localhost").port(1433).build();
1114        let builder: ConfigBuilder = config.into();
1115        let config = builder.database("master").build();
1116
1117        assert_eq!("localhost:1433", config.get_addr());
1118        assert_eq!(Some("master"), config.database.as_deref());
1119    }
1120
1121    #[test]
1122    fn config_from_builder_carries_builder_settings() {
1123        // `From<ConfigBuilder>` must return the built inner config, not a default.
1124        let config: Config = Config::builder().host("db.internal").port(2020).into();
1125        assert_eq!("db.internal", config.get_host());
1126        assert_eq!(2020, config.get_port());
1127    }
1128
1129    #[test]
1130    fn get_packet_size_reflects_the_set_value() {
1131        let mut config = Config::new();
1132        assert_eq!(config.get_packet_size(), None);
1133        config.packet_size(8192);
1134        assert_eq!(config.get_packet_size(), Some(8192));
1135    }
1136
1137    #[test]
1138    fn server_spn_setters() {
1139        let mut config = Config::new();
1140        assert_eq!(config.get_server_spn(), None);
1141        config.server_spn("MSSQLSvc/a:1");
1142        assert_eq!(config.get_server_spn(), Some("MSSQLSvc/a:1"));
1143        config.server_spn("");
1144        assert_eq!(config.get_server_spn(), None);
1145
1146        let config = Config::builder().server_spn("MSSQLSvc/b:2").build();
1147        assert_eq!(config.get_server_spn(), Some("MSSQLSvc/b:2"));
1148    }
1149
1150    #[test]
1151    fn from_jdbc_string_parses_host_and_port() {
1152        let config =
1153            Config::from_jdbc_string("jdbc:sqlserver://db.example.com:2345").expect("valid jdbc");
1154        assert_eq!("db.example.com", config.get_host());
1155        assert_eq!(2345, config.get_port());
1156    }
1157
1158    #[cfg(any(
1159        feature = "rustls",
1160        feature = "native-tls",
1161        feature = "vendored-openssl"
1162    ))]
1163    #[test]
1164    fn get_hostname_in_certificate_falls_back_to_host() {
1165        let mut config = Config::new();
1166        config.host("real.host");
1167        // Unset: falls back to the connection host.
1168        assert_eq!(config.get_hostname_in_certificate(), "real.host");
1169        // Set: returns the explicit certificate hostname.
1170        config.hostname_in_certificate("cert.host");
1171        assert_eq!(config.get_hostname_in_certificate(), "cert.host");
1172    }
1173
1174    #[cfg(any(
1175        feature = "rustls",
1176        feature = "native-tls",
1177        feature = "vendored-openssl"
1178    ))]
1179    #[test]
1180    fn client_certificate_sets_cert_and_key_source() {
1181        let mut config = Config::new();
1182        assert!(config.get_client_certificate().is_none());
1183
1184        config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1185
1186        let cert = config
1187            .get_client_certificate()
1188            .expect("client certificate should be set");
1189        match &cert.source {
1190            ClientCertSource::CertAndKey { cert, key } => {
1191                assert_eq!(cert, &PathBuf::from("/tmp/client.pem"));
1192                assert_eq!(key, &PathBuf::from("/tmp/client.key"));
1193            }
1194            #[allow(unreachable_patterns)]
1195            other => panic!("expected CertAndKey source, got {other:?}"),
1196        }
1197    }
1198
1199    #[cfg(any(
1200        feature = "rustls",
1201        feature = "native-tls",
1202        feature = "vendored-openssl"
1203    ))]
1204    #[test]
1205    fn config_builder_sets_client_certificate() {
1206        let config = Config::builder()
1207            .host("localhost")
1208            .client_certificate("cert.der", "key.der")
1209            .build();
1210
1211        match &config
1212            .get_client_certificate()
1213            .expect("client certificate should be set")
1214            .source
1215        {
1216            ClientCertSource::CertAndKey { cert, key } => {
1217                assert_eq!(cert, &PathBuf::from("cert.der"));
1218                assert_eq!(key, &PathBuf::from("key.der"));
1219            }
1220            #[allow(unreachable_patterns)]
1221            other => panic!("expected CertAndKey source, got {other:?}"),
1222        }
1223    }
1224
1225    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1226    #[test]
1227    fn client_certificate_pkcs12_sets_bundle_source() {
1228        let mut config = Config::new();
1229        config.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t");
1230
1231        match &config
1232            .get_client_certificate()
1233            .expect("client certificate should be set")
1234            .source
1235        {
1236            ClientCertSource::Pkcs12 { path, password } => {
1237                assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1238                assert_eq!(password.as_str(), "s3cr3t");
1239            }
1240            other => panic!("expected Pkcs12 source, got {other:?}"),
1241        }
1242    }
1243
1244    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1245    #[test]
1246    fn client_certificate_debug_redacts_pkcs12_password() {
1247        let mut config = Config::new();
1248        config.client_certificate_pkcs12("/tmp/identity.pfx", "topsecret");
1249
1250        let dbg = format!("{:?}", config.get_client_certificate().unwrap());
1251        assert!(dbg.contains("<redacted>"));
1252        assert!(!dbg.contains("topsecret"));
1253    }
1254
1255    #[cfg(all(unix, feature = "sspi-rs"))]
1256    #[test]
1257    fn ado_integrated_security_sspi_with_credentials_uses_windows_ntlm() {
1258        let config = Config::from_ado_string(
1259            "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=DOMAIN\\user;pwd=secret",
1260        )
1261        .unwrap();
1262
1263        match &config.auth {
1264            AuthMethod::Windows(auth) => {
1265                assert_eq!("user", auth.user);
1266                assert_eq!(Some("DOMAIN"), auth.domain.as_deref());
1267            }
1268            other => panic!("expected Windows NTLM auth, got {other:?}"),
1269        }
1270    }
1271
1272    #[test]
1273    fn config_direct_setters_populate_fields() {
1274        let mut config = Config::new();
1275        config.database("northwind");
1276        config.instance_name("SQLEXPRESS");
1277        config.client_name("workstation-7");
1278
1279        assert_eq!(Some("northwind"), config.database.as_deref());
1280        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1281        assert_eq!(Some("workstation-7"), config.client_name.as_deref());
1282    }
1283
1284    #[test]
1285    fn get_port_defaults_without_port_or_instance() {
1286        // No explicit port and no instance -> default SQL Server port.
1287        let config = Config::new();
1288        assert_eq!(1433, config.get_port());
1289    }
1290
1291    #[test]
1292    fn get_port_uses_sql_browser_port_for_named_instance() {
1293        // A named instance without an explicit port -> SQL Browser port.
1294        let mut config = Config::new();
1295        config.instance_name("SQLEXPRESS");
1296        assert_eq!(1434, config.get_port());
1297    }
1298
1299    #[test]
1300    #[should_panic(expected = "mutual exclusive")]
1301    fn trust_cert_after_trust_cert_ca_panics() {
1302        let mut config = Config::new();
1303        config.trust_cert_ca("/tmp/ca.crt");
1304        config.trust_cert();
1305    }
1306
1307    #[test]
1308    #[should_panic(expected = "mutual exclusive")]
1309    fn trust_cert_ca_after_trust_cert_panics() {
1310        let mut config = Config::new();
1311        config.trust_cert();
1312        config.trust_cert_ca("/tmp/ca.crt");
1313    }
1314
1315    #[test]
1316    fn trust_cert_ca_sets_ca_location() {
1317        let mut config = Config::new();
1318        config.trust_cert_ca("/tmp/ca.crt");
1319        assert!(matches!(
1320            config.trust,
1321            TrustConfig::CaCertificateLocation(_)
1322        ));
1323    }
1324
1325    #[test]
1326    fn config_builder_covers_all_setters() {
1327        let config = Config::builder()
1328            .host("localhost")
1329            .instance_name("SQLEXPRESS")
1330            .encryption(EncryptionLevel::Off)
1331            .trust_cert_ca("/tmp/ca.crt")
1332            .build();
1333
1334        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1335        assert!(matches!(config.encryption, EncryptionLevel::Off));
1336        assert!(matches!(
1337            config.trust,
1338            TrustConfig::CaCertificateLocation(_)
1339        ));
1340    }
1341
1342    #[test]
1343    fn config_builder_trust_cert_sets_trust_all() {
1344        let config = Config::builder().trust_cert().build();
1345        assert!(matches!(config.trust, TrustConfig::TrustAll));
1346    }
1347
1348    #[test]
1349    #[should_panic(expected = "mutual exclusive")]
1350    fn config_builder_trust_cert_after_ca_panics() {
1351        Config::builder().trust_cert_ca("/tmp/ca.crt").trust_cert();
1352    }
1353
1354    #[test]
1355    #[should_panic(expected = "mutual exclusive")]
1356    fn config_builder_trust_cert_ca_after_trust_cert_panics() {
1357        Config::builder().trust_cert().trust_cert_ca("/tmp/ca.crt");
1358    }
1359
1360    #[test]
1361    fn from_ado_string_populates_optional_fields() {
1362        let config = Config::from_ado_string(
1363            "server=tcp:my-server.com\\SQLEXPRESS;database=northwind;\
1364             HostNameInCertificate=cert.host;WorkstationID=ws-1",
1365        )
1366        .expect("valid ado string");
1367
1368        assert_eq!("my-server.com", config.get_host());
1369        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1370        assert_eq!(Some("northwind"), config.database.as_deref());
1371        assert_eq!(Some("cert.host"), config.hostname_in_certificate.as_deref());
1372        assert_eq!(Some("ws-1"), config.client_name.as_deref());
1373    }
1374
1375    #[cfg(any(
1376        feature = "rustls",
1377        feature = "native-tls",
1378        feature = "vendored-openssl"
1379    ))]
1380    #[test]
1381    fn client_cert_source_debug_formats_cert_and_key() {
1382        let mut config = Config::new();
1383        config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1384
1385        let dbg = format!("{:?}", config.get_client_certificate().unwrap().source);
1386        assert!(dbg.contains("CertAndKey"));
1387        assert!(dbg.contains("client.pem"));
1388        assert!(dbg.contains("client.key"));
1389    }
1390
1391    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1392    #[test]
1393    fn config_builder_sets_pkcs12_client_certificate() {
1394        let config = Config::builder()
1395            .client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t")
1396            .build();
1397
1398        match &config
1399            .get_client_certificate()
1400            .expect("client certificate should be set")
1401            .source
1402        {
1403            ClientCertSource::Pkcs12 { path, password } => {
1404                assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1405                assert_eq!(password.as_str(), "s3cr3t");
1406            }
1407            other => panic!("expected Pkcs12 source, got {other:?}"),
1408        }
1409    }
1410
1411    #[cfg(all(unix, feature = "sspi-rs"))]
1412    #[test]
1413    fn ado_integrated_security_sspi_with_partial_credentials_uses_windows() {
1414        // Only a username (no password) -> falls into the catch-all NTLM arm.
1415        let config = Config::from_ado_string(
1416            "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=onlyuser",
1417        )
1418        .unwrap();
1419
1420        match &config.auth {
1421            AuthMethod::Windows(auth) => {
1422                assert_eq!("onlyuser", auth.user);
1423            }
1424            other => panic!("expected Windows auth, got {other:?}"),
1425        }
1426    }
1427
1428    #[test]
1429    fn trust_cert_with_trust_cert_ca_is_an_error_not_a_panic() {
1430        let res = Config::from_ado_string(
1431            "server=tcp:h,1433;TrustServerCertificate=true;TrustServerCertificateCA=ca.pem",
1432        );
1433        assert!(matches!(res, Err(crate::Error::Conversion(_))));
1434
1435        let res = Config::from_jdbc_string(
1436            "jdbc:sqlserver://h:1433;trustServerCertificate=true;TrustServerCertificateCA=ca.pem",
1437        );
1438        assert!(matches!(res, Err(crate::Error::Conversion(_))));
1439    }
1440
1441    #[cfg(feature = "tds80")]
1442    #[test]
1443    fn strict_ignores_trust_server_certificate() {
1444        let config =
1445            Config::from_ado_string("server=tcp:h,1433;Encrypt=Strict;TrustServerCertificate=true")
1446                .unwrap();
1447        assert!(matches!(config.trust, TrustConfig::Default));
1448
1449        // The CA is still honoured.
1450        let config = Config::from_ado_string(
1451            "server=tcp:h,1433;encrypt=strict;TrustServerCertificate=yes;TrustServerCertificateCA=ca.pem",
1452        )
1453        .unwrap();
1454        assert!(matches!(
1455            config.trust,
1456            TrustConfig::CaCertificateLocation(_)
1457        ));
1458
1459        // Set through the API, it is ignored when TLS is set up.
1460        let mut config = Config::new();
1461        config.trust_cert();
1462        assert!(matches!(config.tls_trust(), TrustConfig::TrustAll));
1463        config.encryption(EncryptionLevel::Strict);
1464        assert!(matches!(config.tls_trust(), TrustConfig::Default));
1465    }
1466
1467    #[test]
1468    fn connect_timeout_defaults_and_setters() {
1469        use std::time::Duration;
1470        assert_eq!(
1471            Config::new().connect_timeout(),
1472            Some(Duration::from_secs(15))
1473        );
1474
1475        let mut config = Config::new();
1476        config.set_connect_timeout(None);
1477        assert_eq!(config.connect_timeout(), None);
1478
1479        let config = Config::builder()
1480            .connect_timeout(Some(Duration::from_secs(3)))
1481            .build();
1482        assert_eq!(config.connect_timeout(), Some(Duration::from_secs(3)));
1483
1484        let config = Config::from_ado_string("server=tcp:h,1433;Connect Timeout=0").unwrap();
1485        assert_eq!(config.connect_timeout(), None);
1486        let config = Config::from_jdbc_string("jdbc:sqlserver://h;loginTimeout=9").unwrap();
1487        assert_eq!(config.connect_timeout(), Some(Duration::from_secs(9)));
1488    }
1489}