Skip to content

Commit e7cc82f

Browse files
committed
feat!: client_info string doesn't have to decode well
1 parent 019d645 commit e7cc82f

2 files changed

Lines changed: 193 additions & 62 deletions

File tree

  • crates/ethportal-api/src/types/ping_extensions/extensions
  • testing/ethportal-peertest/src/scenarios

crates/ethportal-api/src/types/ping_extensions/extensions/type_0.rs

Lines changed: 191 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{fmt::Display, str::FromStr};
1+
use std::{fmt::Display, iter::repeat, str::FromStr};
22

33
use alloy::primitives::U256;
44
use anyhow::{bail, ensure};
@@ -24,22 +24,22 @@ use crate::{
2424
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
2525
#[serde(rename_all = "camelCase")]
2626
pub 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

3232
impl 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

162191
impl Display for ClientInfo {
@@ -177,15 +206,15 @@ impl Display for ClientInfo {
177206
impl 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,

testing/ethportal-peertest/src/scenarios/ping.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ pub async fn test_ping_capabilities_payload_type(target: &Client, peertest: &Pee
208208

209209
assert_eq!(capabilities_payload.data_radius, Distance::MAX);
210210
assert_eq!(
211-
capabilities_payload.client_info,
212-
Some(ClientInfo::trin_client_info())
211+
capabilities_payload.get_client_info(),
212+
ClientInfo::trin_client_info(),
213213
);
214214
assert_eq!(capabilities_payload.capabilities.len(), 3);
215215
assert_eq!(result.enr_seq, bootnode_sequence);

0 commit comments

Comments
 (0)