1use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::codec::write_utf16_string;
21use crate::prelude::*;
22use crate::version::TdsVersion;
23
24pub const LOGIN7_HEADER_SIZE: usize = 94;
26
27#[derive(Debug, Clone, Copy, Default)]
29pub struct OptionFlags1 {
30 pub byte_order_be: bool,
32 pub char_ebcdic: bool,
34 pub float_ieee: bool,
36 pub dump_load_off: bool,
38 pub use_db_notify: bool,
40 pub database_fatal: bool,
42 pub set_lang_warn: bool,
44}
45
46impl OptionFlags1 {
47 #[must_use]
58 pub fn to_byte(&self) -> u8 {
59 let mut flags = 0u8;
60 if self.byte_order_be {
61 flags |= 0x01; }
63 if self.char_ebcdic {
64 flags |= 0x02; }
66 if self.dump_load_off {
69 flags |= 0x10; }
71 if self.use_db_notify {
72 flags |= 0x20; }
74 if self.database_fatal {
75 flags |= 0x40; }
77 if self.set_lang_warn {
78 flags |= 0x80; }
80 flags
81 }
82}
83
84#[derive(Debug, Clone, Copy, Default)]
86pub struct OptionFlags2 {
87 pub language_fatal: bool,
89 pub odbc: bool,
91 pub tran_boundary: bool,
93 pub cache_connect: bool,
95 pub user_type: u8,
97 pub integrated_security: bool,
99}
100
101impl OptionFlags2 {
102 #[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#[derive(Debug, Clone, Copy, Default)]
128pub struct TypeFlags {
129 pub sql_type: u8,
131 pub oledb: bool,
133 pub read_only_intent: bool,
135}
136
137impl TypeFlags {
138 #[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#[derive(Debug, Clone, Copy, Default)]
155pub struct OptionFlags3 {
156 pub change_password: bool,
158 pub user_instance: bool,
160 pub send_yukon_binary_xml: bool,
162 pub unknown_collation_handling: bool,
164 pub extension: bool,
166}
167
168impl OptionFlags3 {
169 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[repr(u8)]
195#[non_exhaustive]
196pub enum FeatureId {
197 SessionRecovery = 0x01,
199 FedAuth = 0x02,
201 ColumnEncryption = 0x04,
203 GlobalTransactions = 0x05,
205 AzureSqlSupport = 0x08,
207 DataClassification = 0x09,
209 Utf8Support = 0x0A,
211 AzureSqlDnsCaching = 0x0B,
213 Terminator = 0xFF,
215}
216
217#[derive(Debug, Clone)]
219pub struct Login7 {
220 pub tds_version: TdsVersion,
222 pub packet_size: u32,
224 pub client_prog_version: u32,
226 pub client_pid: u32,
228 pub connection_id: u32,
230 pub option_flags1: OptionFlags1,
232 pub option_flags2: OptionFlags2,
234 pub type_flags: TypeFlags,
236 pub option_flags3: OptionFlags3,
238 pub client_timezone: i32,
240 pub client_lcid: u32,
242 pub hostname: String,
244 pub username: String,
246 pub password: String,
248 pub app_name: String,
250 pub server_name: String,
252 pub unused: String,
254 pub library_name: String,
256 pub language: String,
258 pub database: String,
260 pub client_id: [u8; 6],
262 pub sspi_data: Vec<u8>,
264 pub attach_db_file: String,
266 pub new_password: String,
268 pub features: Vec<FeatureExtension>,
270}
271
272#[derive(Debug, Clone)]
274pub struct FeatureExtension {
275 pub feature_id: FeatureId,
277 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 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(), option_flags3: OptionFlags3 {
307 unknown_collation_handling: true,
308 ..Default::default()
309 },
310 client_timezone: 0,
311 client_lcid: 0x0409, 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 #[must_use]
333 pub fn new() -> Self {
334 Self::default()
335 }
336
337 #[must_use]
339 pub fn with_tds_version(mut self, version: TdsVersion) -> Self {
340 self.tds_version = version;
341 self
342 }
343
344 #[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 #[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 #[must_use]
367 pub fn with_database(mut self, database: impl Into<String>) -> Self {
368 self.database = database.into();
369 self
370 }
371
372 #[must_use]
374 pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
375 self.hostname = hostname.into();
376 self
377 }
378
379 #[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 #[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 #[must_use]
398 pub fn with_language(mut self, language: impl Into<String>) -> Self {
399 self.language = language.into();
400 self
401 }
402
403 #[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 #[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 #[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 #[must_use]
427 pub fn encode(&self) -> Bytes {
428 let mut buf = BytesMut::with_capacity(512);
429
430 let mut offset: u32 = LOGIN7_HEADER_SIZE as u32;
439
440 let hostname_len = self.hostname.encode_utf16().count() as u16;
442 let username_len = self.username.encode_utf16().count() as u16;
443 let password_len = self.password.encode_utf16().count() as u16;
444 let app_name_len = self.app_name.encode_utf16().count() as u16;
445 let server_name_len = self.server_name.encode_utf16().count() as u16;
446 let unused_len = self.unused.encode_utf16().count() as u16;
447 let library_name_len = self.library_name.encode_utf16().count() as u16;
448 let language_len = self.language.encode_utf16().count() as u16;
449 let database_len = self.database.encode_utf16().count() as u16;
450 let sspi_len = self.sspi_data.len();
451 let attach_db_len = self.attach_db_file.encode_utf16().count() as u16;
452 let new_password_len = self.new_password.encode_utf16().count() as u16;
453
454 let (cb_sspi, cb_sspi_long): (u16, u32) = if sspi_len >= 0xFFFF {
461 (0xFFFF, sspi_len as u32)
462 } else {
463 (sspi_len as u16, 0)
464 };
465
466 let mut var_data = BytesMut::new();
468
469 let hostname_offset = offset;
471 write_utf16_string(&mut var_data, &self.hostname);
472 offset += u32::from(hostname_len) * 2;
473
474 let username_offset = offset;
476 write_utf16_string(&mut var_data, &self.username);
477 offset += u32::from(username_len) * 2;
478
479 let password_offset = offset;
481 Self::write_obfuscated_password(&mut var_data, &self.password);
482 offset += u32::from(password_len) * 2;
483
484 let app_name_offset = offset;
486 write_utf16_string(&mut var_data, &self.app_name);
487 offset += u32::from(app_name_len) * 2;
488
489 let server_name_offset = offset;
491 write_utf16_string(&mut var_data, &self.server_name);
492 offset += u32::from(server_name_len) * 2;
493
494 let extension_offset = if self.option_flags3.extension {
511 let feature_data_offset = offset
514 + 4 + u32::from(library_name_len) * 2
516 + u32::from(language_len) * 2
517 + u32::from(database_len) * 2
518 + sspi_len as u32
519 + u32::from(attach_db_len) * 2
520 + u32::from(new_password_len) * 2;
521 let pointer_offset = offset;
522 var_data.put_u32_le(feature_data_offset);
525 offset += 4;
526 pointer_offset
527 } else {
528 let unused_offset = offset;
529 write_utf16_string(&mut var_data, &self.unused);
530 offset += u32::from(unused_len) * 2;
531 unused_offset
532 };
533
534 let library_name_offset = offset;
536 write_utf16_string(&mut var_data, &self.library_name);
537 offset += u32::from(library_name_len) * 2;
538
539 let language_offset = offset;
541 write_utf16_string(&mut var_data, &self.language);
542 offset += u32::from(language_len) * 2;
543
544 let database_offset = offset;
546 write_utf16_string(&mut var_data, &self.database);
547 offset += u32::from(database_len) * 2;
548
549 let sspi_offset = offset;
554 var_data.put_slice(&self.sspi_data);
555 offset += sspi_len as u32;
556
557 let attach_db_offset = offset;
559 write_utf16_string(&mut var_data, &self.attach_db_file);
560 offset += u32::from(attach_db_len) * 2;
561
562 let new_password_offset = offset;
564 if !self.new_password.is_empty() {
565 Self::write_obfuscated_password(&mut var_data, &self.new_password);
566 }
567 #[allow(unused_assignments)]
568 {
569 offset += u32::from(new_password_len) * 2;
570 }
571
572 debug_assert!(
579 attach_db_len == 0 || attach_db_offset <= u32::from(u16::MAX),
580 "attach_db offset {attach_db_offset} exceeds USHORT with a non-empty field",
581 );
582 debug_assert!(
583 new_password_len == 0 || new_password_offset <= u32::from(u16::MAX),
584 "change_password offset {new_password_offset} exceeds USHORT with a non-empty field",
585 );
586
587 if self.option_flags3.extension {
589 for feature in &self.features {
590 var_data.put_u8(feature.feature_id as u8);
591 var_data.put_u32_le(feature.data.len() as u32);
592 var_data.put_slice(&feature.data);
593 }
594 var_data.put_u8(FeatureId::Terminator as u8);
595 }
596
597 let total_length = LOGIN7_HEADER_SIZE + var_data.len();
599
600 buf.put_u32_le(total_length as u32); buf.put_u32_le(self.tds_version.raw()); buf.put_u32_le(self.packet_size); buf.put_u32_le(self.client_prog_version); buf.put_u32_le(self.client_pid); buf.put_u32_le(self.connection_id); buf.put_u8(self.option_flags1.to_byte());
610 buf.put_u8(self.option_flags2.to_byte());
611 buf.put_u8(self.type_flags.to_byte());
612 buf.put_u8(self.option_flags3.to_byte());
613
614 buf.put_i32_le(self.client_timezone); buf.put_u32_le(self.client_lcid); buf.put_u16_le(hostname_offset as u16);
619 buf.put_u16_le(hostname_len);
620 buf.put_u16_le(username_offset as u16);
621 buf.put_u16_le(username_len);
622 buf.put_u16_le(password_offset as u16);
623 buf.put_u16_le(password_len);
624 buf.put_u16_le(app_name_offset as u16);
625 buf.put_u16_le(app_name_len);
626 buf.put_u16_le(server_name_offset as u16);
627 buf.put_u16_le(server_name_len);
628
629 if self.option_flags3.extension {
631 buf.put_u16_le(extension_offset as u16);
632 buf.put_u16_le(4); } else {
634 buf.put_u16_le(extension_offset as u16);
635 buf.put_u16_le(unused_len);
636 }
637
638 buf.put_u16_le(library_name_offset as u16);
639 buf.put_u16_le(library_name_len);
640 buf.put_u16_le(language_offset as u16);
641 buf.put_u16_le(language_len);
642 buf.put_u16_le(database_offset as u16);
643 buf.put_u16_le(database_len);
644
645 buf.put_slice(&self.client_id);
647
648 buf.put_u16_le(sspi_offset as u16);
649 buf.put_u16_le(cb_sspi);
650 buf.put_u16_le(attach_db_offset as u16);
651 buf.put_u16_le(attach_db_len);
652 buf.put_u16_le(new_password_offset as u16);
653 buf.put_u16_le(new_password_len);
654
655 buf.put_u32_le(cb_sspi_long);
658
659 buf.put_slice(&var_data);
661
662 buf.freeze()
663 }
664
665 fn write_obfuscated_password(dst: &mut impl BufMut, password: &str) {
670 for c in password.encode_utf16() {
671 let low = (c & 0xFF) as u8;
672 let high = ((c >> 8) & 0xFF) as u8;
673
674 let low_enc = low.rotate_right(4) ^ 0xA5;
677 let high_enc = high.rotate_right(4) ^ 0xA5;
678
679 dst.put_u8(low_enc);
680 dst.put_u8(high_enc);
681 }
682 }
683}
684
685#[cfg(test)]
686#[allow(clippy::unwrap_used)]
687mod tests {
688 use super::*;
689
690 #[test]
691 fn test_login7_default() {
692 let login = Login7::new();
693 assert_eq!(login.tds_version, TdsVersion::V7_4);
694 assert_eq!(login.packet_size, 4096);
695 assert!(login.option_flags2.odbc);
696 }
697
698 #[test]
699 fn test_login7_encode() {
700 let login = Login7::new()
701 .with_hostname("TESTHOST")
702 .with_sql_auth("testuser", "testpass")
703 .with_database("testdb")
704 .with_app_name("TestApp");
705
706 let encoded = login.encode();
707
708 assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
710
711 let tds_version = u32::from_le_bytes([encoded[4], encoded[5], encoded[6], encoded[7]]);
713 assert_eq!(tds_version, TdsVersion::V7_4.raw());
714 }
715
716 #[test]
717 fn test_login7_encodes_hostname_and_language_fields() {
718 fn utf16le(s: &str) -> Vec<u8> {
719 s.encode_utf16().flat_map(u16::to_le_bytes).collect()
720 }
721 fn contains(hay: &[u8], needle: &[u8]) -> bool {
722 hay.windows(needle.len()).any(|w| w == needle)
723 }
724
725 let encoded = Login7::new()
726 .with_hostname("TESTHOST")
727 .with_language("us_english")
728 .with_sql_auth("u", "p")
729 .encode();
730 assert!(
731 contains(&encoded, &utf16le("TESTHOST")),
732 "HostName field must be serialized into the LOGIN7 packet"
733 );
734 assert!(
735 contains(&encoded, &utf16le("us_english")),
736 "Language field must be serialized into the LOGIN7 packet"
737 );
738
739 let bare = Login7::new().with_sql_auth("u", "p").encode();
742 assert!(
743 !contains(&bare, &utf16le("TESTHOST")),
744 "HostName must be absent when unset"
745 );
746 assert!(
747 !contains(&bare, &utf16le("us_english")),
748 "Language must be absent when unset"
749 );
750 }
751
752 #[test]
758 fn test_login7_read_only_intent_bit() {
759 let read_only = Login7::new()
760 .with_sql_auth("u", "p")
761 .with_read_only_intent(true);
762 assert_eq!(read_only.type_flags.to_byte() & 0x20, 0x20);
763 let encoded = read_only.encode();
764 assert_eq!(
765 encoded[26] & 0x20,
766 0x20,
767 "READONLY_INTENT must be set in the encoded TypeFlags byte"
768 );
769
770 let read_write = Login7::new().with_sql_auth("u", "p");
771 assert_eq!(read_write.type_flags.to_byte() & 0x20, 0);
772 let encoded = read_write.encode();
773 assert_eq!(
774 encoded[26] & 0x20,
775 0,
776 "READONLY_INTENT must be clear by default"
777 );
778 }
779
780 #[test]
781 fn test_password_obfuscation() {
782 let mut buf = BytesMut::new();
784 Login7::write_obfuscated_password(&mut buf, "a");
785
786 assert_eq!(buf.len(), 2);
791 assert_eq!(buf[0], 0xB3);
792 assert_eq!(buf[1], 0xA5);
793 }
794
795 #[test]
809 fn test_login7_feature_extension_pointer_indirection() {
810 let login = Login7::new()
811 .with_hostname("HOST")
812 .with_sql_auth("u", "p")
813 .with_database("db")
814 .with_app_name("app")
815 .with_feature(FeatureExtension {
816 feature_id: FeatureId::ColumnEncryption,
817 data: Bytes::from_static(&[0x01]),
818 });
819
820 let encoded = login.encode();
821 assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
822
823 assert_eq!(
838 encoded[27] & 0x10,
839 0x10,
840 "option_flags3.extension bit must be set"
841 );
842
843 const OFFSET_TABLE_START: usize = 36;
852 const EXTENSION_SLOT: usize = OFFSET_TABLE_START + 5 * 4; let ib_extension =
854 u16::from_le_bytes([encoded[EXTENSION_SLOT], encoded[EXTENSION_SLOT + 1]]) as usize;
855 let cb_extension =
856 u16::from_le_bytes([encoded[EXTENSION_SLOT + 2], encoded[EXTENSION_SLOT + 3]]);
857 assert_eq!(cb_extension, 4, "cbExtension must be 4 per MS-TDS §2.2.6.4");
858
859 assert!(
861 ib_extension + 4 <= encoded.len(),
862 "ibExtension out of bounds"
863 );
864
865 let feature_ext_offset = u32::from_le_bytes([
869 encoded[ib_extension],
870 encoded[ib_extension + 1],
871 encoded[ib_extension + 2],
872 encoded[ib_extension + 3],
873 ]) as usize;
874 assert!(
875 feature_ext_offset + 6 <= encoded.len(), "FeatureExt offset {feature_ext_offset} out of bounds (packet is {} bytes)",
877 encoded.len()
878 );
879 assert_eq!(
880 encoded[feature_ext_offset], 0x04,
881 "first byte of FeatureExt block should be FeatureId::ColumnEncryption (0x04)"
882 );
883 let data_len = u32::from_le_bytes([
884 encoded[feature_ext_offset + 1],
885 encoded[feature_ext_offset + 2],
886 encoded[feature_ext_offset + 3],
887 encoded[feature_ext_offset + 4],
888 ]);
889 assert_eq!(data_len, 1, "ColumnEncryption version payload is 1 byte");
890 assert_eq!(
891 encoded[feature_ext_offset + 5],
892 0x01,
893 "ColumnEncryption payload is version byte 0x01"
894 );
895 assert_eq!(
896 encoded[feature_ext_offset + 6],
897 0xFF,
898 "FeatureExt stream terminator 0xFF must follow"
899 );
900
901 assert!(
906 ib_extension < feature_ext_offset,
907 "ibExtension ({ib_extension}) must point at the u32 pointer, \
908 which lives before FeatureExt data ({feature_ext_offset})"
909 );
910 }
911
912 #[test]
917 fn test_login7_sspi_small_inline() {
918 let sspi = vec![0xABu8; 200];
919 let e = Login7::new().with_integrated_auth(sspi.clone()).encode();
920
921 let sspi_offset = u16::from_le_bytes([e[78], e[79]]) as usize;
922 let cb_sspi = u16::from_le_bytes([e[80], e[81]]);
923 let cb_sspi_long = u32::from_le_bytes([e[90], e[91], e[92], e[93]]);
924
925 assert_eq!(cb_sspi, 200, "small SSPI length goes inline in cbSSPI");
926 assert_eq!(
927 cb_sspi_long, 0,
928 "cbSSPILong is unused for a small SSPI blob"
929 );
930 assert_eq!(
931 &e[sspi_offset..sspi_offset + 200],
932 &sspi[..],
933 "SSPI bytes round-trip at sspi_offset"
934 );
935 }
936
937 #[test]
943 fn test_login7_sspi_long_indirection() {
944 let sspi = vec![0x5Au8; 70_000];
945 let e = Login7::new()
946 .with_integrated_auth(sspi.clone())
947 .with_feature(FeatureExtension {
948 feature_id: FeatureId::ColumnEncryption,
949 data: Bytes::from_static(&[0x01]),
950 })
951 .encode();
952
953 let sspi_offset = u16::from_le_bytes([e[78], e[79]]) as usize;
954 let cb_sspi = u16::from_le_bytes([e[80], e[81]]);
955 let cb_sspi_long = u32::from_le_bytes([e[90], e[91], e[92], e[93]]);
956
957 assert_eq!(
958 cb_sspi, 0xFFFF,
959 "cbSSPI must be the 0xFFFF sentinel for a >USHORT SSPI blob"
960 );
961 assert_eq!(
962 cb_sspi_long, 70_000,
963 "cbSSPILong carries the real SSPI length"
964 );
965 assert_eq!(
966 &e[sspi_offset..sspi_offset + 70_000],
967 &sspi[..],
968 "large SSPI bytes round-trip at sspi_offset"
969 );
970
971 let total = u32::from_le_bytes([e[0], e[1], e[2], e[3]]) as usize;
973 assert_eq!(total, e.len(), "Length field matches encoded size");
974
975 const EXTENSION_SLOT: usize = 36 + 5 * 4;
977 let ib_extension = u16::from_le_bytes([e[EXTENSION_SLOT], e[EXTENSION_SLOT + 1]]) as usize;
978 let feature_ext_offset = u32::from_le_bytes([
979 e[ib_extension],
980 e[ib_extension + 1],
981 e[ib_extension + 2],
982 e[ib_extension + 3],
983 ]) as usize;
984 assert!(
985 feature_ext_offset > 70_000,
986 "FeatureExt block must land past the SSPI blob (offset {feature_ext_offset})"
987 );
988 assert_eq!(
989 e[feature_ext_offset], 0x04,
990 "FeatureExt block starts with the ColumnEncryption id"
991 );
992 assert_eq!(
993 e[feature_ext_offset + 5],
994 0x01,
995 "ColumnEncryption version byte"
996 );
997 assert_eq!(e[feature_ext_offset + 6], 0xFF, "FeatureExt terminator");
998 }
999
1000 #[test]
1004 fn test_login7_sspi_length_boundary() {
1005 let e = Login7::new()
1006 .with_integrated_auth(vec![0u8; 0xFFFE])
1007 .encode();
1008 assert_eq!(
1009 u16::from_le_bytes([e[80], e[81]]),
1010 0xFFFE,
1011 "0xFFFE stays inline"
1012 );
1013 assert_eq!(
1014 u32::from_le_bytes([e[90], e[91], e[92], e[93]]),
1015 0,
1016 "cbSSPILong unused at 0xFFFE"
1017 );
1018
1019 let e = Login7::new()
1020 .with_integrated_auth(vec![0u8; 0xFFFF])
1021 .encode();
1022 assert_eq!(
1023 u16::from_le_bytes([e[80], e[81]]),
1024 0xFFFF,
1025 "0xFFFF switches to the sentinel"
1026 );
1027 assert_eq!(
1028 u32::from_le_bytes([e[90], e[91], e[92], e[93]]),
1029 0xFFFF,
1030 "cbSSPILong carries the real length at the boundary"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_option_flags() {
1036 let flags1 = OptionFlags1::default();
1037 assert_eq!(flags1.to_byte(), 0x00);
1038
1039 let flags2 = OptionFlags2 {
1040 odbc: true,
1041 integrated_security: true,
1042 ..Default::default()
1043 };
1044 assert_eq!(flags2.to_byte(), 0x82);
1045
1046 let flags3 = OptionFlags3 {
1047 extension: true,
1048 ..Default::default()
1049 };
1050 assert_eq!(flags3.to_byte(), 0x10);
1051 }
1052}