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]
722 fn test_login7_read_only_intent_bit() {
723 let read_only = Login7::new()
724 .with_sql_auth("u", "p")
725 .with_read_only_intent(true);
726 assert_eq!(read_only.type_flags.to_byte() & 0x20, 0x20);
727 let encoded = read_only.encode();
728 assert_eq!(
729 encoded[26] & 0x20,
730 0x20,
731 "READONLY_INTENT must be set in the encoded TypeFlags byte"
732 );
733
734 let read_write = Login7::new().with_sql_auth("u", "p");
735 assert_eq!(read_write.type_flags.to_byte() & 0x20, 0);
736 let encoded = read_write.encode();
737 assert_eq!(
738 encoded[26] & 0x20,
739 0,
740 "READONLY_INTENT must be clear by default"
741 );
742 }
743
744 #[test]
745 fn test_password_obfuscation() {
746 let mut buf = BytesMut::new();
748 Login7::write_obfuscated_password(&mut buf, "a");
749
750 assert_eq!(buf.len(), 2);
755 assert_eq!(buf[0], 0xB3);
756 assert_eq!(buf[1], 0xA5);
757 }
758
759 #[test]
773 fn test_login7_feature_extension_pointer_indirection() {
774 let login = Login7::new()
775 .with_hostname("HOST")
776 .with_sql_auth("u", "p")
777 .with_database("db")
778 .with_app_name("app")
779 .with_feature(FeatureExtension {
780 feature_id: FeatureId::ColumnEncryption,
781 data: Bytes::from_static(&[0x01]),
782 });
783
784 let encoded = login.encode();
785 assert!(encoded.len() >= LOGIN7_HEADER_SIZE);
786
787 assert_eq!(
802 encoded[27] & 0x10,
803 0x10,
804 "option_flags3.extension bit must be set"
805 );
806
807 const OFFSET_TABLE_START: usize = 36;
816 const EXTENSION_SLOT: usize = OFFSET_TABLE_START + 5 * 4; let ib_extension =
818 u16::from_le_bytes([encoded[EXTENSION_SLOT], encoded[EXTENSION_SLOT + 1]]) as usize;
819 let cb_extension =
820 u16::from_le_bytes([encoded[EXTENSION_SLOT + 2], encoded[EXTENSION_SLOT + 3]]);
821 assert_eq!(cb_extension, 4, "cbExtension must be 4 per MS-TDS §2.2.6.4");
822
823 assert!(
825 ib_extension + 4 <= encoded.len(),
826 "ibExtension out of bounds"
827 );
828
829 let feature_ext_offset = u32::from_le_bytes([
833 encoded[ib_extension],
834 encoded[ib_extension + 1],
835 encoded[ib_extension + 2],
836 encoded[ib_extension + 3],
837 ]) as usize;
838 assert!(
839 feature_ext_offset + 6 <= encoded.len(), "FeatureExt offset {feature_ext_offset} out of bounds (packet is {} bytes)",
841 encoded.len()
842 );
843 assert_eq!(
844 encoded[feature_ext_offset], 0x04,
845 "first byte of FeatureExt block should be FeatureId::ColumnEncryption (0x04)"
846 );
847 let data_len = u32::from_le_bytes([
848 encoded[feature_ext_offset + 1],
849 encoded[feature_ext_offset + 2],
850 encoded[feature_ext_offset + 3],
851 encoded[feature_ext_offset + 4],
852 ]);
853 assert_eq!(data_len, 1, "ColumnEncryption version payload is 1 byte");
854 assert_eq!(
855 encoded[feature_ext_offset + 5],
856 0x01,
857 "ColumnEncryption payload is version byte 0x01"
858 );
859 assert_eq!(
860 encoded[feature_ext_offset + 6],
861 0xFF,
862 "FeatureExt stream terminator 0xFF must follow"
863 );
864
865 assert!(
870 ib_extension < feature_ext_offset,
871 "ibExtension ({ib_extension}) must point at the u32 pointer, \
872 which lives before FeatureExt data ({feature_ext_offset})"
873 );
874 }
875
876 #[test]
881 fn test_login7_sspi_small_inline() {
882 let sspi = vec![0xABu8; 200];
883 let e = Login7::new().with_integrated_auth(sspi.clone()).encode();
884
885 let sspi_offset = u16::from_le_bytes([e[78], e[79]]) as usize;
886 let cb_sspi = u16::from_le_bytes([e[80], e[81]]);
887 let cb_sspi_long = u32::from_le_bytes([e[90], e[91], e[92], e[93]]);
888
889 assert_eq!(cb_sspi, 200, "small SSPI length goes inline in cbSSPI");
890 assert_eq!(
891 cb_sspi_long, 0,
892 "cbSSPILong is unused for a small SSPI blob"
893 );
894 assert_eq!(
895 &e[sspi_offset..sspi_offset + 200],
896 &sspi[..],
897 "SSPI bytes round-trip at sspi_offset"
898 );
899 }
900
901 #[test]
907 fn test_login7_sspi_long_indirection() {
908 let sspi = vec![0x5Au8; 70_000];
909 let e = Login7::new()
910 .with_integrated_auth(sspi.clone())
911 .with_feature(FeatureExtension {
912 feature_id: FeatureId::ColumnEncryption,
913 data: Bytes::from_static(&[0x01]),
914 })
915 .encode();
916
917 let sspi_offset = u16::from_le_bytes([e[78], e[79]]) as usize;
918 let cb_sspi = u16::from_le_bytes([e[80], e[81]]);
919 let cb_sspi_long = u32::from_le_bytes([e[90], e[91], e[92], e[93]]);
920
921 assert_eq!(
922 cb_sspi, 0xFFFF,
923 "cbSSPI must be the 0xFFFF sentinel for a >USHORT SSPI blob"
924 );
925 assert_eq!(
926 cb_sspi_long, 70_000,
927 "cbSSPILong carries the real SSPI length"
928 );
929 assert_eq!(
930 &e[sspi_offset..sspi_offset + 70_000],
931 &sspi[..],
932 "large SSPI bytes round-trip at sspi_offset"
933 );
934
935 let total = u32::from_le_bytes([e[0], e[1], e[2], e[3]]) as usize;
937 assert_eq!(total, e.len(), "Length field matches encoded size");
938
939 const EXTENSION_SLOT: usize = 36 + 5 * 4;
941 let ib_extension = u16::from_le_bytes([e[EXTENSION_SLOT], e[EXTENSION_SLOT + 1]]) as usize;
942 let feature_ext_offset = u32::from_le_bytes([
943 e[ib_extension],
944 e[ib_extension + 1],
945 e[ib_extension + 2],
946 e[ib_extension + 3],
947 ]) as usize;
948 assert!(
949 feature_ext_offset > 70_000,
950 "FeatureExt block must land past the SSPI blob (offset {feature_ext_offset})"
951 );
952 assert_eq!(
953 e[feature_ext_offset], 0x04,
954 "FeatureExt block starts with the ColumnEncryption id"
955 );
956 assert_eq!(
957 e[feature_ext_offset + 5],
958 0x01,
959 "ColumnEncryption version byte"
960 );
961 assert_eq!(e[feature_ext_offset + 6], 0xFF, "FeatureExt terminator");
962 }
963
964 #[test]
968 fn test_login7_sspi_length_boundary() {
969 let e = Login7::new()
970 .with_integrated_auth(vec![0u8; 0xFFFE])
971 .encode();
972 assert_eq!(
973 u16::from_le_bytes([e[80], e[81]]),
974 0xFFFE,
975 "0xFFFE stays inline"
976 );
977 assert_eq!(
978 u32::from_le_bytes([e[90], e[91], e[92], e[93]]),
979 0,
980 "cbSSPILong unused at 0xFFFE"
981 );
982
983 let e = Login7::new()
984 .with_integrated_auth(vec![0u8; 0xFFFF])
985 .encode();
986 assert_eq!(
987 u16::from_le_bytes([e[80], e[81]]),
988 0xFFFF,
989 "0xFFFF switches to the sentinel"
990 );
991 assert_eq!(
992 u32::from_le_bytes([e[90], e[91], e[92], e[93]]),
993 0xFFFF,
994 "cbSSPILong carries the real length at the boundary"
995 );
996 }
997
998 #[test]
999 fn test_option_flags() {
1000 let flags1 = OptionFlags1::default();
1001 assert_eq!(flags1.to_byte(), 0x00);
1002
1003 let flags2 = OptionFlags2 {
1004 odbc: true,
1005 integrated_security: true,
1006 ..Default::default()
1007 };
1008 assert_eq!(flags2.to_byte(), 0x82);
1009
1010 let flags3 = OptionFlags3 {
1011 extension: true,
1012 ..Default::default()
1013 };
1014 assert_eq!(flags3.to_byte(), 0x10);
1015 }
1016}