1- use std:: { fmt:: Display , str:: FromStr } ;
1+ use std:: { fmt:: Display , iter :: repeat , str:: FromStr } ;
22
33use alloy:: primitives:: U256 ;
44use anyhow:: { bail, ensure} ;
@@ -24,22 +24,22 @@ use crate::{
2424#[ derive( PartialEq , Eq , Debug , Clone , Serialize , Deserialize ) ]
2525#[ serde( rename_all = "camelCase" ) ]
2626pub struct ClientInfoRadiusCapabilities {
27- pub client_info : Option < ClientInfo > ,
27+ pub client_info : String ,
2828 pub data_radius : Distance ,
2929 pub capabilities : VariableList < PingExtensionType , U400 > ,
3030}
3131
3232impl ClientInfoRadiusCapabilities {
3333 pub fn new ( radius : Distance , capabilities : Vec < PingExtensionType > ) -> Self {
3434 Self {
35- client_info : Some ( ClientInfo :: trin_client_info ( ) ) ,
35+ client_info : ClientInfo :: trin_client_info ( ) . to_string ( ) ,
3636 data_radius : radius,
3737 capabilities : VariableList :: from ( capabilities) ,
3838 }
3939 }
4040
4141 pub fn new_with_client_info (
42- client_info : Option < ClientInfo > ,
42+ client_info : String ,
4343 radius : Distance ,
4444 capabilities : Vec < PingExtensionType > ,
4545 ) -> Self {
@@ -50,6 +50,13 @@ impl ClientInfoRadiusCapabilities {
5050 }
5151 }
5252
53+ /// Returns [ClientInfo] type.
54+ ///
55+ /// See [ClientInfo::from_str_or_empty] for exact behaviour.
56+ pub fn get_client_info ( & self ) -> ClientInfo {
57+ ClientInfo :: from_str_or_empty ( & self . client_info )
58+ }
59+
5360 /// ClientType is not robust and should not be used for any critical logic.
5461 /// It can't be used to reliably identify the client type from ClientInfoRadiusCapabilities,
5562 /// since clients can include amendments to their client name, an example of this is Trin
@@ -59,11 +66,7 @@ impl ClientInfoRadiusCapabilities {
5966 /// For projects built on Portal like Glados, it is recommended the respective projects
6067 /// maintain their own client type parsing logic.
6168 pub fn get_client_type ( & self ) -> ClientType {
62- if let Some ( client_info) = & self . client_info {
63- ClientType :: from ( client_info. client_name . as_str ( ) )
64- } else {
65- ClientType :: Unknown ( None )
66- }
69+ ClientType :: from ( self . get_client_info ( ) . client_name . as_str ( ) )
6770 }
6871}
6972
@@ -83,11 +86,7 @@ impl Encode for ClientInfoRadiusCapabilities {
8386 + <U256 as Encode >:: ssz_fixed_len ( )
8487 + <VariableList < u16 , U400 > as Encode >:: ssz_fixed_len ( ) ;
8588 let mut encoder = SszEncoder :: container ( buf, offset) ;
86- let client_info = match & self . client_info {
87- Some ( client_info) => client_info. to_string ( ) ,
88- None => "" . to_string ( ) ,
89- } ;
90- let bytes: Vec < u8 > = client_info. as_bytes ( ) . to_vec ( ) ;
89+ let bytes: Vec < u8 > = self . client_info . as_bytes ( ) . to_vec ( ) ;
9190 let client_info: VariableList < u8 , U200 > = VariableList :: from ( bytes) ;
9291
9392 encoder. append ( & client_info) ;
@@ -112,15 +111,9 @@ impl Decode for ClientInfoRadiusCapabilities {
112111 let data_radius: U256 = decoder. decode_next ( ) ?;
113112 let capabilities: VariableList < PingExtensionType , U400 > = decoder. decode_next ( ) ?;
114113
115- let string = String :: from_utf8 ( client_info. to_vec ( ) ) . map_err ( |_| {
114+ let client_info = String :: from_utf8 ( client_info. to_vec ( ) ) . map_err ( |_| {
116115 ssz:: DecodeError :: BytesInvalid ( format ! ( "Invalid utf8 string: {client_info:?}" ) )
117116 } ) ?;
118- let client_info = match string. as_str ( ) {
119- "" => None ,
120- _ => Some ( ClientInfo :: from_str ( & string) . map_err ( |err| {
121- ssz:: DecodeError :: BytesInvalid ( format ! ( "Failed to parse client info: {err:?}" ) )
122- } ) ?) ,
123- } ;
124117
125118 Ok ( Self {
126119 client_info,
@@ -157,6 +150,42 @@ impl ClientInfo {
157150 programming_language_version : format ! ( "rustc{PROGRAMMING_LANGUAGE_VERSION}" ) ,
158151 }
159152 }
153+
154+ /// Parses a string `s` to return value of this type.
155+ ///
156+ /// Unlike [FromStr::from_str], this function doesn't fail. This means that if input doesn't
157+ /// follow strict format, parsing might result in completely wrong interpretation (e.g.
158+ /// `client_version` might be set to `operating_system`).
159+ pub fn from_str_or_empty ( s : & str ) -> Self {
160+ let mut parts = s. split ( '/' ) ;
161+
162+ let client_name = parts. next ( ) . unwrap_or_default ( ) ;
163+
164+ let client_version_and_short_commit = parts. next ( ) . unwrap_or_default ( ) ;
165+ let ( client_version, short_commit) = client_version_and_short_commit
166+ . splitn ( 2 , '-' )
167+ . chain ( repeat ( "" ) )
168+ . next_tuple ( )
169+ . expect ( "must have enough elemets" ) ;
170+
171+ let os_and_cpu = parts. next ( ) . unwrap_or_default ( ) ;
172+ let ( operating_system, cpu_architecture) = os_and_cpu
173+ . splitn ( 2 , '-' )
174+ . chain ( repeat ( "" ) )
175+ . next_tuple ( )
176+ . expect ( "muct have enough elements" ) ;
177+
178+ let programming_language_version = parts. next ( ) . unwrap_or_default ( ) ;
179+
180+ Self {
181+ client_name : client_name. to_string ( ) ,
182+ client_version : client_version. to_string ( ) ,
183+ short_commit : short_commit. to_string ( ) ,
184+ operating_system : operating_system. to_string ( ) ,
185+ cpu_architecture : cpu_architecture. to_string ( ) ,
186+ programming_language_version : programming_language_version. to_string ( ) ,
187+ }
188+ }
160189}
161190
162191impl Display for ClientInfo {
@@ -177,15 +206,15 @@ impl Display for ClientInfo {
177206impl FromStr for ClientInfo {
178207 type Err = anyhow:: Error ;
179208
180- fn from_str ( string : & str ) -> Result < Self , anyhow:: Error > {
181- ensure ! ( string . len( ) <= 200 , "Client info string is too long" ) ;
182- let parts: Vec < & str > = string . split ( '/' ) . collect ( ) ;
209+ fn from_str ( s : & str ) -> Result < Self , anyhow:: Error > {
210+ ensure ! ( s . len( ) <= 200 , "Client info string is too long" ) ;
211+ let parts: Vec < & str > = s . split ( '/' ) . collect ( ) ;
183212
184213 if parts. len ( ) != 4 {
185214 bail ! (
186215 "Invalid client info string: should have 4 /'s instead got {} | {}" ,
187216 parts. len( ) ,
188- string
217+ s
189218 ) ;
190219 }
191220
@@ -249,8 +278,130 @@ mod tests {
249278 utils:: bytes:: { hex_decode, hex_encode} ,
250279 } ;
251280
281+ mod client_info {
282+ use super :: * ;
283+
284+ #[ test]
285+ fn from_str ( ) {
286+ let client_info = ClientInfo :: trin_client_info ( ) ;
287+ let string = client_info. to_string ( ) ;
288+ let decoded = ClientInfo :: from_str ( & string) . unwrap ( ) ;
289+ assert_eq ! ( client_info, decoded) ;
290+ }
291+
292+ #[ rstest]
293+ /// Fails because there are not enough parts
294+ #[ case( "trin/0.1.1-2b00d730/linux-x86_64" ) ]
295+ /// Fails because there are too many parts
296+ #[ case( "trin/0.1.1-2b00d730/linux-x86_64/rustc1.81.0/extra" ) ]
297+ /// Fails because the short commit is missing
298+ #[ case( "trin/0.1.1/linux-x86_64/rustc1.81.0" ) ]
299+ /// Fails because the CPU architecture is missing
300+ #[ case( "trin/0.1.1-2b00d730/linux/rustc1.81.0" ) ]
301+ /// Fails because client string is too long
302+ #[ case( & "t" . repeat( 201 ) ) ]
303+ #[ should_panic]
304+ fn from_str_invalid ( #[ case] string : & str ) {
305+ ClientInfo :: from_str ( string) . unwrap ( ) ;
306+ }
307+
308+ #[ rstest]
309+ /// Regular client info format
310+ #[ case:: regular(
311+ "trin/0.1.1-2b00d730/linux-x86_64/rustc1.81.0" ,
312+ "0.1.1" ,
313+ "2b00d730" ,
314+ "linux" ,
315+ "x86_64" ,
316+ "rustc1.81.0"
317+ ) ]
318+ /// Only Client name
319+ #[ case:: only_client_name( "trin" , "" , "" , "" , "" , "" ) ]
320+ /// Only Client name and slashes
321+ #[ case:: only_client_name_with_slashes( "trin///" , "" , "" , "" , "" , "" ) ]
322+ /// Only Client name and slashes and dashes
323+ #[ case:: only_client_name_with_slashes_and_dashes( "trin/-/-/" , "" , "" , "" , "" , "" ) ]
324+ /// Short commit is missing
325+ #[ case:: missing_commit(
326+ "trin/0.1.1/linux-x86_64/rustc1.81.0" ,
327+ "0.1.1" ,
328+ "" ,
329+ "linux" ,
330+ "x86_64" ,
331+ "rustc1.81.0"
332+ ) ]
333+ /// CPU architecture is missing
334+ #[ case:: missing_cpu_architecture(
335+ "trin/0.1.1-2b00d730/linux/rustc1.81.0" ,
336+ "0.1.1" ,
337+ "2b00d730" ,
338+ "linux" ,
339+ "" ,
340+ "rustc1.81.0"
341+ ) ]
342+ /// Programming language is missing
343+ #[ case:: missing_programming_language(
344+ "trin/0.1.1-2b00d730/linux-x86_64" ,
345+ "0.1.1" ,
346+ "2b00d730" ,
347+ "linux" ,
348+ "x86_64" ,
349+ ""
350+ ) ]
351+ /// Extra part
352+ #[ case:: extra_part(
353+ "trin/0.1.1-2b00d730/linux-x86_64/rustc1.81.0/extra" ,
354+ "0.1.1" ,
355+ "2b00d730" ,
356+ "linux" ,
357+ "x86_64" ,
358+ "rustc1.81.0"
359+ ) ]
360+ fn from_str_or_empty (
361+ #[ case] string : & str ,
362+ #[ case] client_version : & str ,
363+ #[ case] short_commit : & str ,
364+ #[ case] operating_system : & str ,
365+ #[ case] cpu_architecture : & str ,
366+ #[ case] programming_language_version : & str ,
367+ ) {
368+ assert_eq ! (
369+ ClientInfo :: from_str_or_empty( string) ,
370+ ClientInfo {
371+ client_name: "trin" . to_string( ) ,
372+ client_version: client_version. to_string( ) ,
373+ short_commit: short_commit. to_string( ) ,
374+ operating_system: operating_system. to_string( ) ,
375+ cpu_architecture: cpu_architecture. to_string( ) ,
376+ programming_language_version: programming_language_version. to_string( ) ,
377+ } ,
378+ ) ;
379+ }
380+
381+ #[ rstest]
382+ #[ case( "" ) ]
383+ #[ case( "/" ) ]
384+ #[ case( "//" ) ]
385+ #[ case( "///" ) ]
386+ #[ case( "////" ) ]
387+ #[ case( "/-/-/" ) ]
388+ fn from_empty_string ( #[ case] string : & str ) {
389+ assert_eq ! (
390+ ClientInfo :: from_str_or_empty( string) ,
391+ ClientInfo {
392+ client_name: "" . to_string( ) ,
393+ client_version: "" . to_string( ) ,
394+ short_commit: "" . to_string( ) ,
395+ operating_system: "" . to_string( ) ,
396+ cpu_architecture: "" . to_string( ) ,
397+ programming_language_version: "" . to_string( ) ,
398+ } ,
399+ ) ;
400+ }
401+ }
402+
252403 #[ test]
253- fn test_client_info_radius_capabilities ( ) {
404+ fn client_info_radius_capabilities ( ) {
254405 let radius = Distance :: from ( U256 :: from ( 42 ) ) ;
255406 let capabilities = vec ! [
256407 PingExtensionType :: Capabilities ,
@@ -276,42 +427,17 @@ mod tests {
276427 }
277428 }
278429
279- #[ test]
280- fn test_client_info_from_str ( ) {
281- let client_info = ClientInfo :: trin_client_info ( ) ;
282- let string = client_info. to_string ( ) ;
283- let decoded = ClientInfo :: from_str ( & string) . unwrap ( ) ;
284- assert_eq ! ( client_info, decoded) ;
285- }
286-
287- #[ rstest]
288- /// Fails because there are not enough parts
289- #[ case( "trin/0.1.1-2b00d730/linux-x86_64" ) ]
290- /// Fails because there are too many parts
291- #[ case( "trin/0.1.1-2b00d730/linux-x86_64/rustc1.81.0/extra" ) ]
292- /// Fails because the short commit is missing
293- #[ case( "trin/0.1.1/linux-x86_64/rustc1.81.0" ) ]
294- /// Fails because the CPU architecture is missing
295- #[ case( "trin/0.1.1-2b00d730/linux/rustc1.81.0" ) ]
296- /// Fails because client string is too long
297- #[ case( & "t" . repeat( 201 ) ) ]
298- #[ should_panic]
299- fn test_client_info_from_str_invalid ( #[ case] string : & str ) {
300- ClientInfo :: from_str ( string) . unwrap ( ) ;
301- }
302-
303430 #[ test]
304431 fn message_encoding_ping_capabilities_with_client_info ( ) {
305432 let data_radius = Distance :: from ( U256 :: MAX - U256 :: from ( 1 ) ) ;
306- let client_info =
307- ClientInfo :: from_str ( "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0" ) . unwrap ( ) ;
433+ let client_info = "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0" . to_string ( ) ;
308434 let capabilities = vec ! [
309435 PingExtensionType :: Capabilities ,
310436 PingExtensionType :: BasicRadius ,
311437 PingExtensionType :: Error ,
312438 ] ;
313439 let capabilities_payload = ClientInfoRadiusCapabilities :: new_with_client_info (
314- Some ( client_info) ,
440+ client_info,
315441 data_radius,
316442 capabilities,
317443 ) ;
@@ -340,8 +466,11 @@ mod tests {
340466 PingExtensionType :: BasicRadius ,
341467 PingExtensionType :: Error ,
342468 ] ;
343- let capabilities_payload =
344- ClientInfoRadiusCapabilities :: new_with_client_info ( None , data_radius, capabilities) ;
469+ let capabilities_payload = ClientInfoRadiusCapabilities :: new_with_client_info (
470+ String :: default ( ) ,
471+ data_radius,
472+ capabilities,
473+ ) ;
345474 let payload = CustomPayload :: from ( capabilities_payload) ;
346475 let ping = Ping {
347476 enr_seq : 1 ,
@@ -362,15 +491,14 @@ mod tests {
362491 #[ test]
363492 fn message_encoding_pong_capabilities_with_client_info ( ) {
364493 let data_radius = Distance :: from ( U256 :: MAX - U256 :: from ( 1 ) ) ;
365- let client_info =
366- ClientInfo :: from_str ( "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0" ) . unwrap ( ) ;
494+ let client_info = "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0" . to_string ( ) ;
367495 let capabilities = vec ! [
368496 PingExtensionType :: Capabilities ,
369497 PingExtensionType :: BasicRadius ,
370498 PingExtensionType :: Error ,
371499 ] ;
372500 let capabilities_payload = ClientInfoRadiusCapabilities :: new_with_client_info (
373- Some ( client_info) ,
501+ client_info,
374502 data_radius,
375503 capabilities,
376504 ) ;
@@ -399,8 +527,11 @@ mod tests {
399527 PingExtensionType :: BasicRadius ,
400528 PingExtensionType :: Error ,
401529 ] ;
402- let capabilities_payload =
403- ClientInfoRadiusCapabilities :: new_with_client_info ( None , data_radius, capabilities) ;
530+ let capabilities_payload = ClientInfoRadiusCapabilities :: new_with_client_info (
531+ String :: default ( ) ,
532+ data_radius,
533+ capabilities,
534+ ) ;
404535 let payload = CustomPayload :: from ( capabilities_payload) ;
405536 let pong = Pong {
406537 enr_seq : 1 ,
0 commit comments