|
1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
4 | | -// cspell:ignore Hdvcmxk |
| 4 | +// cspell:ignore Hdvcmxk unpadded |
5 | 5 |
|
6 | 6 | //! Base64 encoding and decoding functions. |
7 | 7 |
|
@@ -332,3 +332,214 @@ pub mod option { |
332 | 332 | <Option<String>>::serialize(&encoded, serializer) |
333 | 333 | } |
334 | 334 | } |
| 335 | + |
| 336 | +#[cfg(test)] |
| 337 | +mod tests { |
| 338 | + use super::{ |
| 339 | + decode, decode_url_safe, deserialize, deserialize_url_safe, encode, encode_url_safe, |
| 340 | + option, serialize, serialize_url_safe, |
| 341 | + }; |
| 342 | + use serde::{Deserialize, Serialize}; |
| 343 | + |
| 344 | + #[test] |
| 345 | + fn standard_encode() { |
| 346 | + assert_eq!(encode(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ=="); |
| 347 | + assert_eq!(encode(b""), ""); |
| 348 | + assert_eq!(encode(b"f"), "Zg=="); |
| 349 | + assert_eq!(encode(b"fo"), "Zm8="); |
| 350 | + assert_eq!(encode(b"foo"), "Zm9v"); |
| 351 | + } |
| 352 | + |
| 353 | + #[test] |
| 354 | + fn standard_decode() { |
| 355 | + assert_eq!(decode("SGVsbG8sIHdvcmxkIQ==").unwrap(), b"Hello, world!"); |
| 356 | + assert_eq!(decode("").unwrap(), b""); |
| 357 | + assert_eq!(decode("Zg==").unwrap(), b"f"); |
| 358 | + assert_eq!(decode("Zm8=").unwrap(), b"fo"); |
| 359 | + assert_eq!(decode("Zm9v").unwrap(), b"foo"); |
| 360 | + } |
| 361 | + |
| 362 | + #[test] |
| 363 | + fn url_safe_encode() { |
| 364 | + assert_eq!(encode_url_safe(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ"); |
| 365 | + assert_eq!(encode_url_safe(b""), ""); |
| 366 | + assert_eq!(encode_url_safe(b"f"), "Zg"); |
| 367 | + assert_eq!(encode_url_safe(b"fo"), "Zm8"); |
| 368 | + assert_eq!(encode_url_safe(b"foo"), "Zm9v"); |
| 369 | + |
| 370 | + // Verify no padding in base64url encoding |
| 371 | + assert!(!encode_url_safe(b"f").contains('=')); |
| 372 | + assert!(!encode_url_safe(b"fo").contains('=')); |
| 373 | + assert!(!encode_url_safe(b"foo").contains('=')); |
| 374 | + } |
| 375 | + |
| 376 | + #[test] |
| 377 | + fn url_safe_decode() { |
| 378 | + assert_eq!( |
| 379 | + decode_url_safe("SGVsbG8sIHdvcmxkIQ").unwrap(), |
| 380 | + b"Hello, world!" |
| 381 | + ); |
| 382 | + assert_eq!(decode_url_safe("").unwrap(), b""); |
| 383 | + assert_eq!(decode_url_safe("Zg").unwrap(), b"f"); |
| 384 | + assert_eq!(decode_url_safe("Zm8").unwrap(), b"fo"); |
| 385 | + assert_eq!(decode_url_safe("Zm9v").unwrap(), b"foo"); |
| 386 | + } |
| 387 | + |
| 388 | + #[test] |
| 389 | + fn roundtrip_standard() { |
| 390 | + let data = b"The quick brown fox jumps over the lazy dog"; |
| 391 | + assert_eq!(decode(encode(data)).unwrap(), data); |
| 392 | + } |
| 393 | + |
| 394 | + #[test] |
| 395 | + fn roundtrip_url_safe() { |
| 396 | + let data = b"The quick brown fox jumps over the lazy dog"; |
| 397 | + assert_eq!(decode_url_safe(encode_url_safe(data)).unwrap(), data); |
| 398 | + } |
| 399 | + |
| 400 | + #[derive(Serialize, Deserialize)] |
| 401 | + struct TestStruct { |
| 402 | + #[serde(serialize_with = "serialize", deserialize_with = "deserialize")] |
| 403 | + data: Vec<u8>, |
| 404 | + } |
| 405 | + |
| 406 | + #[test] |
| 407 | + fn serde_standard() { |
| 408 | + let original = TestStruct { |
| 409 | + data: b"test data".to_vec(), |
| 410 | + }; |
| 411 | + let json = serde_json::to_string(&original).unwrap(); |
| 412 | + assert!(json.contains("dGVzdCBkYXRh")); |
| 413 | + let deserialized: TestStruct = serde_json::from_str(&json).unwrap(); |
| 414 | + assert_eq!(deserialized.data, original.data); |
| 415 | + } |
| 416 | + |
| 417 | + #[derive(Serialize, Deserialize)] |
| 418 | + struct TestStructUrlSafe { |
| 419 | + #[serde( |
| 420 | + serialize_with = "serialize_url_safe", |
| 421 | + deserialize_with = "deserialize_url_safe" |
| 422 | + )] |
| 423 | + data: Vec<u8>, |
| 424 | + } |
| 425 | + |
| 426 | + #[test] |
| 427 | + fn serde_url_safe() { |
| 428 | + let original = TestStructUrlSafe { |
| 429 | + data: b"test data".to_vec(), |
| 430 | + }; |
| 431 | + let json = serde_json::to_string(&original).unwrap(); |
| 432 | + assert!(json.contains("dGVzdCBkYXRh")); |
| 433 | + let deserialized: TestStructUrlSafe = serde_json::from_str(&json).unwrap(); |
| 434 | + assert_eq!(deserialized.data, original.data); |
| 435 | + } |
| 436 | + |
| 437 | + #[derive(Serialize, Deserialize)] |
| 438 | + struct TestOptionalStruct { |
| 439 | + #[serde( |
| 440 | + serialize_with = "option::serialize", |
| 441 | + deserialize_with = "option::deserialize" |
| 442 | + )] |
| 443 | + data: Option<Vec<u8>>, |
| 444 | + } |
| 445 | + |
| 446 | + #[test] |
| 447 | + fn serde_option_some() { |
| 448 | + let original = TestOptionalStruct { |
| 449 | + data: Some(b"test data".to_vec()), |
| 450 | + }; |
| 451 | + let json = serde_json::to_string(&original).unwrap(); |
| 452 | + assert!(json.contains("dGVzdCBkYXRh")); |
| 453 | + let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap(); |
| 454 | + assert_eq!(deserialized.data, original.data); |
| 455 | + } |
| 456 | + |
| 457 | + #[test] |
| 458 | + fn serde_option_none() { |
| 459 | + let original = TestOptionalStruct { data: None }; |
| 460 | + let json = serde_json::to_string(&original).unwrap(); |
| 461 | + assert!(json.contains("null")); |
| 462 | + let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap(); |
| 463 | + assert_eq!(deserialized.data, None); |
| 464 | + } |
| 465 | + |
| 466 | + #[derive(Serialize, Deserialize)] |
| 467 | + struct TestOptionalStructUrlSafe { |
| 468 | + #[serde( |
| 469 | + serialize_with = "option::serialize_url_safe", |
| 470 | + deserialize_with = "option::deserialize_url_safe" |
| 471 | + )] |
| 472 | + data: Option<Vec<u8>>, |
| 473 | + } |
| 474 | + |
| 475 | + #[test] |
| 476 | + fn serde_option_url_safe_some() { |
| 477 | + let original = TestOptionalStructUrlSafe { |
| 478 | + data: Some(b"test data".to_vec()), |
| 479 | + }; |
| 480 | + let json = serde_json::to_string(&original).unwrap(); |
| 481 | + assert!(json.contains("dGVzdCBkYXRh")); |
| 482 | + let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap(); |
| 483 | + assert_eq!(deserialized.data, original.data); |
| 484 | + } |
| 485 | + |
| 486 | + #[test] |
| 487 | + fn serde_option_url_safe_none() { |
| 488 | + let original = TestOptionalStructUrlSafe { data: None }; |
| 489 | + let json = serde_json::to_string(&original).unwrap(); |
| 490 | + assert!(json.contains("null")); |
| 491 | + let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap(); |
| 492 | + assert_eq!(deserialized.data, None); |
| 493 | + } |
| 494 | + |
| 495 | + #[test] |
| 496 | + fn padding_behavior_differences() { |
| 497 | + // base64url encoding never produces padding |
| 498 | + let encoded_url_safe = encode_url_safe(b"f"); |
| 499 | + assert!(!encoded_url_safe.contains('=')); |
| 500 | + |
| 501 | + // standard base64 encoding produces padding when needed |
| 502 | + let encoded_standard = encode(b"f"); |
| 503 | + assert!(encoded_standard.contains('=')); |
| 504 | + |
| 505 | + // Both decoders accept both padded and unpadded input (DecodePaddingMode::Indifferent) |
| 506 | + assert_eq!(decode_url_safe("Zg==").unwrap(), b"f"); // padded input accepted |
| 507 | + assert_eq!(decode_url_safe("Zg").unwrap(), b"f"); // unpadded input accepted |
| 508 | + assert_eq!(decode("Zg==").unwrap(), b"f"); // padded input accepted |
| 509 | + assert_eq!(decode("Zg").unwrap(), b"f"); // unpadded input accepted |
| 510 | + } |
| 511 | + |
| 512 | + #[test] |
| 513 | + fn decode_truly_invalid_fails() { |
| 514 | + // Characters outside base64 alphabet should fail |
| 515 | + assert!(decode("Zg!@").is_err()); |
| 516 | + assert!(decode_url_safe("Zg!@").is_err()); |
| 517 | + |
| 518 | + // Invalid length for certain inputs should fail |
| 519 | + assert!(decode("Z").is_err()); // single character |
| 520 | + assert!(decode_url_safe("Z").is_err()); |
| 521 | + } |
| 522 | + |
| 523 | + #[test] |
| 524 | + fn decode_cross_alphabet_characters_fail() { |
| 525 | + // Test data containing base64url-specific characters (- and _) should fail standard base64 decoding |
| 526 | + // These strings contain characters that are valid in base64url but not in standard base64 |
| 527 | + assert!(decode("SGVs-bG8sIHdvcmxkIQ").is_err()); // contains '-' (base64url char 62) |
| 528 | + assert!(decode("SGVs_bG8sIHdvcmxkIQ").is_err()); // contains '_' (base64url char 63) |
| 529 | + |
| 530 | + // Test data containing standard base64-specific characters (+ and /) should fail base64url decoding |
| 531 | + // These strings contain characters that are valid in standard base64 but not in base64url |
| 532 | + assert!(decode_url_safe("SGVs+bG8sIHdvcmxkIQ").is_err()); // contains '+' (base64 char 62) |
| 533 | + assert!(decode_url_safe("SGVs/bG8sIHdvcmxkIQ").is_err()); // contains '/' (base64 char 63) |
| 534 | + } |
| 535 | + |
| 536 | + #[test] |
| 537 | + fn decode_invalid_standard() { |
| 538 | + assert!(decode("invalid!@#$").is_err()); |
| 539 | + } |
| 540 | + |
| 541 | + #[test] |
| 542 | + fn decode_invalid_url_safe() { |
| 543 | + assert!(decode_url_safe("invalid!@#$").is_err()); |
| 544 | + } |
| 545 | +} |
0 commit comments