uuid_extra/
extra_base64.rs

1use 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
6// region:    --- v4
7
8/// Generates a new UUID version 4 and encodes it using standard Base64.
9pub fn new_v4_b64() -> String {
10	let uuid = new_v4();
11	general_purpose::STANDARD.encode(uuid.as_bytes())
12}
13
14/// Generates a new UUID version 4 and encodes it using URL-safe Base64.
15pub fn new_v4_b64url() -> String {
16	let uuid = new_v4();
17	general_purpose::URL_SAFE.encode(uuid.as_bytes())
18}
19
20/// Generates a new UUID version 4 and encodes it using URL-safe Base64 without padding.
21pub fn new_v4_b64url_nopad() -> String {
22	let uuid = new_v4();
23	general_purpose::URL_SAFE_NO_PAD.encode(uuid.as_bytes())
24}
25
26// endregion: --- v4
27
28// region:    --- v7
29
30/// Generates a new UUID version 7 and encodes it using standard Base64.
31pub fn new_v7_b64() -> String {
32	let uuid = new_v7();
33	general_purpose::STANDARD.encode(uuid.as_bytes())
34}
35
36/// Generates a new UUID version 7 and encodes it using URL-safe Base64.
37pub fn new_v7_b64url() -> String {
38	let uuid = new_v7();
39	general_purpose::URL_SAFE.encode(uuid.as_bytes())
40}
41
42/// Generates a new UUID version 7 and encodes it using URL-safe Base64 without padding.
43pub fn new_v7_b64url_nopad() -> String {
44	let uuid = new_v7();
45	general_purpose::URL_SAFE_NO_PAD.encode(uuid.as_bytes())
46}
47
48// endregion: --- v7
49
50// region:    --- From String
51
52/// Decodes a standard Base64 encoded string into a UUID.
53pub 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
58/// Decodes a standard Base64 encoded string into an epoch millisecond timestamp.
59/// This function is valid only for UUID v7.
60pub fn b64_to_epoch_ms(s: &str) -> Result<i64> {
61	let uuid = from_b64(s)?;
62	to_time_epoch_ms(&uuid)
63}
64
65/// Decodes a URL-safe Base64 encoded string (with padding) into a UUID.
66pub 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
71/// Decodes a URL-safe Base64 encoded string (with padding) into an epoch millisecond timestamp.
72/// This function is valid only for UUID v7.
73pub fn b64url_to_epoch_ms(s: &str) -> Result<i64> {
74	let uuid = from_b64url(s)?;
75	to_time_epoch_ms(&uuid)
76}
77
78/// Decodes a URL-safe Base64 encoded string (without padding) into a UUID.
79pub 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
84/// Decodes a URL-safe Base64 encoded string (without padding) into an epoch millisecond timestamp.
85/// This function is valid only for UUID v7.
86pub 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// endregion: --- From String
92
93// region:    --- Tests
94
95#[cfg(test)]
96mod tests {
97	type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>; // For tests.
98
99	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		// -- Setup & Fixtures
106		// (no specific setup needed for this test)
107
108		// -- Exec
109		let b64_uuid = new_v4_b64();
110
111		// -- Check
112		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		// Decode and verify UUID
127		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		// -- Setup & Fixtures
136		// (no specific setup needed for this test)
137
138		// -- Exec
139		let b64url_uuid = new_v4_b64url();
140
141		// -- Check
142		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		// Decode and verify UUID
157		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		// -- Setup & Fixtures
166		// (no specific setup needed for this test)
167
168		// -- Exec
169		let b64url_nopad_uuid = new_v4_b64url_nopad();
170
171		// -- Check
172		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		// Decode and verify UUID
187		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		// -- Setup & Fixtures
196		// (no specific setup needed for this test)
197
198		// -- Exec
199		let b64_uuid = new_v7_b64();
200
201		// -- Check
202		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		// Decode and verify UUID
217		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		// -- Setup & Fixtures
226		// (no specific setup needed for this test)
227
228		// -- Exec
229		let b64url_uuid = new_v7_b64url();
230
231		// -- Check
232		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		// Decode and verify UUID
247		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		// -- Setup & Fixtures
256		// (no specific setup needed for this test)
257
258		// -- Exec
259		let b64url_nopad_uuid = new_v7_b64url_nopad();
260
261		// -- Check
262		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		// Decode and verify UUID
277		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	// region:    --- Tests for from_... functions
284
285	#[test]
286	fn test_extra_base64_from_b64_ok() -> Result<()> {
287		// -- Setup & Fixtures
288		let original_uuid = Uuid::new_v4();
289		let b64_string = b64_gp::STANDARD.encode(original_uuid.as_bytes());
290
291		// -- Exec
292		let decoded_uuid_res = from_b64(&b64_string);
293
294		// -- Check
295		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		// -- Setup & Fixtures
307		let invalid_b64_string = "ThisIsNotValidBase64!=";
308
309		// -- Exec
310		let decoded_uuid_res = from_b64(invalid_b64_string);
311
312		// -- Check
313		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		// -- Setup & Fixtures
325		let short_b64_string = b64_gp::STANDARD.encode("short"); // Decodes to 5 bytes
326
327		// -- Exec
328		let decoded_uuid_res = from_b64(&short_b64_string);
329
330		// -- Check
331		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		// -- Setup & Fixtures
344		let original_uuid = Uuid::new_v4();
345		let b64url_string = b64_gp::URL_SAFE.encode(original_uuid.as_bytes());
346
347		// -- Exec
348		let decoded_uuid_res = from_b64url(&b64url_string);
349
350		// -- Check
351		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		// -- Setup & Fixtures
363		let invalid_b64url_string = "ThisIsNotValidBase64Url+"; // '+' is not valid for URL_SAFE if not part of encoding
364
365		// -- Exec
366		let decoded_uuid_res = from_b64url(invalid_b64url_string);
367
368		// -- Check
369		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		// -- Setup & Fixtures
376		let short_b64url_string = b64_gp::URL_SAFE.encode("short"); // Decodes to 5 bytes
377
378		// -- Exec
379		let decoded_uuid_res = from_b64url(&short_b64url_string);
380
381		// -- Check
382		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		// -- Setup & Fixtures
394		let original_uuid = Uuid::new_v4();
395		let b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode(original_uuid.as_bytes());
396
397		// -- Exec
398		let decoded_uuid_res = from_b64url_nopad(&b64url_nopad_string);
399
400		// -- Check
401		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		// -- Setup & Fixtures
413		let invalid_b64url_nopad_string = "ThisIsNotValidBase64UrlNoPad="; // '=' is not valid for NO_PAD
414
415		// -- Exec
416		let decoded_uuid_res = from_b64url_nopad(invalid_b64url_nopad_string);
417
418		// -- Check
419		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		// -- Setup & Fixtures
426		let short_b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode("short"); // Decodes to 5 bytes
427
428		// -- Exec
429		let decoded_uuid_res = from_b64url_nopad(&short_b64url_nopad_string);
430
431		// -- Check
432		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	// endregion: --- Tests for from_... functions
442
443	#[test]
444	fn test_extra_base64_b64_to_epoch_ms_ok() -> Result<()> {
445		// -- Setup & Fixtures
446		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		// -- Exec
451		let extracted_ts = b64_to_epoch_ms(&b64_string)?;
452
453		// -- Check
454		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		// -- Setup & Fixtures
461		let uuid_v4 = new_v4();
462		let b64_string = b64_gp::STANDARD.encode(uuid_v4.as_bytes());
463
464		// -- Exec
465		let result = b64_to_epoch_ms(&b64_string);
466
467		// -- Check
468		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		// -- Setup & Fixtures
481		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		// -- Exec
486		let extracted_ts = b64url_to_epoch_ms(&b64url_string)?;
487
488		// -- Check
489		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		// -- Setup & Fixtures
496		let uuid_v4 = new_v4();
497		let b64url_string = b64_gp::URL_SAFE.encode(uuid_v4.as_bytes());
498
499		// -- Exec
500		let result = b64url_to_epoch_ms(&b64url_string);
501
502		// -- Check
503		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		// -- Setup & Fixtures
516		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		// -- Exec
521		let extracted_ts = b64url_nopad_to_epoch_ms(&b64url_nopad_string)?;
522
523		// -- Check
524		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		// -- Setup & Fixtures
531		let uuid_v4 = new_v4();
532		let b64url_nopad_string = b64_gp::URL_SAFE_NO_PAD.encode(uuid_v4.as_bytes());
533
534		// -- Exec
535		let result = b64url_nopad_to_epoch_ms(&b64url_nopad_string);
536
537		// -- Check
538		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// endregion: --- Tests