uuid_extra/
extra_base64.rs1use crate::extra_uuid::{new_v4, new_v7, to_time_epoch_ms};
2use crate::{Error, Result, support};
3use base64::{engine::general_purpose, Engine as _};
4use uuid::Uuid;
5
6pub fn new_v4_b64() -> String {
10 let uuid = new_v4();
11 general_purpose::STANDARD.encode(uuid.as_bytes())
12}
13
14pub fn new_v4_b64url() -> String {
16 let uuid = new_v4();
17 general_purpose::URL_SAFE.encode(uuid.as_bytes())
18}
19
20pub fn new_v4_b64url_nopad() -> String {
22 let uuid = new_v4();
23 general_purpose::URL_SAFE_NO_PAD.encode(uuid.as_bytes())
24}
25
26pub fn new_v7_b64() -> String {
32 let uuid = new_v7();
33 general_purpose::STANDARD.encode(uuid.as_bytes())
34}
35
36pub fn new_v7_b64url() -> String {
38 let uuid = new_v7();
39 general_purpose::URL_SAFE.encode(uuid.as_bytes())
40}
41
42pub fn new_v7_b64url_nopad() -> String {
44 let uuid = new_v7();
45 general_purpose::URL_SAFE_NO_PAD.encode(uuid.as_bytes())
46}
47
48pub fn from_b64(s: &str) -> Result<Uuid> {
54 let decoded_bytes = general_purpose::STANDARD.decode(s).map_err(Error::custom_from_err)?;
55 support::from_vec_u8(decoded_bytes, "base64")
56}
57
58pub fn b64_to_epoch_ms(s: &str) -> Result<i64> {
61 let uuid = from_b64(s)?;
62 to_time_epoch_ms(&uuid)
63}
64
65pub fn from_b64url(s: &str) -> Result<Uuid> {
67 let decoded_bytes = general_purpose::URL_SAFE.decode(s).map_err(Error::custom_from_err)?;
68 support::from_vec_u8(decoded_bytes, "base64url")
69}
70
71pub fn b64url_to_epoch_ms(s: &str) -> Result<i64> {
74 let uuid = from_b64url(s)?;
75 to_time_epoch_ms(&uuid)
76}
77
78pub fn from_b64url_nopad(s: &str) -> Result<Uuid> {
80 let decoded_bytes = general_purpose::URL_SAFE_NO_PAD.decode(s).map_err(Error::custom_from_err)?;
81 support::from_vec_u8(decoded_bytes, "base64url-nopad")
82}
83
84pub fn b64url_nopad_to_epoch_ms(s: &str) -> Result<i64> {
87 let uuid = from_b64url_nopad(s)?;
88 to_time_epoch_ms(&uuid)
89}
90
91#[cfg(test)]
96mod tests {
97 type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>; use super::*;
100 use base64::engine::general_purpose as b64_gp;
101 use uuid::{Uuid, Version};
102
103 #[test]
104 fn test_extra_base64_new_v4_b64_simple() -> Result<()> {
105 let b64_uuid = new_v4_b64();
110
111 assert_eq!(
113 b64_uuid.len(),
114 24,
115 "Standard Base64 of UUID should be 24 chars with padding"
116 );
117 assert!(
118 b64_uuid.ends_with("=="),
119 "Standard Base64 should be padded with '==' for 16 bytes"
120 );
121 assert!(
122 !b64_uuid.contains('-') && !b64_uuid.contains('_'),
123 "Standard Base64 should not contain URL-safe characters '-' or '_'"
124 );
125
126 let decoded_bytes = b64_gp::STANDARD.decode(&b64_uuid)?;
128 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
129 assert_eq!(uuid.get_version(), Some(Version::Random));
130 Ok(())
131 }
132
133 #[test]
134 fn test_extra_base64_new_v4_b64url_simple() -> Result<()> {
135 let b64url_uuid = new_v4_b64url();
140
141 assert_eq!(
143 b64url_uuid.len(),
144 24,
145 "URL-safe Base64 of UUID should be 24 chars with padding"
146 );
147 assert!(
148 b64url_uuid.ends_with("=="),
149 "URL-safe Base64 with padding should be padded with '==' for 16 bytes"
150 );
151 assert!(
152 !b64url_uuid.contains('+') && !b64url_uuid.contains('/'),
153 "URL-safe Base64 should not contain '+' or '/'"
154 );
155
156 let decoded_bytes = b64_gp::URL_SAFE.decode(&b64url_uuid)?;
158 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
159 assert_eq!(uuid.get_version(), Some(Version::Random));
160 Ok(())
161 }
162
163 #[test]
164 fn test_extra_base64_new_v4_b64url_nopad_simple() -> Result<()> {
165 let b64url_nopad_uuid = new_v4_b64url_nopad();
170
171 assert_eq!(
173 b64url_nopad_uuid.len(),
174 22,
175 "URL-safe Base64 (no pad) of UUID should be 22 chars"
176 );
177 assert!(
178 !b64url_nopad_uuid.ends_with('='),
179 "URL-safe Base64 (no pad) should not have padding"
180 );
181 assert!(
182 !b64url_nopad_uuid.contains('+') && !b64url_nopad_uuid.contains('/'),
183 "URL-safe Base64 (no pad) should not contain '+' or '/'"
184 );
185
186 let decoded_bytes = b64_gp::URL_SAFE_NO_PAD.decode(&b64url_nopad_uuid)?;
188 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
189 assert_eq!(uuid.get_version(), Some(Version::Random));
190 Ok(())
191 }
192
193 #[test]
194 fn test_extra_base64_new_v7_b64_simple() -> Result<()> {
195 let b64_uuid = new_v7_b64();
200
201 assert_eq!(
203 b64_uuid.len(),
204 24,
205 "Standard Base64 of V7 UUID should be 24 chars with padding"
206 );
207 assert!(
208 b64_uuid.ends_with("=="),
209 "Standard Base64 should be padded with '==' for 16 bytes"
210 );
211 assert!(
212 !b64_uuid.contains('-') && !b64_uuid.contains('_'),
213 "Standard Base64 should not contain URL-safe characters '-' or '_'"
214 );
215
216 let decoded_bytes = b64_gp::STANDARD.decode(&b64_uuid)?;
218 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
219 assert_eq!(uuid.get_version(), Some(Version::SortRand));
220 Ok(())
221 }
222
223 #[test]
224 fn test_extra_base64_new_v7_b64url_simple() -> Result<()> {
225 let b64url_uuid = new_v7_b64url();
230
231 assert_eq!(
233 b64url_uuid.len(),
234 24,
235 "URL-safe Base64 of V7 UUID should be 24 chars with padding"
236 );
237 assert!(
238 b64url_uuid.ends_with("=="),
239 "URL-safe Base64 with padding should be padded with '==' for 16 bytes"
240 );
241 assert!(
242 !b64url_uuid.contains('+') && !b64url_uuid.contains('/'),
243 "URL-safe Base64 should not contain '+' or '/'"
244 );
245
246 let decoded_bytes = b64_gp::URL_SAFE.decode(&b64url_uuid)?;
248 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
249 assert_eq!(uuid.get_version(), Some(Version::SortRand));
250 Ok(())
251 }
252
253 #[test]
254 fn test_extra_base64_new_v7_b64url_nopad_simple() -> Result<()> {
255 let b64url_nopad_uuid = new_v7_b64url_nopad();
260
261 assert_eq!(
263 b64url_nopad_uuid.len(),
264 22,
265 "URL-safe Base64 (no pad) of V7 UUID should be 22 chars"
266 );
267 assert!(
268 !b64url_nopad_uuid.ends_with('='),
269 "URL-safe Base64 (no pad) should not have padding"
270 );
271 assert!(
272 !b64url_nopad_uuid.contains('+') && !b64url_nopad_uuid.contains('/'),
273 "URL-safe Base64 (no pad) should not contain '+' or '/'"
274 );
275
276 let decoded_bytes = b64_gp::URL_SAFE_NO_PAD.decode(&b64url_nopad_uuid)?;
278 let uuid = Uuid::from_bytes(decoded_bytes.try_into().map_err(|_| "Failed to convert vec to array")?);
279 assert_eq!(uuid.get_version(), Some(Version::SortRand));
280 Ok(())
281 }
282
283 #[test]
286 fn test_extra_base64_from_b64_ok() -> Result<()> {
287 let original_uuid = Uuid::new_v4();
289 let b64_string = b64_gp::STANDARD.encode(original_uuid.as_bytes());
290
291 let decoded_uuid_res = from_b64(&b64_string);
293
294 assert!(decoded_uuid_res.is_ok(), "Decoding should succeed");
296 assert_eq!(
297 decoded_uuid_res.unwrap(),
298 original_uuid,
299 "Decoded UUID should match original"
300 );
301 Ok(())
302 }
303
304 #[test]
305 fn test_extra_base64_from_b64_err_invalid_char() -> Result<()> {
306 let invalid_b64_string = "ThisIsNotValidBase64!=";
308
309 let decoded_uuid_res = from_b64(invalid_b64_string);
311
312 assert!(decoded_uuid_res.is_err(), "Decoding should fail for invalid characters");
314 let err_msg = decoded_uuid_res.err().unwrap().to_string();
315 assert!(
316 err_msg.contains("Invalid symbol"),
317 "Error message should indicate 'Invalid symbol'"
318 );
319 Ok(())
320 }
321
322 #[test]
323 fn test_extra_base64_from_b64_err_wrong_len() -> Result<()> {
324 let short_b64_string = b64_gp::STANDARD.encode("short"); let decoded_uuid_res = from_b64(&short_b64_string);
329
330 assert!(decoded_uuid_res.is_err(), "Decoding should fail for wrong length");
332 let err_msg = decoded_uuid_res.err().unwrap().to_string();
333 println!("->> {err_msg}");
334 assert!(
335 err_msg.contains("FailToDecode16U8"),
336 "Error message should indicate wrong length"
337 );
338 Ok(())
339 }
340
341 #[test]
342 fn test_extra_base64_from_b64url_ok() -> Result<()> {
343 let original_uuid = Uuid::new_v4();
345 let b64url_string = b64_gp::URL_SAFE.encode(original_uuid.as_bytes());
346
347 let decoded_uuid_res = from_b64url(&b64url_string);
349
350 assert!(decoded_uuid_res.is_ok(), "Decoding should succeed");
352 assert_eq!(
353 decoded_uuid_res.unwrap(),
354 original_uuid,
355 "Decoded UUID should match original"
356 );
357 Ok(())
358 }
359
360 #[test]
361 fn test_extra_base64_from_b64url_err_invalid_char() -> Result<()> {
362 let invalid_b64url_string = "ThisIsNotValidBase64Url+"; let decoded_uuid_res = from_b64url(invalid_b64url_string);
367
368 assert!(decoded_uuid_res.is_err(), "Decoding should fail for invalid characters");
370 Ok(())
371 }
372
373 #[test]
374 fn test_extra_base64_from_b64url_err_wrong_len() -> Result<()> {
375 let short_b64url_string = b64_gp::URL_SAFE.encode("short"); let decoded_uuid_res = from_b64url(&short_b64url_string);
380
381 assert!(decoded_uuid_res.is_err(), "Decoding should fail for wrong length");
383 let err_msg = decoded_uuid_res.err().unwrap().to_string();
384 assert!(
385 err_msg.contains("FailToDecode16U8 { context: \"base64url\""),
386 "Error message should indicate FailToDecode16U8 for base64url"
387 );
388 Ok(())
389 }
390
391 #[test]
392 fn test_extra_base64_from_b64url_nopad_ok() -> Result<()> {
393 let original_uuid = Uuid::new_v4();
395 let b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode(original_uuid.as_bytes());
396
397 let decoded_uuid_res = from_b64url_nopad(&b64url_nopad_string);
399
400 assert!(decoded_uuid_res.is_ok(), "Decoding should succeed");
402 assert_eq!(
403 decoded_uuid_res.unwrap(),
404 original_uuid,
405 "Decoded UUID should match original"
406 );
407 Ok(())
408 }
409
410 #[test]
411 fn test_extra_base64_from_b64url_nopad_err_invalid_char() -> Result<()> {
412 let invalid_b64url_nopad_string = "ThisIsNotValidBase64UrlNoPad="; let decoded_uuid_res = from_b64url_nopad(invalid_b64url_nopad_string);
417
418 assert!(decoded_uuid_res.is_err(), "Decoding should fail for invalid characters");
420 Ok(())
421 }
422
423 #[test]
424 fn test_extra_base64_from_b64url_nopad_err_wrong_len() -> Result<()> {
425 let short_b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode("short"); let decoded_uuid_res = from_b64url_nopad(&short_b64url_nopad_string);
430
431 assert!(decoded_uuid_res.is_err(), "Decoding should fail for wrong length");
433 let err_msg = decoded_uuid_res.err().unwrap().to_string();
434 assert!(
435 err_msg.contains("FailToDecode16U8 { context: \"base64url-nopad\""),
436 "Error message should indicate FailToDecode16U8 for base64url-nopad"
437 );
438 Ok(())
439 }
440
441 #[test]
444 fn test_extra_base64_b64_to_epoch_ms_ok() -> Result<()> {
445 let original_uuid = new_v7();
447 let b64_string = b64_gp::STANDARD.encode(original_uuid.as_bytes());
448 let original_ts = to_time_epoch_ms(&original_uuid)?;
449
450 let extracted_ts = b64_to_epoch_ms(&b64_string)?;
452
453 assert_eq!(extracted_ts, original_ts);
455 Ok(())
456 }
457
458 #[test]
459 fn test_extra_base64_b64_to_epoch_ms_err_not_v7() -> Result<()> {
460 let uuid_v4 = new_v4();
462 let b64_string = b64_gp::STANDARD.encode(uuid_v4.as_bytes());
463
464 let result = b64_to_epoch_ms(&b64_string);
466
467 assert!(result.is_err());
469 match result {
470 Err(Error::FailExtractTimeNoUuidV7(id)) => {
471 assert_eq!(id, uuid_v4);
472 }
473 _ => panic!("Expected FailExtractTimeNoUuidV7 error"),
474 }
475 Ok(())
476 }
477
478 #[test]
479 fn test_extra_base64_b64url_to_epoch_ms_ok() -> Result<()> {
480 let original_uuid = new_v7();
482 let b64url_string = b64_gp::URL_SAFE.encode(original_uuid.as_bytes());
483 let original_ts = to_time_epoch_ms(&original_uuid)?;
484
485 let extracted_ts = b64url_to_epoch_ms(&b64url_string)?;
487
488 assert_eq!(extracted_ts, original_ts);
490 Ok(())
491 }
492
493 #[test]
494 fn test_extra_base64_b64url_to_epoch_ms_err_not_v7() -> Result<()> {
495 let uuid_v4 = new_v4();
497 let b64url_string = b64_gp::URL_SAFE.encode(uuid_v4.as_bytes());
498
499 let result = b64url_to_epoch_ms(&b64url_string);
501
502 assert!(result.is_err());
504 match result {
505 Err(Error::FailExtractTimeNoUuidV7(id)) => {
506 assert_eq!(id, uuid_v4);
507 }
508 _ => panic!("Expected FailExtractTimeNoUuidV7 error"),
509 }
510 Ok(())
511 }
512
513 #[test]
514 fn test_extra_base64_b64url_nopad_to_epoch_ms_ok() -> Result<()> {
515 let original_uuid = new_v7();
517 let b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode(original_uuid.as_bytes());
518 let original_ts = to_time_epoch_ms(&original_uuid)?;
519
520 let extracted_ts = b64url_nopad_to_epoch_ms(&b64url_nopad_string)?;
522
523 assert_eq!(extracted_ts, original_ts);
525 Ok(())
526 }
527
528 #[test]
529 fn test_extra_base64_b64url_nopad_to_epoch_ms_err_not_v7() -> Result<()> {
530 let uuid_v4 = new_v4();
532 let b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode(uuid_v4.as_bytes());
533
534 let result = b64url_nopad_to_epoch_ms(&b64url_nopad_string);
536
537 assert!(result.is_err());
539 match result {
540 Err(Error::FailExtractTimeNoUuidV7(id)) => {
541 assert_eq!(id, uuid_v4);
542 }
543 _ => panic!("Expected FailExtractTimeNoUuidV7 error"),
544 }
545 Ok(())
546 }
547}
548
549