Skip to main content

tds_protocol/
login7.rs

1//! TDS LOGIN7 packet construction.
2//!
3//! The LOGIN7 packet is sent by the client to authenticate with SQL Server.
4//! It contains client information, credentials, and feature negotiation data.
5//!
6//! ## Packet Structure
7//!
8//! The LOGIN7 packet has a complex structure with:
9//! - Fixed-length header (94 bytes)
10//! - Variable-length data section (strings are UTF-16LE)
11//! - Optional feature extension block
12//!
13//! ## Security Note
14//!
15//! The password is obfuscated (not encrypted) using a simple XOR + bit rotation.
16//! Always use TLS encryption for the connection.
17
18use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::codec::write_utf16_string;
21use crate::prelude::*;
22use crate::version::TdsVersion;
23
24/// LOGIN7 packet header size (fixed portion).
25pub const LOGIN7_HEADER_SIZE: usize = 94;
26
27/// LOGIN7 option flags 1.
28#[derive(Debug, Clone, Copy, Default)]
29pub struct OptionFlags1 {
30    /// Use big-endian byte order.
31    pub byte_order_be: bool,
32    /// Character set (0 = ASCII, 1 = EBCDIC).
33    pub char_ebcdic: bool,
34    /// Floating point representation (0 = IEEE 754, 1 = VAX, 2 = ND5000).
35    pub float_ieee: bool,
36    /// Dump/load off.
37    pub dump_load_off: bool,
38    /// Use DB notification.
39    pub use_db_notify: bool,
40    /// Database is fatal.
41    pub database_fatal: bool,
42    /// Set language warning.
43    pub set_lang_warn: bool,
44}
45
46impl OptionFlags1 {
47    /// Convert to byte.
48    ///
49    /// Per MS-TDS 2.2.6.3 LOGIN7, OptionFlags1 layout:
50    /// - bit 0: fByteOrder (0=little-endian, 1=big-endian)
51    /// - bit 1: fChar (0=ASCII, 1=EBCDIC)
52    /// - bits 2-3: fFloat (0=IEEE 754, 1=VAX, 2=ND5000)
53    /// - bit 4: fDumpLoad
54    /// - bit 5: fUseDB
55    /// - bit 6: fDatabase
56    /// - bit 7: fSetLang
57    #[must_use]
58    pub fn to_byte(&self) -> u8 {
59        let mut flags = 0u8;
60        if self.byte_order_be {
61            flags |= 0x01; // bit 0
62        }
63        if self.char_ebcdic {
64            flags |= 0x02; // bit 1
65        }
66        // Note: fFloat is bits 2-3, IEEE 754 = 0, so leave as 0 for IEEE
67        // float_ieee being true means we use IEEE (which is 0, the default)
68        if self.dump_load_off {
69            flags |= 0x10; // bit 4
70        }
71        if self.use_db_notify {
72            flags |= 0x20; // bit 5
73        }
74        if self.database_fatal {
75            flags |= 0x40; // bit 6
76        }
77        if self.set_lang_warn {
78            flags |= 0x80; // bit 7
79        }
80        flags
81    }
82}
83
84/// LOGIN7 option flags 2.
85#[derive(Debug, Clone, Copy, Default)]
86pub struct OptionFlags2 {
87    /// Language is fatal.
88    pub language_fatal: bool,
89    /// ODBC driver.
90    pub odbc: bool,
91    /// Obsolete: transaction boundary.
92    pub tran_boundary: bool,
93    /// Obsolete: cache connect.
94    pub cache_connect: bool,
95    /// User type (0 = Normal, 1 = Server, 2 = DQ login, 3 = Replication).
96    pub user_type: u8,
97    /// Integrated security.
98    pub integrated_security: bool,
99}
100
101impl OptionFlags2 {
102    /// Convert to byte.
103    #[must_use]
104    pub fn to_byte(&self) -> u8 {
105        let mut flags = 0u8;
106        if self.language_fatal {
107            flags |= 0x01;
108        }
109        if self.odbc {
110            flags |= 0x02;
111        }
112        if self.tran_boundary {
113            flags |= 0x04;
114        }
115        if self.cache_connect {
116            flags |= 0x08;
117        }
118        flags |= (self.user_type & 0x07) << 4;
119        if self.integrated_security {
120            flags |= 0x80;
121        }
122        flags
123    }
124}
125
126/// LOGIN7 type flags.
127#[derive(Debug, Clone, Copy, Default)]
128pub struct TypeFlags {
129    /// SQL type (0 = DFLT, 1 = TSQL).
130    pub sql_type: u8,
131    /// OLEDB driver.
132    pub oledb: bool,
133    /// Read-only intent.
134    pub read_only_intent: bool,
135}
136
137impl TypeFlags {
138    /// Convert to byte.
139    #[must_use]
140    pub fn to_byte(&self) -> u8 {
141        let mut flags = 0u8;
142        flags |= self.sql_type & 0x0F;
143        if self.oledb {
144            flags |= 0x10;
145        }
146        if self.read_only_intent {
147            flags |= 0x20;
148        }
149        flags
150    }
151}
152
153/// LOGIN7 option flags 3.
154#[derive(Debug, Clone, Copy, Default)]
155pub struct OptionFlags3 {
156    /// Change password.
157    pub change_password: bool,
158    /// User instance.
159    pub user_instance: bool,
160    /// Send YUKON binary XML.
161    pub send_yukon_binary_xml: bool,
162    /// Unknown collation handling.
163    pub unknown_collation_handling: bool,
164    /// Feature extension.
165    pub extension: bool,
166}
167
168impl OptionFlags3 {
169    /// Convert to byte.
170    #[must_use]
171    pub fn to_byte(&self) -> u8 {
172        let mut flags = 0u8;
173        if self.change_password {
174            flags |= 0x01;
175        }
176        if self.user_instance {
177            flags |= 0x02;
178        }
179        if self.send_yukon_binary_xml {
180            flags |= 0x04;
181        }
182        if self.unknown_collation_handling {
183            flags |= 0x08;
184        }
185        if self.extension {
186            flags |= 0x10;
187        }
188        flags
189    }
190}
191
192/// Feature extension types.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[repr(u8)]
195#[non_exhaustive]
196pub enum FeatureId {
197    /// Session recovery.
198    SessionRecovery = 0x01,
199    /// Federated authentication.
200    FedAuth = 0x02,
201    /// Column encryption.
202    ColumnEncryption = 0x04,
203    /// Global transactions.
204    GlobalTransactions = 0x05,
205    /// Azure SQL Support for DB.
206    AzureSqlSupport = 0x08,
207    /// Data classification.
208    DataClassification = 0x09,
209    /// UTF-8 support.
210    Utf8Support = 0x0A,
211    /// Azure SQL DNS Caching.
212    AzureSqlDnsCaching = 0x0B,
213    /// Terminator.
214    Terminator = 0xFF,
215}
216
217/// LOGIN7 packet builder.
218#[derive(Debug, Clone)]
219pub struct Login7 {
220    /// TDS version to request.
221    pub tds_version: TdsVersion,
222    /// Requested packet size.
223    pub packet_size: u32,
224    /// Client program version.
225    pub client_prog_version: u32,
226    /// Client process ID.
227    pub client_pid: u32,
228    /// Connection ID (for connection pooling).
229    pub connection_id: u32,
230    /// Option flags 1.
231    pub option_flags1: OptionFlags1,
232    /// Option flags 2.
233    pub option_flags2: OptionFlags2,
234    /// Type flags.
235    pub type_flags: TypeFlags,
236    /// Option flags 3.
237    pub option_flags3: OptionFlags3,
238    /// Client timezone offset in minutes.
239    pub client_timezone: i32,
240    /// Client LCID (locale ID).
241    pub client_lcid: u32,
242    /// Hostname (client machine name).
243    pub hostname: String,
244    /// Username for SQL authentication.
245    pub username: String,
246    /// Password for SQL authentication.
247    pub password: String,
248    /// Application name.
249    pub app_name: String,
250    /// Server name.
251    pub server_name: String,
252    /// Unused field.
253    pub unused: String,
254    /// Client library name.
255    pub library_name: String,
256    /// Language.
257    pub language: String,
258    /// Database name.
259    pub database: String,
260    /// Client ID (MAC address, typically zeros).
261    pub client_id: [u8; 6],
262    /// SSPI data for integrated authentication.
263    pub sspi_data: Vec<u8>,
264    /// Attach DB filename (for LocalDB).
265    pub attach_db_file: String,
266    /// New password (for password change).
267    pub new_password: String,
268    /// Feature extensions.
269    pub features: Vec<FeatureExtension>,
270}
271
272/// Feature extension data.
273#[derive(Debug, Clone)]
274pub struct FeatureExtension {
275    /// Feature ID.
276    pub feature_id: FeatureId,
277    /// Feature data.
278    pub data: Bytes,
279}
280
281impl Default for Login7 {
282    fn default() -> Self {
283        #[cfg(feature = "std")]
284        let client_pid = std::process::id();
285        #[cfg(not(feature = "std"))]
286        let client_pid = 0;
287
288        Self {
289            tds_version: TdsVersion::V7_4,
290            packet_size: 4096,
291            client_prog_version: 0,
292            client_pid,
293            connection_id: 0,
294            // Match Tiberius/standard SQL Server client flags
295            option_flags1: OptionFlags1 {
296                use_db_notify: true,
297                database_fatal: true,
298                ..Default::default()
299            },
300            option_flags2: OptionFlags2 {
301                language_fatal: true,
302                odbc: true,
303                ..Default::default()
304            },
305            type_flags: TypeFlags::default(), // TSQL type is in sql_type field
306            option_flags3: OptionFlags3 {
307                unknown_collation_handling: true,
308                ..Default::default()
309            },
310            client_timezone: 0,
311            client_lcid: 0x0409, // English (US)
312            hostname: String::new(),
313            username: String::new(),
314            password: String::new(),
315            app_name: String::from("rust-mssql-driver"),
316            server_name: String::new(),
317            unused: String::new(),
318            library_name: String::from("rust-mssql-driver"),
319            language: String::new(),
320            database: String::new(),
321            client_id: [0u8; 6],
322            sspi_data: Vec::new(),
323            attach_db_file: String::new(),
324            new_password: String::new(),
325            features: Vec::new(),
326        }
327    }
328}
329
330impl Login7 {
331    /// Create a new Login7 packet builder.
332    #[must_use]
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// Set the TDS version.
338    #[must_use]
339    pub fn with_tds_version(mut self, version: TdsVersion) -> Self {
340        self.tds_version = version;
341        self
342    }
343
344    /// Set SQL authentication credentials.
345    #[must_use]
346    pub fn with_sql_auth(
347        mut self,
348        username: impl Into<String>,
349        password: impl Into<String>,
350    ) -> Self {
351        self.username = username.into();
352        self.password = password.into();
353        self.option_flags2.integrated_security = false;
354        self
355    }
356
357    /// Enable integrated (Windows) authentication.
358    #[must_use]
359    pub fn with_integrated_auth(mut self, sspi_data: Vec<u8>) -> Self {
360        self.sspi_data = sspi_data;
361        self.option_flags2.integrated_security = true;
362        self
363    }
364
365    /// Set the database to connect to.
366    #[must_use]
367    pub fn with_database(mut self, database: impl Into<String>) -> Self {
368        self.database = database.into();
369        self
370    }
371
372    /// Set the hostname (client machine name).
373    #[must_use]
374    pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
375        self.hostname = hostname.into();
376        self
377    }
378
379    /// Set the application name.
380    #[must_use]
381    pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
382        self.app_name = app_name.into();
383        self
384    }
385
386    /// Set the server name.
387    #[must_use]
388    pub fn with_server_name(mut self, server_name: impl Into<String>) -> Self {
389        self.server_name = server_name.into();
390        self
391    }
392
393    /// Set the session language for server warning/error messages.
394    ///
395    /// The language name can be up to 128 characters (e.g., `"us_english"`).
396    /// When not set, the server uses its default language.
397    #[must_use]
398    pub fn with_language(mut self, language: impl Into<String>) -> Self {
399        self.language = language.into();
400        self
401    }
402
403    /// Set the packet size.
404    #[must_use]
405    pub fn with_packet_size(mut self, packet_size: u32) -> Self {
406        self.packet_size = packet_size;
407        self
408    }
409
410    /// Enable read-only intent for readable secondary connections.
411    #[must_use]
412    pub fn with_read_only_intent(mut self, read_only: bool) -> Self {
413        self.type_flags.read_only_intent = read_only;
414        self
415    }
416
417    /// Add a feature extension.
418    #[must_use]
419    pub fn with_feature(mut self, feature: FeatureExtension) -> Self {
420        self.option_flags3.extension = true;
421        self.features.push(feature);
422        self
423    }
424
425    /// Encode the LOGIN7 packet to bytes.
426    #[must_use]
427    pub fn encode(&self) -> Bytes {
428        let mut buf = BytesMut::with_capacity(512);
429
430        // Calculate variable data offsets
431        // Variable data starts after the 94-byte fixed header
432        let mut offset = LOGIN7_HEADER_SIZE as u16;
433
434        // Pre-calculate all UTF-16 lengths
435        let hostname_len = self.hostname.encode_utf16().count() as u16;
436        let username_len = self.username.encode_utf16().count() as u16;
437        let password_len = self.password.encode_utf16().count() as u16;
438        let app_name_len = self.app_name.encode_utf16().count() as u16;
439        let server_name_len = self.server_name.encode_utf16().count() as u16;
440        let unused_len = self.unused.encode_utf16().count() as u16;
441        let library_name_len = self.library_name.encode_utf16().count() as u16;
442        let language_len = self.language.encode_utf16().count() as u16;
443        let database_len = self.database.encode_utf16().count() as u16;
444        let sspi_len = self.sspi_data.len() as u16;
445        let attach_db_len = self.attach_db_file.encode_utf16().count() as u16;
446        let new_password_len = self.new_password.encode_utf16().count() as u16;
447
448        // Build variable data buffer
449        let mut var_data = BytesMut::new();
450
451        // Hostname
452        let hostname_offset = offset;
453        write_utf16_string(&mut var_data, &self.hostname);
454        offset += hostname_len * 2;
455
456        // Username
457        let username_offset = offset;
458        write_utf16_string(&mut var_data, &self.username);
459        offset += username_len * 2;
460
461        // Password (obfuscated)
462        let password_offset = offset;
463        Self::write_obfuscated_password(&mut var_data, &self.password);
464        offset += password_len * 2;
465
466        // App name
467        let app_name_offset = offset;
468        write_utf16_string(&mut var_data, &self.app_name);
469        offset += app_name_len * 2;
470
471        // Server name
472        let server_name_offset = offset;
473        write_utf16_string(&mut var_data, &self.server_name);
474        offset += server_name_len * 2;
475
476        // Unused / Feature extension pointer.
477        //
478        // Per MS-TDS §2.2.6.4, when fExtension is set, the variable-length
479        // slot that normally holds the `Unused` string is replaced by a
480        // 4-byte u32 containing the absolute offset of the FeatureExt data
481        // block. The offset/length table's `ibExtension` field points to
482        // THAT u32 (not to the FeatureExt data itself), and `cbExtension`
483        // is fixed at 4.
484        //
485        // The previously-shipped code set `ibExtension = base` (feature
486        // data offset) instead of the offset of the 4-byte pointer, causing
487        // SQL Server to read the first four bytes of our FeatureExt blob
488        // (e.g., `0x04, 0x01, 0x00, 0x00` for ColumnEncryption version 1)
489        // as a u32 offset — landing deep inside the hostname string and
490        // failing the LOGIN7 parse. The connection was dropped mid-handshake
491        // with no server-side diagnostic, which is why the bug sat unnoticed.
492        let extension_offset = if self.option_flags3.extension {
493            // Absolute offset where the FeatureExt block will land once all
494            // remaining var_data fields are written.
495            let feature_data_offset = offset
496                + 4 // the u32 pointer we're about to write
497                + library_name_len * 2
498                + language_len * 2
499                + database_len * 2
500                + sspi_len
501                + attach_db_len * 2
502                + new_password_len * 2;
503            let pointer_offset = offset;
504            // Write the u32 that ibExtension will point TO. Its value is the
505            // offset of the actual FeatureExt block.
506            var_data.put_u32_le(feature_data_offset as u32);
507            offset += 4;
508            pointer_offset
509        } else {
510            let unused_offset = offset;
511            write_utf16_string(&mut var_data, &self.unused);
512            offset += unused_len * 2;
513            unused_offset
514        };
515
516        // Library name
517        let library_name_offset = offset;
518        write_utf16_string(&mut var_data, &self.library_name);
519        offset += library_name_len * 2;
520
521        // Language
522        let language_offset = offset;
523        write_utf16_string(&mut var_data, &self.language);
524        offset += language_len * 2;
525
526        // Database
527        let database_offset = offset;
528        write_utf16_string(&mut var_data, &self.database);
529        offset += database_len * 2;
530
531        // Client ID (6 bytes)
532        // (Already handled in fixed header)
533
534        // SSPI
535        let sspi_offset = offset;
536        var_data.put_slice(&self.sspi_data);
537        offset += sspi_len;
538
539        // Attach DB file
540        let attach_db_offset = offset;
541        write_utf16_string(&mut var_data, &self.attach_db_file);
542        offset += attach_db_len * 2;
543
544        // Change password
545        let new_password_offset = offset;
546        if !self.new_password.is_empty() {
547            Self::write_obfuscated_password(&mut var_data, &self.new_password);
548        }
549        #[allow(unused_assignments)]
550        {
551            offset += new_password_len * 2;
552        }
553
554        // Feature extensions (if any)
555        if self.option_flags3.extension {
556            for feature in &self.features {
557                var_data.put_u8(feature.feature_id as u8);
558                var_data.put_u32_le(feature.data.len() as u32);
559                var_data.put_slice(&feature.data);
560            }
561            var_data.put_u8(FeatureId::Terminator as u8);
562        }
563
564        // Calculate total length
565        let total_length = LOGIN7_HEADER_SIZE + var_data.len();
566
567        // Write fixed header
568        buf.put_u32_le(total_length as u32); // Length
569        buf.put_u32_le(self.tds_version.raw()); // TDS version
570        buf.put_u32_le(self.packet_size); // Packet size
571        buf.put_u32_le(self.client_prog_version); // Client program version
572        buf.put_u32_le(self.client_pid); // Client PID
573        buf.put_u32_le(self.connection_id); // Connection ID
574
575        // Option flags
576        buf.put_u8(self.option_flags1.to_byte());
577        buf.put_u8(self.option_flags2.to_byte());
578        buf.put_u8(self.type_flags.to_byte());
579        buf.put_u8(self.option_flags3.to_byte());
580
581        buf.put_i32_le(self.client_timezone); // Client timezone
582        buf.put_u32_le(self.client_lcid); // Client LCID
583
584        // Variable length field offsets and lengths
585        buf.put_u16_le(hostname_offset);
586        buf.put_u16_le(hostname_len);
587        buf.put_u16_le(username_offset);
588        buf.put_u16_le(username_len);
589        buf.put_u16_le(password_offset);
590        buf.put_u16_le(password_len);
591        buf.put_u16_le(app_name_offset);
592        buf.put_u16_le(app_name_len);
593        buf.put_u16_le(server_name_offset);
594        buf.put_u16_le(server_name_len);
595
596        // Extension offset (or unused)
597        if self.option_flags3.extension {
598            buf.put_u16_le(extension_offset as u16);
599            buf.put_u16_le(4); // Size of offset pointer
600        } else {
601            buf.put_u16_le(extension_offset as u16);
602            buf.put_u16_le(unused_len);
603        }
604
605        buf.put_u16_le(library_name_offset);
606        buf.put_u16_le(library_name_len);
607        buf.put_u16_le(language_offset);
608        buf.put_u16_le(language_len);
609        buf.put_u16_le(database_offset);
610        buf.put_u16_le(database_len);
611
612        // Client ID (6 bytes)
613        buf.put_slice(&self.client_id);
614
615        buf.put_u16_le(sspi_offset);
616        buf.put_u16_le(sspi_len);
617        buf.put_u16_le(attach_db_offset);
618        buf.put_u16_le(attach_db_len);
619        buf.put_u16_le(new_password_offset);
620        buf.put_u16_le(new_password_len);
621
622        // SSPI Long (4 bytes, for SSPI > 65535 bytes)
623        buf.put_u32_le(0);
624
625        // Append variable data
626        buf.put_slice(&var_data);
627
628        buf.freeze()
629    }
630
631    /// Write password with TDS obfuscation.
632    ///
633    /// Per MS-TDS spec: For every byte in the password buffer, the client SHOULD first
634    /// swap the four high bits with the four low bits and then do a bit-XOR with 0xA5.
635    fn write_obfuscated_password(dst: &mut impl BufMut, password: &str) {
636        for c in password.encode_utf16() {
637            let low = (c & 0xFF) as u8;
638            let high = ((c >> 8) & 0xFF) as u8;
639
640            // Step 1: Swap nibbles (rotate by 4 bits)
641            // Step 2: XOR with 0xA5
642            let low_enc = low.rotate_right(4) ^ 0xA5;
643            let high_enc = high.rotate_right(4) ^ 0xA5;
644
645            dst.put_u8(low_enc);
646            dst.put_u8(high_enc);
647        }
648    }
649}
650
651#[cfg(test)]
652#[allow(clippy::unwrap_used)]
653mod tests {
654    use super::*;
655
656    #[test]
657    fn test_login7_default() {
658        let login = Login7::new();
659        assert_eq!(login.tds_version, TdsVersion::V7_4);
660        assert_eq!(login.packet_size, 4096);
661        assert!(login.option_flags2.odbc);
662    }
663
664    #[test]
665    fn test_login7_encode() {
666        let login = Login7::new()
667            .with_hostname("TESTHOST")
668            .with_sql_auth("testuser", "testpass")
669            .with_database("testdb")
670            .with_app_name("TestApp");
671
672        let encoded = login.encode();
673
674        // Check that the packet starts with a valid length
675        assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
676
677        // Check TDS version at offset 4 (after length)
678        let tds_version = u32::from_le_bytes([encoded[4], encoded[5], encoded[6], encoded[7]]);
679        assert_eq!(tds_version, TdsVersion::V7_4.raw());
680    }
681
682    #[test]
683    fn test_password_obfuscation() {
684        // Known test case: "a" should encode to specific bytes
685        let mut buf = BytesMut::new();
686        Login7::write_obfuscated_password(&mut buf, "a");
687
688        // 'a' = 0x0061 in UTF-16LE
689        // Per MS-TDS: swap nibbles FIRST, then XOR with 0xA5
690        // Low byte: 0x61 swap nibbles = 0x16, XOR 0xA5 = 0xB3
691        // High byte: 0x00 swap nibbles = 0x00, XOR 0xA5 = 0xA5
692        assert_eq!(buf.len(), 2);
693        assert_eq!(buf[0], 0xB3);
694        assert_eq!(buf[1], 0xA5);
695    }
696
697    /// Per MS-TDS §2.2.6.4, when `fExtension=1` the slot normally occupied
698    /// by `Unused` in the offset/length table becomes `ibExtension`/`cbExtension`:
699    ///   * `ibExtension` = absolute offset of a 4-byte u32.
700    ///   * `cbExtension` = 4 (fixed).
701    ///   * The u32 at that location = absolute offset of the FeatureExt data.
702    ///   * FeatureExt data = zero or more `(FeatureID u8, DataLen u32_le, Data)`
703    ///     triples, terminated by `0xFF`.
704    ///
705    /// Regression test for the bug where `ibExtension` was pointing directly
706    /// at the FeatureExt data (skipping the pointer indirection) AND the
707    /// pointer's value was off-by-4 (did not count the pointer slot itself).
708    /// Combined, SQL Server read the first four bytes of the FeatureExt blob
709    /// as a u32 offset and silently dropped the connection at LOGIN7.
710    #[test]
711    fn test_login7_feature_extension_pointer_indirection() {
712        let login = Login7::new()
713            .with_hostname("HOST")
714            .with_sql_auth("u", "p")
715            .with_database("db")
716            .with_app_name("app")
717            .with_feature(FeatureExtension {
718                feature_id: FeatureId::ColumnEncryption,
719                data: Bytes::from_static(&[0x01]),
720            });
721
722        let encoded = login.encode();
723        assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
724
725        // Fixed header layout, per the `encode()` method:
726        //   0..4   length (u32)
727        //   4..8   TDS version (u32)
728        //   8..12  packet size (u32)
729        //  12..16  client program version (u32)
730        //  16..20  client PID (u32)
731        //  20..24  connection ID (u32)
732        //  24     OptionFlags1
733        //  25     OptionFlags2
734        //  26     TypeFlags
735        //  27     OptionFlags3   <-- fExtension bit lives here
736        //  28..32 client timezone (i32)
737        //  32..36 client LCID (u32)
738        //  36..   offset/length table begins
739        assert_eq!(
740            encoded[27] & 0x10,
741            0x10,
742            "option_flags3.extension bit must be set"
743        );
744
745        // ibExtension/cbExtension live in the offset/length table right after
746        // server_name's (offset, len) pair:
747        //   0: hostname (off, len)       4 bytes
748        //   1: username (off, len)       4
749        //   2: password (off, len)       4
750        //   3: app_name (off, len)       4
751        //   4: server_name (off, len)    4
752        //   5: ibExtension, cbExtension  4   <-- what we want
753        const OFFSET_TABLE_START: usize = 36;
754        const EXTENSION_SLOT: usize = OFFSET_TABLE_START + 5 * 4; // = 56
755        let ib_extension =
756            u16::from_le_bytes([encoded[EXTENSION_SLOT], encoded[EXTENSION_SLOT + 1]]) as usize;
757        let cb_extension =
758            u16::from_le_bytes([encoded[EXTENSION_SLOT + 2], encoded[EXTENSION_SLOT + 3]]);
759        assert_eq!(cb_extension, 4, "cbExtension must be 4 per MS-TDS §2.2.6.4");
760
761        // ibExtension must point to a 4-byte region still inside the packet.
762        assert!(
763            ib_extension + 4 <= encoded.len(),
764            "ibExtension out of bounds"
765        );
766
767        // Dereference the pointer. That u32 is the absolute offset of the
768        // FeatureExt data, which must also land inside the packet and must
769        // start with our FeatureId (0x04 = ColumnEncryption).
770        let feature_ext_offset = u32::from_le_bytes([
771            encoded[ib_extension],
772            encoded[ib_extension + 1],
773            encoded[ib_extension + 2],
774            encoded[ib_extension + 3],
775        ]) as usize;
776        assert!(
777            feature_ext_offset + 6 <= encoded.len(), // 1 id + 4 len + 1 data + 0xFF terminator
778            "FeatureExt offset {feature_ext_offset} out of bounds (packet is {} bytes)",
779            encoded.len()
780        );
781        assert_eq!(
782            encoded[feature_ext_offset], 0x04,
783            "first byte of FeatureExt block should be FeatureId::ColumnEncryption (0x04)"
784        );
785        let data_len = u32::from_le_bytes([
786            encoded[feature_ext_offset + 1],
787            encoded[feature_ext_offset + 2],
788            encoded[feature_ext_offset + 3],
789            encoded[feature_ext_offset + 4],
790        ]);
791        assert_eq!(data_len, 1, "ColumnEncryption version payload is 1 byte");
792        assert_eq!(
793            encoded[feature_ext_offset + 5],
794            0x01,
795            "ColumnEncryption payload is version byte 0x01"
796        );
797        assert_eq!(
798            encoded[feature_ext_offset + 6],
799            0xFF,
800            "FeatureExt stream terminator 0xFF must follow"
801        );
802
803        // Belt-and-suspenders: ibExtension must NOT point at the FeatureExt
804        // data directly (that was the original bug — it skipped the u32
805        // pointer indirection). The pointer's own offset is strictly less
806        // than the feature data's offset.
807        assert!(
808            ib_extension < feature_ext_offset,
809            "ibExtension ({ib_extension}) must point at the u32 pointer, \
810             which lives before FeatureExt data ({feature_ext_offset})"
811        );
812    }
813
814    #[test]
815    fn test_option_flags() {
816        let flags1 = OptionFlags1::default();
817        assert_eq!(flags1.to_byte(), 0x00);
818
819        let flags2 = OptionFlags2 {
820            odbc: true,
821            integrated_security: true,
822            ..Default::default()
823        };
824        assert_eq!(flags2.to_byte(), 0x82);
825
826        let flags3 = OptionFlags3 {
827            extension: true,
828            ..Default::default()
829        };
830        assert_eq!(flags3.to_byte(), 0x10);
831    }
832}