diff --git a/passkey-types/src/utils/serde.rs b/passkey-types/src/utils/serde.rs index 4af804e..d9d6621 100644 --- a/passkey-types/src/utils/serde.rs +++ b/passkey-types/src/utils/serde.rs @@ -1,5 +1,7 @@ //! Utilities to be used in serde derives for more robust (de)serializations. +use std::str::FromStr; + use serde::{ de::{Error, Visitor}, Deserialize, Deserializer, @@ -126,7 +128,7 @@ struct StringOrNum(pub std::marker::PhantomData); impl<'de, T> Visitor<'de> for StringOrNum where - T: std::str::FromStr + TryFrom + TryFrom, + T: FromStr + TryFrom + TryFrom, { type Value = T; @@ -138,7 +140,13 @@ where where E: Error, { - std::str::FromStr::from_str(v).map_err(|_| E::custom("Was not a stringified number")) + if let Ok(v) = FromStr::from_str(v) { + Ok(v) + } else if let Ok(v) = f64::from_str(v) { + self.visit_f64(v) + } else { + Err(E::custom("Was not a stringified number")) + } } fn visit_string(self, v: String) -> Result @@ -203,6 +211,23 @@ where { self.visit_u64(v.into()) } + + fn visit_f32(self, v: f32) -> Result + where + E: Error, + { + self.visit_f64(v.into()) + } + + fn visit_f64(self, v: f64) -> Result + where + E: Error, + { + #[allow(clippy::as_conversions)] + // Ensure the float has an integer representation, + // or be 0 if it is a non-integer number + self.visit_i64(if v.is_normal() { v as i64 } else { 0 }) + } } pub(crate) fn maybe_stringified<'de, D>(de: D) -> Result, D::Error> @@ -212,3 +237,97 @@ where de.deserialize_any(StringOrNum(std::marker::PhantomData)) .map(Some) } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn from_float_representations() { + #[derive(Deserialize)] + struct FromFloat { + #[serde(deserialize_with = "maybe_stringified")] + num: Option, + } + + let float_with_0 = r#"{"num": 0.0}"#; + let result: FromFloat = + serde_json::from_str(float_with_0).expect("failed to parse from 0.0"); + assert_eq!(result.num, Some(0)); + + let float_ends_with_0 = r#"{"num": 1800.0}"#; + let result: FromFloat = + serde_json::from_str(float_ends_with_0).expect("failed to parse from 1800.0"); + assert_eq!(result.num, Some(1800)); + + let float_ends_with_num = r#"{"num": 1800.1234}"#; + let result: FromFloat = + serde_json::from_str(float_ends_with_num).expect("failed to parse from 1800.1234"); + assert_eq!(result.num, Some(1800)); + + let sub_zero = r#"{"num": 0.1234}"#; + let result: FromFloat = + serde_json::from_str(sub_zero).expect("failed to parse from 0.1234"); + assert_eq!(result.num, Some(0)); + + let scientific = r#"{"num": 1.0e-308}"#; + let result: FromFloat = + serde_json::from_str(scientific).expect("failed to parse from 1.0e-308"); + assert_eq!(result.num, Some(0)); + + // Ignoring these cases because `serde_json` will fail to deserialize these values + // https://github.com/serde-rs/json/issues/842 + + // let nan = r#"{"num": NaN}"#; + // let result: FromFloat = serde_json::from_str(nan).expect("failed to parse from NaN"); + // assert_eq!(result.num, Some(0)); + + // let inf = r#"{"num": Infinity}"#; + // let result: FromFloat = serde_json::from_str(inf).expect("failed to parse from Infinity"); + // assert_eq!(result.num, Some(0)); + + // let neg_inf = r#"{"num": -Infinity}"#; + // let result: FromFloat = + // serde_json::from_str(neg_inf).expect("failed to parse from -Infinity"); + // assert_eq!(result.num, Some(0)); + + let float_with_0_str = r#"{"num": "0.0"}"#; + let result: FromFloat = + serde_json::from_str(float_with_0_str).expect("failed to parse from stringified 0.0"); + assert_eq!(result.num, Some(0)); + + let float_ends_with_0_str = r#"{"num": "1800.0"}"#; + let result: FromFloat = serde_json::from_str(float_ends_with_0_str) + .expect("failed to parse from stringified 1800.0"); + assert_eq!(result.num, Some(1800)); + + let float_ends_with_num_str = r#"{"num": "1800.1234"}"#; + let result: FromFloat = serde_json::from_str(float_ends_with_num_str) + .expect("failed to parse from stringified 1800.1234"); + assert_eq!(result.num, Some(1800)); + + let sub_zero_str = r#"{"num": "0.1234"}"#; + let result: FromFloat = + serde_json::from_str(sub_zero_str).expect("failed to parse from stringified 0.1234"); + assert_eq!(result.num, Some(0)); + + let scientific_str = r#"{"num": "1.0e-308"}"#; + let result: FromFloat = serde_json::from_str(scientific_str) + .expect("failed to parse from stringified 1.0e-308"); + assert_eq!(result.num, Some(0)); + + let nan_str = r#"{"num": "NaN"}"#; + let result: FromFloat = + serde_json::from_str(nan_str).expect("failed to parse from stringified NaN"); + assert_eq!(result.num, Some(0)); + + let inf_str = r#"{"num": "Infinity"}"#; + let result: FromFloat = + serde_json::from_str(inf_str).expect("failed to parse from stringified Infinity"); + assert_eq!(result.num, Some(0)); + + let neg_inf_str = r#"{"num": "-Infinity"}"#; + let result: FromFloat = + serde_json::from_str(neg_inf_str).expect("failed to parse from stringified -Infinity"); + assert_eq!(result.num, Some(0)); + } +} diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index f92c118..4f0ce7f 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -656,7 +656,9 @@ mod tests { use serde::{Deserialize, Serialize}; use super::CredentialCreationOptions; - use crate::webauthn::{ClientDataType, CollectedClientData}; + use crate::webauthn::{ + ClientDataType, CollectedClientData, PublicKeyCredentialCreationOptions, + }; // Normal client data from Chrome assertion const CLIENT_DATA_JSON_STRING: &str = r#"{ @@ -990,4 +992,33 @@ mod tests { let client_data_json = serde_json::to_string(&ccd).unwrap(); assert_eq!(client_data_json, CROSS_ORIGIN_FALSE); } + + #[test] + fn float_as_timeout() { + let json = r#"{ + "pubKeyCredParams": [ + { "type": "public-key", "alg": -7 }, + { "type": "public-key", "alg": -257 } + ], + "authenticatorSelection": { + "authenticatorAttachment": "platform", + "requireResidentKey": true, + "residentKey": "required", + "userVerification": "required" + }, + "challenge": "MjAyNC0wNy0zMVQxNTozNDowNFpbQkAyMmI3ZDgwOQ\u003d\u003d", + "attestation": "none", + "rp": { "id": "www.paypal.com", "name": "PayPal" }, + "timeout": 1800000.0, + "user": { + "id": "ZDExMTQ2ZWNlY2U3YmE2MGYwMGRhMGE2MWJiZjRiMzk2ZDlkOTBjMDcxOWY0N2Y3Yjc2NGQ0ZGRmMGMxMGRlYQ\u003d\u003d", + "name": "test", + "displayName": "test" + } + }"#; + + let deserialized: PublicKeyCredentialCreationOptions = serde_json::from_str(json).unwrap(); + + assert_eq!(deserialized.timeout, Some(1800000)); + } }