Skip to content

Commit

Permalink
Merge pull request #43 from 1Password/progdrasil/handle-float-values-…
Browse files Browse the repository at this point in the history
…deserialization

Handle floating point values for deserialization
  • Loading branch information
Progdrasil authored Aug 5, 2024
2 parents 80ef0aa + c33afa2 commit cca6b24
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 3 deletions.
123 changes: 121 additions & 2 deletions passkey-types/src/utils/serde.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -126,7 +128,7 @@ struct StringOrNum<T>(pub std::marker::PhantomData<T>);

impl<'de, T> Visitor<'de> for StringOrNum<T>
where
T: std::str::FromStr + TryFrom<i64> + TryFrom<u64>,
T: FromStr + TryFrom<i64> + TryFrom<u64>,
{
type Value = T;

Expand All @@ -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<E>(self, v: String) -> Result<Self::Value, E>
Expand Down Expand Up @@ -203,6 +211,23 @@ where
{
self.visit_u64(v.into())
}

fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_f64(v.into())
}

fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
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<Option<u32>, D::Error>
Expand All @@ -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<u32>,
}

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));
}
}
33 changes: 32 additions & 1 deletion passkey-types/src/webauthn/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"{
Expand Down Expand Up @@ -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));
}
}

0 comments on commit cca6b24

Please sign in to comment.