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