@@ -158,6 +158,7 @@ pub enum TokenType {
158158
159159/// FedAuth feature extension ID
160160pub const FEATURE_EXT_FEDAUTH : u8 = 0x02 ;
161+ pub const FEATURE_EXT_USER_AGENT : u8 = 0x10 ;
161162/// FedAuth terminator
162163pub const FEATURE_EXT_TERMINATOR : u8 = 0xFF ;
163164
@@ -172,6 +173,8 @@ pub struct Login7AuthInfo {
172173 pub fedauth_library : u8 ,
173174 /// Server name from Login7 packet (the data source string sent by client)
174175 pub server_name : Option < String > ,
176+ /// User Agent string from Login7 packet (if present)
177+ pub user_agent : Option < String > ,
175178}
176179
177180/// Parse Login7 packet to extract FedAuth feature extension with access token
@@ -322,7 +325,23 @@ pub fn parse_login7_auth(packet_data: &[u8]) -> Login7AuthInfo {
322325 }
323326 }
324327 }
325- break ;
328+ } else if feature_id == FEATURE_EXT_USER_AGENT && i + 5 + feat_len <= data. len ( ) {
329+ let feat_data = & data[ i + 5 ..i + 5 + feat_len] ;
330+ if feat_data. len ( ) . is_multiple_of ( 2 ) {
331+ let u16_chars: Vec < u16 > = feat_data
332+ . chunks_exact ( 2 )
333+ . map ( |chunk| u16:: from_le_bytes ( [ chunk[ 0 ] , chunk[ 1 ] ] ) )
334+ . collect ( ) ;
335+ if let Ok ( user_agent) = String :: from_utf16 ( & u16_chars) {
336+ debug ! ( "Parsed UserAgent from Login7 (len: {})" , user_agent. len( ) ) ;
337+ auth_info. user_agent = Some ( user_agent) ;
338+ }
339+ } else {
340+ debug ! (
341+ "Skipping UserAgent FeatureExtension due to odd data length: {}" ,
342+ feat_len
343+ ) ;
344+ }
326345 }
327346
328347 // Skip to next feature entry
@@ -607,8 +626,15 @@ pub fn build_query_result(response: &crate::query_response::QueryResponse) -> By
607626 for col in & response. columns {
608627 result. put_u32_le ( 0 ) ; // UserType
609628 result. put_u16_le ( 0x0000 ) ; // Flags: not nullable, no special flags
610- result. put_u8 ( col. data_type . tds_type_code ( ) ) ; // Type code
611- result. put_u8 ( col. data_type . max_length ( ) ) ; // Max length
629+ result. put_u8 ( col. data_type . tds_type_code ( ) ) ;
630+ if col. data_type == crate :: query_response:: SqlDataType :: NVarChar {
631+ // Required to support string responses (e.g., @@USERAGENT).
632+ // TDS ColMetadata mandates a 5-byte collation suffix for variable-length types.
633+ result. put_u16_le ( 8000 ) ; // NVARCHAR(4000) max byte capacity
634+ result. put_slice ( & [ 0x09 , 0x04 , 0xD0 , 0x00 , 0x34 ] ) ; // SQL_Latin1_General_CP1_CI_AS
635+ } else {
636+ result. put_u8 ( col. data_type . max_length ( ) ) ;
637+ }
612638
613639 // Column name (UTF-16LE)
614640 let name_len = col. name . chars ( ) . count ( ) as u8 ;
@@ -883,4 +909,58 @@ mod tests {
883909 // After header, should have EnvChange token (0xE3)
884910 assert_eq ! ( response[ PACKET_HEADER_SIZE ] , TokenType :: EnvChange as u8 ) ;
885911 }
912+
913+ #[ test]
914+ fn test_parse_login7_user_agent ( ) {
915+ let user_agent = "TestAgent" ;
916+ let utf16_bytes: Vec < u8 > = user_agent
917+ . encode_utf16 ( )
918+ . flat_map ( |ch| ch. to_le_bytes ( ) . into_iter ( ) )
919+ . collect ( ) ;
920+
921+ let mut payload = BytesMut :: zeroed ( 100 ) ;
922+
923+ // OptionFlags3 at byte 27: set FeatureExt bit (0x10)
924+ payload[ 27 ] = 0x10 ;
925+
926+ // FeatureExt table entry at bytes 56-57 (points to offset 60)
927+ payload[ 56 ] = 60 ;
928+ payload[ 57 ] = 0 ;
929+
930+ // DWORD at byte 60 pointing to the actual feature data at byte 64
931+ payload[ 60 ..64 ] . copy_from_slice ( & 64u32 . to_le_bytes ( ) ) ;
932+
933+ // Truncate the buffer at 64 so we can append the feature bytes directly
934+ payload. truncate ( 64 ) ;
935+
936+ // Append UserAgent feature
937+ payload. put_u8 ( FEATURE_EXT_USER_AGENT ) ; // 0x10
938+ payload. put_u32_le ( utf16_bytes. len ( ) as u32 ) ;
939+ payload. put_slice ( & utf16_bytes) ;
940+ payload. put_u8 ( FEATURE_EXT_TERMINATOR ) ; // 0xFF
941+
942+ let result = parse_login7_auth ( & payload) ;
943+ assert_eq ! ( result. user_agent. unwrap( ) , "TestAgent" ) ;
944+ }
945+
946+ #[ test]
947+ fn test_parse_login7_odd_length_user_agent ( ) {
948+ let mut payload = BytesMut :: zeroed ( 100 ) ;
949+ payload[ 27 ] = 0x10 ; // OptionFlags3 (FeatureExt bit)
950+ payload[ 56 ] = 60 ; // Option offset
951+ payload[ 57 ] = 0 ;
952+ payload[ 60 ..64 ] . copy_from_slice ( & 64u32 . to_le_bytes ( ) ) ; // Feature offset
953+ payload. truncate ( 64 ) ;
954+
955+ // FeatureId (1 byte) = 2 (UserAgent)
956+ // FeatureDataLen (4 bytes) = 5 (odd length)
957+ // FeatureData (5 bytes) = [1, 2, 3, 4, 5]
958+ payload. put_u8 ( FEATURE_EXT_USER_AGENT ) ; // 2
959+ payload. put_u32_le ( 5 ) ;
960+ payload. put_slice ( & [ 1 , 2 , 3 , 4 , 5 ] ) ;
961+ payload. put_u8 ( FEATURE_EXT_TERMINATOR ) ; // 0xFF
962+
963+ let result = parse_login7_auth ( & payload) ;
964+ assert_eq ! ( result. user_agent, None ) ;
965+ }
886966}
0 commit comments