Skip to main content

rumtk_core/
strings.rs

1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2024  Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025  MedicalMasses L.L.C. <contact@medicalmasses.com>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 */
20use crate::core::{is_unique, RUMResult, RUMVec};
21use crate::types::RUMBuffer;
22use base64::prelude::*;
23use chardetng::EncodingDetector;
24use encoding_rs::Encoding;
25use std::cmp::min;
26pub use std::format as rumtk_format;
27use unicode_segmentation::UnicodeSegmentation;
28/**************************** Constants**************************************/
29const ESCAPED_STRING_WINDOW: usize = 6;
30const ASCII_ESCAPE_CHAR: char = '\\';
31const MIN_ASCII_READABLE: char = ' ';
32const MAX_ASCII_READABLE: char = '~';
33pub const EMPTY_STRING: &str = "";
34pub static EMPTY_RUMSTRING: RUMString = RUMString::default();
35pub const DOT_STR: &str = ".";
36pub const EMPTY_STRING_OPTION: Option<&str> = Some("");
37pub const READABLE_ASCII: &str = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
38
39/**************************** Types *****************************************/
40pub type RUMString = String;
41pub type EscapeException<'a> = (&'a str, &'a str);
42pub type EscapeExceptions<'a> = &'a [EscapeException<'a>];
43pub type StringReplacementPair<'a> = [(&'a str, &'a str)];
44pub type Grapheme<'a> = &'a str;
45pub type GraphemeStringView<'a> = RUMVec<Grapheme<'a>>;
46pub type GraphemePattern<'a> = &'a [Grapheme<'a>];
47pub type GraphemeSlice<'b, 'a> = &'b [Grapheme<'a>];
48pub type GraphemePatternPair<'a> = (GraphemePattern<'a>, GraphemePattern<'a>);
49
50///
51/// The equivalent to a `stringview` but at the grapheme level. Meaning, we can use this view to
52/// iterate through a string at the full `UTF8` implementation
53///
54#[derive(Default, Debug, PartialEq, Clone)]
55pub struct GraphemeStr<'a> {
56    view: GraphemeStringView<'a>,
57    start: usize,
58    end: usize,
59}
60
61impl<'a> GraphemeStr<'a> {
62    pub fn from(string: &'a str) -> Self {
63        let view = string.graphemes(true).collect::<GraphemeStringView>();
64        Self::from_view(view)
65    }
66
67    pub fn from_view(view: GraphemeStringView<'a>) -> Self {
68        let start = 0;
69        let end = view.len();
70        Self { view, start, end }
71    }
72
73    pub fn at(&self, index: usize) -> Grapheme<'a> {
74        self.view[index]
75    }
76
77    pub fn trim(&self, pattern: &GraphemePatternPair<'a>) -> Self {
78        let (left_pattern, right_pattern) = pattern;
79        self.trim_left(left_pattern).trim_right(right_pattern)
80    }
81
82    pub fn trim_left(&self, pattern: &GraphemePattern<'a>) -> Self {
83        let new_offset = self.find(pattern, self.start);
84        Self {
85            view: self.view.clone(),
86            start: new_offset,
87            end: self.end,
88        }
89    }
90
91    pub fn trim_right(&self, pattern: &GraphemePattern<'a>) -> Self {
92        let new_offset = self.rfind(pattern, self.end);
93        Self {
94            view: self.view.clone(),
95            start: self.start,
96            end: new_offset,
97        }
98    }
99
100    pub fn splice(&self, skip_pattern: &GraphemePatternPair<'a>) -> Self {
101        let (left_pattern, right_pattern) = skip_pattern;
102        let mut new_view = GraphemeStringView::with_capacity(self.end - self.start);
103        let mut offset = self.start;
104        let l_pattern_s = left_pattern.len();
105
106        while offset < self.end {
107            let target_s = self.find(left_pattern, offset) + l_pattern_s;
108            for i in offset..target_s {
109                new_view.push(self.view[i]);
110            }
111            offset = self.find(right_pattern, target_s);
112        }
113
114        GraphemeStr::from_view(new_view)
115    }
116
117    pub fn find(&self, pattern: &GraphemePattern<'a>, offset: usize) -> usize {
118        let pattern_s = pattern.len();
119        let mut new_offset = offset;
120        let mut pattern_end = new_offset + pattern_s;
121
122        while new_offset < self.end && pattern_end < self.end {
123            if self.view[new_offset..pattern_end] == **pattern {
124                break;
125            }
126
127            new_offset += 1;
128            pattern_end = new_offset + pattern_s;
129        }
130
131        new_offset
132    }
133
134    pub fn rfind(&self, pattern: &GraphemePattern<'a>, offset: usize) -> usize {
135        let pattern_s = pattern.len();
136        let mut new_offset = offset;
137        while new_offset > self.start {
138            if self.view[new_offset - pattern_s..new_offset] == **pattern {
139                break;
140            }
141
142            new_offset -= 1;
143        }
144
145        new_offset
146    }
147
148    pub fn len(&self) -> usize {
149        self.end - self.start
150    }
151
152    pub fn get_graphemes(&self) -> GraphemeSlice<'_, 'a> {
153        &self.view[self.start..self.end]
154    }
155
156    pub fn truncate(&self, size: usize) -> Self {
157        let end = min(size, self.end);
158        Self {
159            view: self.view.clone(),
160            start: self.start,
161            end,
162        }
163    }
164
165    pub fn is_unique(&self) -> bool {
166        is_unique(&self.view)
167    }
168}
169
170impl ToString for GraphemeStr<'_> {
171    fn to_string(&self) -> String {
172        let mut new_string = String::with_capacity(self.len());
173
174        for grapheme in self.view[self.start..self.end].iter() {
175            new_string.push_str(grapheme);
176        }
177
178        new_string
179    }
180}
181
182impl RUMStringConversions for GraphemeStr<'_> {}
183
184/**************************** Traits ****************************************/
185
186pub trait StringLike {
187    fn with_capacity(capacity: usize) -> Self;
188    fn push_str(&mut self, string: &str);
189}
190
191pub trait AsString {
192    fn as_string(&self) -> RUMString;
193}
194
195pub trait AsStr {
196    fn as_str(&self) -> &str;
197    fn as_grapheme_str(&self) -> GraphemeStr {
198        GraphemeStr::from(self.as_str())
199    }
200}
201
202pub trait RUMStringConversions: ToString {
203    #[inline(always)]
204    fn to_raw(&self) -> RUMVec<u8> {
205        self.to_string().as_bytes().to_vec()
206    }
207
208    #[inline(always)]
209    fn to_buffer(&self) -> RUMBuffer {
210        string_to_buffer(self.to_string().as_str())
211    }
212}
213
214pub trait StringUtils: AsStr + RUMStringConversions {
215    #[inline(always)]
216    fn duplicate(&self, count: usize) -> RUMString {
217        let mut duplicated = RUMString::with_capacity(count);
218        for i in 0..count {
219            duplicated += &self.as_str();
220        }
221        duplicated
222    }
223
224    fn truncate(&self, count: usize) -> RUMString {
225        self.as_grapheme_str().truncate(count).to_string()
226    }
227}
228
229impl AsStr for String {
230    fn as_str(&self) -> &str {
231        self.as_str()
232    }
233}
234
235impl RUMStringConversions for RUMString {}
236impl StringUtils for RUMString {}
237
238impl RUMStringConversions for str {}
239
240impl AsStr for str {
241    fn as_str(&self) -> &str {
242        self
243    }
244}
245
246impl StringUtils for str {}
247
248impl RUMStringConversions for char {}
249
250pub trait RUMArrayConversions {
251    fn to_string(&self) -> RUMResult<RUMString>;
252}
253
254impl RUMArrayConversions for Vec<u8> {
255    #[inline(always)]
256    fn to_string(&self) -> RUMResult<RUMString> {
257        match RUMString::from_utf8(self.to_owned()) {
258            Ok(s) => Ok(s),
259            Err(e) => Err(rumtk_format!("Failure to parse incoming UTF-8 string: {}", e))
260        }
261    }
262}
263
264impl RUMArrayConversions for &[u8] {
265    #[inline(always)]
266    fn to_string(&self) -> RUMResult<RUMString> {
267        match RUMString::from_utf8(self.to_vec()) {
268            Ok(s) => Ok(s),
269            Err(e) => Err(rumtk_format!("Failure to parse incoming UTF-8 string: {}", e))
270        }
271    }
272}
273
274impl AsString for u8 {
275    fn as_string(&self) -> RUMString {
276        RUMString::from(char::from_u32((*self).into()).unwrap_or_default())
277    }
278}
279
280/**************************** Helpers ***************************************/
281
282pub fn count_tokens_ignoring_pattern(vector: &Vec<&str>, string_token: &RUMString) -> usize {
283    let mut count: usize = 0;
284    for tok in vector.iter() {
285        if string_token != tok {
286            count += 1;
287        }
288    }
289    count
290}
291
292///
293/// Implements decoding this string from its auto-detected encoding to UTF-8.
294/// Failing that we assume the string was encoded in UTF-8 and return a copy.
295///
296/// Note => Decoding is facilitated via the crates chardet-ng and encoding_rs.
297///
298pub fn try_decode(src: &[u8]) -> RUMResult<RUMString> {
299    let mut detector = EncodingDetector::new();
300    detector.feed(&src, true);
301    let encoding = detector.guess(None, true);
302    decode(src, encoding)
303}
304
305///
306/// Implements decoding this string from a specific encoding to UTF-8.
307///
308/// Note => Decoding is facilitated via the crates chardet-ng and encoding_rs.
309///
310pub fn try_decode_with(src: &[u8], encoding_name: &str) -> RUMResult<RUMString> {
311    let encoding = match Encoding::for_label(encoding_name.as_bytes()) {
312        Some(v) => v,
313        None => return Ok(EMPTY_RUMSTRING.clone()),
314    };
315    decode(src, encoding)
316}
317
318///
319/// Implements decoding of input with encoder.
320///
321/// Note => Decoding is facilitated via the crate encoding_rs.
322///
323fn decode(src: &[u8], encoding: &'static Encoding) -> RUMResult<RUMString> {
324    Ok(match encoding.decode_without_bom_handling_and_without_replacement(&src) {
325        Some(res) => RUMString::from(res),
326        None => src.to_string()?,
327    })
328}
329
330///
331/// This function will scan through an escaped string and unescape any escaped characters.
332/// We collect these characters as a byte vector.
333/// Finally, we do a decode pass on the vector to re-encode the bytes **hopefully right** into a
334/// valid UTF-8 string.
335///
336/// This function focuses on reverting the result of [escape], whose output is meant for HL7.
337///
338pub fn unescape_string(escaped_str: &str) -> RUMResult<RUMString> {
339    let graphemes = escaped_str.graphemes(true).collect::<Vec<&str>>();
340    let str_size = graphemes.len();
341    let mut result: Vec<u8> = Vec::with_capacity(escaped_str.len());
342    let mut i = 0;
343    while i < str_size {
344        let seq_start = graphemes[i];
345        match seq_start {
346            "\\" => {
347                let escape_seq = get_grapheme_string(&graphemes, " ", i);
348                let mut c = match unescape(&escape_seq) {
349                    Ok(c) => c,
350                    Err(_why) => Vec::from(escape_seq.as_bytes()),
351                };
352                result.append(&mut c);
353                i += &escape_seq.as_grapheme_str().len();
354            }
355            _ => {
356                result.append(&mut Vec::from(seq_start.as_bytes()));
357                i += 1;
358            }
359        }
360    }
361    Ok(try_decode(result.as_slice())?)
362}
363
364///
365/// Get the grapheme block and concatenate it into a newly allocated [`RUMString`].
366///
367pub fn get_grapheme_string<'a>(
368    graphemes: &Vec<&'a str>,
369    end_grapheme: &str,
370    start_index: usize,
371) -> RUMString {
372    get_grapheme_collection(graphemes, end_grapheme, start_index).join("")
373}
374
375///
376/// Return vector of graphemes from starting spot up until we find the end grapheme.
377///
378/// Because a grapheme may take more than one codepoint characters, these have to be treated as
379/// references to strings.
380///
381pub fn get_grapheme_collection<'a>(
382    graphemes: &Vec<&'a str>,
383    end_grapheme: &str,
384    start_index: usize,
385) -> Vec<&'a str> {
386    let mut result: Vec<&'a str> = Vec::new();
387    for grapheme in graphemes.iter().skip(start_index) {
388        let item = *grapheme;
389        if item == end_grapheme {
390            break;
391        }
392        result.push(item);
393    }
394    result
395}
396
397///
398/// Turn escaped character sequence into the equivalent UTF-8 character
399/// This function accepts \o, \x and \u formats.
400/// This function will also attempt to unescape the common C style control characters.
401/// Anything else needs to be expressed as hex or octal patterns with the formats above.
402///
403/// If I did this right, I should get the "raw" byte sequence out of the escaped string.
404/// We can then use the bytes and attempt a decode() to figure out the string encoding and
405/// get the correct conversion to UTF-8. **Fingers crossed**
406///
407pub fn unescape(escaped_str: &str) -> Result<Vec<u8>, RUMString> {
408    let lower_case = escaped_str.to_lowercase();
409    let mut bytes: Vec<u8> = Vec::with_capacity(3);
410    match &lower_case[0..2] {
411        // Hex notation case. Assume we are getting xxyy bytes
412        "\\x" => {
413            let byte_str = number_to_char_unchecked(&hex_to_number(&lower_case[2..6])?);
414            bytes.append(&mut byte_str.as_bytes().to_vec());
415        }
416        // Unicode notation case, we need to do an extra step or we will lose key bytes.
417        "\\u" => {
418            let byte_str = number_to_char_unchecked(&hex_to_number(&lower_case[2..6])?);
419            bytes.append(&mut byte_str.as_bytes().to_vec());
420        }
421        // Single byte notation case
422        "\\c" => {
423            let byte_str = number_to_char_unchecked(&hex_to_number(&lower_case[2..6])?);
424            bytes.append(&mut byte_str.as_bytes().to_vec());
425        }
426        // Unicode notation case
427        "\\o" => {
428            let byte_str = number_to_char_unchecked(&octal_to_number(&lower_case[2..6])?);
429            bytes.append(&mut byte_str.as_bytes().to_vec());
430        }
431        // Multibyte notation case
432        "\\m" => match lower_case.as_grapheme_str().len() {
433            8 => {
434                bytes.push(hex_to_byte(&lower_case[2..4])?);
435                bytes.push(hex_to_byte(&lower_case[4..6])?);
436                bytes.push(hex_to_byte(&lower_case[6..8])?);
437            }
438            6 => {
439                bytes.push(hex_to_byte(&lower_case[2..4])?);
440                bytes.push(hex_to_byte(&lower_case[4..6])?);
441            }
442            _ => {
443                return Err(rumtk_format!(
444                    "Unknown multibyte sequence. Cannot decode {}",
445                    lower_case
446                ))
447            }
448        },
449        // Custom encoding
450        "\\z" => bytes.append(&mut lower_case.as_bytes().to_vec()),
451        // Single byte codes.
452        _ => bytes.push(unescape_control_byte(&lower_case[0..2])?),
453    }
454    Ok(bytes)
455}
456
457///
458/// Unescape basic character
459/// We use pattern matching to map the basic escape character to its corresponding integer value.
460///
461fn unescape_control(escaped_str: &str) -> Result<char, RUMString> {
462    match escaped_str {
463        // Common control sequences
464        "\\t" => Ok('\t'),
465        "\\b" => Ok('\x08'),
466        "\\n" => Ok('\n'),
467        "\\r" => Ok('\r'),
468        "\\f" => Ok('\x14'),
469        "\\s" => Ok('\x20'),
470        "\\\\" => Ok(ASCII_ESCAPE_CHAR),
471        "\\'" => Ok('\''),
472        "\\\"" => Ok('"'),
473        "\\0" => Ok('\0'),
474        "\\v" => Ok('\x0B'),
475        "\\a" => Ok('\x07'),
476        // Control sequences by
477        _ => Err(rumtk_format!(
478            "Unknown escape sequence? Sequence: {}!",
479            escaped_str
480        )),
481    }
482}
483
484///
485/// Unescape basic character
486/// We use pattern matching to map the basic escape character to its corresponding integer value.
487///
488fn unescape_control_byte(escaped_str: &str) -> Result<u8, RUMString> {
489    match escaped_str {
490        // Common control sequences
491        "\\t" => Ok(9),   // Tab/Character Tabulation
492        "\\b" => Ok(8),   // Backspace
493        "\\n" => Ok(10),  // New line/ Line Feed character
494        "\\r" => Ok(13),  // Carriage Return character
495        "\\f" => Ok(12),  // Form Feed
496        "\\s" => Ok(32),  // Space
497        "\\\\" => Ok(27), // Escape
498        "\\'" => Ok(39),  // Single quote
499        "\\\"" => Ok(34), // Double quote
500        "\\0" => Ok(0),   // Null character
501        "\\v" => Ok(11),  // Vertical Tab/Line Tabulation
502        "\\a" => Ok(7),   // Alert bell
503        // Control sequences by hex
504        //Err(rumtk_format!("Unknown escape sequence? Sequence: {}!", escaped_str))
505        _ => hex_to_byte(escaped_str),
506    }
507}
508
509///
510/// Turn hex string to number (u32)
511///
512fn hex_to_number(hex_str: &str) -> Result<u32, RUMString> {
513    match u32::from_str_radix(&hex_str, 16) {
514        Ok(result) => Ok(result),
515        Err(val) => Err(rumtk_format!(
516            "Failed to parse string with error {}! Input string {} \
517        is not hex string!",
518            val,
519            hex_str
520        )),
521    }
522}
523
524///
525/// Turn hex string to byte (u8)
526///
527fn hex_to_byte(hex_str: &str) -> Result<u8, RUMString> {
528    match u8::from_str_radix(&hex_str, 16) {
529        Ok(result) => Ok(result),
530        Err(val) => Err(rumtk_format!(
531            "Failed to parse string with error {}! Input string {} \
532        is not hex string!",
533            val,
534            hex_str
535        )),
536    }
537}
538
539///
540/// Turn octal string to number (u32)
541///
542fn octal_to_number(hoctal_str: &str) -> Result<u32, RUMString> {
543    match u32::from_str_radix(&hoctal_str, 8) {
544        Ok(result) => Ok(result),
545        Err(val) => Err(rumtk_format!(
546            "Failed to parse string with error {}! Input string {} \
547        is not an octal string!",
548            val,
549            hoctal_str
550        )),
551    }
552}
553
554///
555/// Turn octal string to byte (u32)
556///
557fn octal_to_byte(hoctal_str: &str) -> Result<u8, RUMString> {
558    match u8::from_str_radix(&hoctal_str, 8) {
559        Ok(result) => Ok(result),
560        Err(val) => Err(rumtk_format!(
561            "Failed to parse string with error {}! Input string {} \
562        is not an octal string!",
563            val,
564            hoctal_str
565        )),
566    }
567}
568
569///
570/// Turn number to UTF-8 char
571///
572fn number_to_char(num: &u32) -> Result<RUMString, RUMString> {
573    match char::from_u32(*num) {
574        Some(result) => Ok(result.to_string()),
575        None => Err(rumtk_format!(
576            "Failed to cast number to character! Number {}",
577            num
578        )),
579    }
580}
581
582///
583/// Turn number to UTF-8 char. Normally, calling from_u32 checks if the value is a valid character.
584/// This version uses the less safe from_u32_unchecked() function because we want to get the bytes
585/// and deal with validity at a higher layer.
586///
587fn number_to_char_unchecked(num: &u32) -> RUMString {
588    unsafe { char::from_u32_unchecked(*num).to_string() }
589}
590
591///
592/// Turn UTF-8 character into escaped character sequence as expected in HL7
593///
594/// # Example
595/// ```
596///  use rumtk_core::strings::{escape};
597///  let message = "I ❤ my wife!";
598///  let escaped_message = escape(&message);
599///  assert_eq!("I \\u2764 my wife!", &escaped_message, "Did not get expected escaped string! Got {}!", &escaped_message);
600///```
601///
602pub fn escape(unescaped_str: &str) -> RUMString {
603    basic_escape(unescaped_str, &vec![("{", ""), ("}", "")])
604}
605
606///
607/// Escape UTF-8 characters in UTF-8 string that are beyond ascii range
608///
609/// # Example
610/// ```
611///  use rumtk_core::strings::basic_escape;
612///  let message = "I ❤ my wife!";
613///  let escaped_message = basic_escape(&message, &vec![]);
614///  assert_eq!("I \\u{2764} my wife!", &escaped_message, "Did not get expected escaped string! Got {}!", &escaped_message);
615///```
616pub fn basic_escape(unescaped_str: &str, except: EscapeExceptions) -> RUMString {
617    let escaped = is_escaped_str(unescaped_str);
618    if !escaped {
619        let mut escaped_str = unescaped_str.escape_default().to_string();
620        for (from, to) in except {
621            escaped_str = escaped_str.replace(from, to);
622        }
623        return escaped_str.to_string();
624    }
625    unescaped_str.to_string()
626}
627
628///
629/// Checks if a given string is fully ASCII or within the ASCII range.
630///
631/// Remember: all strings are UTF-8 encoded in Rust, but most ASCII strings fit within the UTF-8
632/// encoding scheme.
633///
634pub fn is_ascii_str(unescaped_str: &str) -> bool {
635    unescaped_str.is_ascii()
636}
637
638///
639/// Checks if an input string is already escaped.
640/// The idea is to avoid escaping the escaped string thus making it a nightmare to undo the
641/// escaping later on.
642///
643/// Basically, if you were to blindly escape the input string, back slashes keep getting escaped.
644/// For example `\r -> \\r -> \\\\r -> ...`.
645///
646pub fn is_escaped_str(unescaped_str: &str) -> bool {
647    if !is_ascii_str(unescaped_str) {
648        return false;
649    }
650
651    for c in unescaped_str.chars() {
652        if !is_printable_char(&c) {
653            return false;
654        }
655    }
656    true
657}
658
659///
660/// Returns whether a character is in the ASCII printable range.
661///
662pub fn is_printable_char(c: &char) -> bool {
663    &MIN_ASCII_READABLE <= c && c <= &MAX_ASCII_READABLE
664}
665
666///
667/// Removes all non ASCII and all non printable characters from string.
668///
669pub fn filter_ascii(unescaped_str: &str, closure: fn(char) -> bool) -> RUMString {
670    let mut filtered = unescaped_str.to_string();
671    filtered.retain(closure);
672    filtered
673}
674
675///
676/// Removes all non ASCII and all non printable characters from string.
677///
678pub fn filter_non_printable_ascii(unescaped_str: &str) -> RUMString {
679    filter_ascii(unescaped_str, |c: char| is_printable_char(&c))
680}
681
682///
683/// Convert buffer to string.
684///
685/// ## Example
686/// ```
687/// use rumtk_core::buffers::{buffer_to_string};
688/// use rumtk_core::strings::{string_to_buffer};
689/// use rumtk_core::types::RUMBuffer;
690///
691/// const expected: &str = "Hello World!";
692/// let buffer = RUMBuffer::from_static(expected.as_bytes());
693/// let result = string_to_buffer(expected);
694///
695/// assert_eq!(result, expected, "str to RUMBuffer conversion failed!");
696/// ```
697///
698pub fn string_to_buffer(data: &str) -> RUMBuffer {
699    RUMBuffer::copy_from_slice(data.as_bytes())
700}
701
702///
703/// Given a set of keys and replacements, transform the input string.
704///
705/// ## Example
706/// ```
707/// use rumtk_core::strings::string_format;
708/// use rumtk_core::types::RUMBuffer;
709///
710/// const expected: &str = "Hello World!";
711/// const template: &str = "Hello {}!";
712/// let result = string_format(template, &[("{}", "World")]);
713///
714/// assert_eq!(result.as_str(), expected, "Formatting of string failed!");
715/// ```
716///
717pub fn string_format(input: &str, formatting: &StringReplacementPair) -> RUMString {
718    let mut output = String::from(input);
719
720    for item in formatting.iter() {
721        output = output.as_str().replace(item.0, item.1);
722    }
723
724    output.to_string()
725}
726
727///
728/// Convenience function for transforming a string into a `base64` encoded string.
729///
730/// ## Example
731/// ```
732///
733/// ```
734///
735pub fn string_to_b64(data: &str) -> String {
736    BASE64_STANDARD.encode(data)
737}
738
739///
740/// Convenience function for transforming a `base64` encoded string back to its original form.
741///
742/// ## Example
743/// ```
744/// ```
745///
746pub fn b64_to_string(data: &String) -> RUMResult<RUMVec<u8>> {
747    match BASE64_STANDARD.decode(data) {
748        Ok(result) => Ok(result),
749        Err(e) => Err(rumtk_format!("Failed to decode base64 string: {}", e)),
750    }
751}