From 1ea9c85adcc35a3da103c33dfca9f57a48f7eddf Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 16 Oct 2025 17:26:55 -0700 Subject: [PATCH 1/3] Add base64 tests Based on feedback from #3214 --- sdk/core/typespec_client_core/src/base64.rs | 165 ++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/sdk/core/typespec_client_core/src/base64.rs b/sdk/core/typespec_client_core/src/base64.rs index 01e88e383d..2a1352af2a 100644 --- a/sdk/core/typespec_client_core/src/base64.rs +++ b/sdk/core/typespec_client_core/src/base64.rs @@ -332,3 +332,168 @@ pub mod option { >::serialize(&encoded, serializer) } } + +#[cfg(test)] +mod tests { + use super::{ + decode, decode_url_safe, deserialize, deserialize_url_safe, encode, encode_url_safe, + option, serialize, serialize_url_safe, + }; + use serde::{Deserialize, Serialize}; + + #[test] + fn standard_encode() { + assert_eq!(encode(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ=="); + assert_eq!(encode(b""), ""); + assert_eq!(encode(b"f"), "Zg=="); + assert_eq!(encode(b"fo"), "Zm8="); + assert_eq!(encode(b"foo"), "Zm9v"); + } + + #[test] + fn standard_decode() { + assert_eq!(decode("SGVsbG8sIHdvcmxkIQ==").unwrap(), b"Hello, world!"); + assert_eq!(decode("").unwrap(), b""); + assert_eq!(decode("Zg==").unwrap(), b"f"); + assert_eq!(decode("Zm8=").unwrap(), b"fo"); + assert_eq!(decode("Zm9v").unwrap(), b"foo"); + } + + #[test] + fn url_safe_encode() { + assert_eq!(encode_url_safe(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ"); + assert_eq!(encode_url_safe(b""), ""); + assert_eq!(encode_url_safe(b"f"), "Zg"); + assert_eq!(encode_url_safe(b"fo"), "Zm8"); + assert_eq!(encode_url_safe(b"foo"), "Zm9v"); + } + + #[test] + fn url_safe_decode() { + assert_eq!( + decode_url_safe("SGVsbG8sIHdvcmxkIQ").unwrap(), + b"Hello, world!" + ); + assert_eq!(decode_url_safe("").unwrap(), b""); + assert_eq!(decode_url_safe("Zg").unwrap(), b"f"); + assert_eq!(decode_url_safe("Zm8").unwrap(), b"fo"); + assert_eq!(decode_url_safe("Zm9v").unwrap(), b"foo"); + } + + #[test] + fn roundtrip_standard() { + let data = b"The quick brown fox jumps over the lazy dog"; + assert_eq!(decode(encode(data)).unwrap(), data); + } + + #[test] + fn roundtrip_url_safe() { + let data = b"The quick brown fox jumps over the lazy dog"; + assert_eq!(decode_url_safe(encode_url_safe(data)).unwrap(), data); + } + + #[derive(Serialize, Deserialize)] + struct TestStruct { + #[serde(serialize_with = "serialize", deserialize_with = "deserialize")] + data: Vec, + } + + #[test] + fn serde_standard() { + let original = TestStruct { + data: b"test data".to_vec(), + }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("dGVzdCBkYXRh")); + let deserialized: TestStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, original.data); + } + + #[derive(Serialize, Deserialize)] + struct TestStructUrlSafe { + #[serde( + serialize_with = "serialize_url_safe", + deserialize_with = "deserialize_url_safe" + )] + data: Vec, + } + + #[test] + fn serde_url_safe() { + let original = TestStructUrlSafe { + data: b"test data".to_vec(), + }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("dGVzdCBkYXRh")); + let deserialized: TestStructUrlSafe = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, original.data); + } + + #[derive(Serialize, Deserialize)] + struct TestOptionalStruct { + #[serde( + serialize_with = "option::serialize", + deserialize_with = "option::deserialize" + )] + data: Option>, + } + + #[test] + fn serde_option_some() { + let original = TestOptionalStruct { + data: Some(b"test data".to_vec()), + }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("dGVzdCBkYXRh")); + let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, original.data); + } + + #[test] + fn serde_option_none() { + let original = TestOptionalStruct { data: None }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("null")); + let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, None); + } + + #[derive(Serialize, Deserialize)] + struct TestOptionalStructUrlSafe { + #[serde( + serialize_with = "option::serialize_url_safe", + deserialize_with = "option::deserialize_url_safe" + )] + data: Option>, + } + + #[test] + fn serde_option_url_safe_some() { + let original = TestOptionalStructUrlSafe { + data: Some(b"test data".to_vec()), + }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("dGVzdCBkYXRh")); + let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, original.data); + } + + #[test] + fn serde_option_url_safe_none() { + let original = TestOptionalStructUrlSafe { data: None }; + let json = serde_json::to_string(&original).unwrap(); + assert!(json.contains("null")); + let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.data, None); + } + + #[test] + fn decode_invalid_standard() { + assert!(decode("invalid!@#$").is_err()); + } + + #[test] + fn decode_invalid_url_safe() { + assert!(decode_url_safe("invalid!@#$").is_err()); + } +} From 60e763d274d72ef0741ad4379436e19eb07f9823 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 17 Oct 2025 12:01:43 -0700 Subject: [PATCH 2/3] Added more tests based on PR feedback Thought of a few other negative cases while I was at it. --- sdk/core/typespec_client_core/src/base64.rs | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/sdk/core/typespec_client_core/src/base64.rs b/sdk/core/typespec_client_core/src/base64.rs index 2a1352af2a..0386a4344f 100644 --- a/sdk/core/typespec_client_core/src/base64.rs +++ b/sdk/core/typespec_client_core/src/base64.rs @@ -366,6 +366,11 @@ mod tests { assert_eq!(encode_url_safe(b"f"), "Zg"); assert_eq!(encode_url_safe(b"fo"), "Zm8"); assert_eq!(encode_url_safe(b"foo"), "Zm9v"); + + // Verify no padding in base64url encoding + assert!(!encode_url_safe(b"f").contains('=')); + assert!(!encode_url_safe(b"fo").contains('=')); + assert!(!encode_url_safe(b"foo").contains('=')); } #[test] @@ -487,6 +492,47 @@ mod tests { assert_eq!(deserialized.data, None); } + #[test] + fn padding_behavior_differences() { + // base64url encoding never produces padding + let encoded_url_safe = encode_url_safe(b"f"); + assert!(!encoded_url_safe.contains('=')); + + // standard base64 encoding produces padding when needed + let encoded_standard = encode(b"f"); + assert!(encoded_standard.contains('=')); + + // Both decoders accept both padded and unpadded input (DecodePaddingMode::Indifferent) + assert_eq!(decode_url_safe("Zg==").unwrap(), b"f"); // padded input accepted + assert_eq!(decode_url_safe("Zg").unwrap(), b"f"); // unpadded input accepted + assert_eq!(decode("Zg==").unwrap(), b"f"); // padded input accepted + assert_eq!(decode("Zg").unwrap(), b"f"); // unpadded input accepted + } + + #[test] + fn decode_truly_invalid_fails() { + // Characters outside base64 alphabet should fail + assert!(decode("Zg!@").is_err()); + assert!(decode_url_safe("Zg!@").is_err()); + + // Invalid length for certain inputs should fail + assert!(decode("Z").is_err()); // single character + assert!(decode_url_safe("Z").is_err()); + } + + #[test] + fn decode_cross_alphabet_characters_fail() { + // Test data containing base64url-specific characters (- and _) should fail standard base64 decoding + // These strings contain characters that are valid in base64url but not in standard base64 + assert!(decode("SGVs-bG8sIHdvcmxkIQ").is_err()); // contains '-' (base64url char 62) + assert!(decode("SGVs_bG8sIHdvcmxkIQ").is_err()); // contains '_' (base64url char 63) + + // Test data containing standard base64-specific characters (+ and /) should fail base64url decoding + // These strings contain characters that are valid in standard base64 but not in base64url + assert!(decode_url_safe("SGVs+bG8sIHdvcmxkIQ").is_err()); // contains '+' (base64 char 62) + assert!(decode_url_safe("SGVs/bG8sIHdvcmxkIQ").is_err()); // contains '/' (base64 char 63) + } + #[test] fn decode_invalid_standard() { assert!(decode("invalid!@#$").is_err()); From 0fca779ac3e81a546711daf7755ccb118f9e4c8c Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 17 Oct 2025 12:58:57 -0700 Subject: [PATCH 3/3] Fix cspell lint --- sdk/core/typespec_client_core/src/base64.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/typespec_client_core/src/base64.rs b/sdk/core/typespec_client_core/src/base64.rs index 0386a4344f..b8dab7afdc 100644 --- a/sdk/core/typespec_client_core/src/base64.rs +++ b/sdk/core/typespec_client_core/src/base64.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// cspell:ignore Hdvcmxk +// cspell:ignore Hdvcmxk unpadded //! Base64 encoding and decoding functions.