mod_encode/
lib.rs

1use charset_normalizer_rs::Encoding as CharsetEncoding;
2use config::{any_err, get_or_create_sub_module};
3use data_encoding::{
4    Encoding, BASE32, BASE32HEX, BASE32HEX_NOPAD, BASE32_NOPAD, BASE64, BASE64URL, BASE64URL_NOPAD,
5    BASE64_NOPAD, HEXLOWER,
6};
7use mlua::{Lua, Value};
8
9/// data_encoding is very strict when considering padding, making it
10/// incompatible with a number of base64 encoders that apply excess
11/// padding in certain situations.
12/// This decode wrapper considers whether the encoder allows padding,
13/// and if so, speculatively removes any trailing padding bytes from
14/// the string and instead uses the no-padding variant of the encoder
15/// specification in order to avoid triggering any length/padding
16/// checks inside the crate.
17fn decode(enc: &Encoding, data: &[u8]) -> mlua::Result<Vec<u8>> {
18    let mut spec = enc.specification();
19    if let Some(pad_char) = spec.padding {
20        let padding_bytes = [pad_char as u8];
21        let mut stripped = data;
22        while let Some(s) = stripped.strip_suffix(&padding_bytes) {
23            stripped = s;
24        }
25        spec.padding.take();
26        return spec
27            .encoding()
28            .map_err(any_err)?
29            .decode(stripped)
30            .map_err(any_err);
31    }
32    enc.decode(data).map_err(any_err)
33}
34
35pub fn register(lua: &Lua) -> anyhow::Result<()> {
36    let digest_mod = get_or_create_sub_module(lua, "encode")?;
37
38    for (name, enc) in [
39        ("base32", BASE32),
40        ("base32hex", BASE32HEX),
41        ("base32hex_nopad", BASE32HEX_NOPAD),
42        ("base32_nopad", BASE32_NOPAD),
43        ("base64", BASE64),
44        ("base64url", BASE64URL),
45        ("base64url_nopad", BASE64URL_NOPAD),
46        ("base64_nopad", BASE64_NOPAD),
47        ("hex", HEXLOWER),
48    ] {
49        let encoder = enc.clone();
50        digest_mod.set(
51            format!("{name}_encode"),
52            lua.create_function(move |_, data: mlua::Value| match data {
53                Value::String(s) => Ok(encoder.encode(&s.as_bytes())),
54                _ => Err(mlua::Error::external(
55                    "parameter must be a string".to_string(),
56                )),
57            })?,
58        )?;
59        digest_mod.set(
60            format!("{name}_decode"),
61            lua.create_function(move |lua, data: String| {
62                let bytes = decode(&enc, data.as_bytes())?;
63                lua.create_string(&bytes)
64            })?,
65        )?;
66    }
67
68    digest_mod.set(
69        "charset_decode",
70        lua.create_function(
71            move |_lua, (charset, input_bytes): (String, mlua::String)| {
72                let encoding = CharsetEncoding::by_name(&charset)
73                    .ok_or_else(|| mlua::Error::external(format!("unknown charset {charset}")))?;
74                encoding
75                    .decode_simple(&input_bytes.as_bytes())
76                    .map_err(|err| {
77                        mlua::Error::external(format!(
78                            "input string did not decode from {charset} bytes: {err}"
79                        ))
80                    })
81            },
82        )?,
83    )?;
84
85    digest_mod.set(
86        "charset_encode",
87        lua.create_function(
88            move |lua, (charset, input_string, ignore_errors): (String, String, Option<bool>)| {
89                let encoding = CharsetEncoding::by_name(&charset)
90                    .ok_or_else(|| mlua::Error::external(format!("unknown charset {charset}")))?;
91                let ignore_errors = ignore_errors.unwrap_or(true);
92                let output_bytes =
93                    encoding
94                        .encode(&input_string, ignore_errors)
95                        .map_err(|err| {
96                            mlua::Error::external(format!(
97                                "input string did not encode into {charset} bytes: {err}"
98                            ))
99                        })?;
100                lua.create_string(output_bytes)
101            },
102        )?,
103    )?;
104
105    Ok(())
106}
107
108#[cfg(test)]
109#[test]
110fn test_decode_padding() {
111    assert_eq!(
112        std::str::from_utf8(&decode(&BASE64, b"MmVtYWlsLmxvZwAuY3N2").unwrap()).unwrap(),
113        "2email.log\0.csv"
114    );
115    assert_eq!(
116        std::str::from_utf8(&decode(&BASE64, b"MmVtYWlsLmxvZwAuY3N2=").unwrap()).unwrap(),
117        "2email.log\0.csv"
118    );
119    assert_eq!(
120        std::str::from_utf8(&decode(&BASE64, b"MmVtYWlsLmxvZwAuY3N2==").unwrap()).unwrap(),
121        "2email.log\0.csv"
122    );
123    assert_eq!(
124        std::str::from_utf8(&decode(&BASE64, b"MmVtYWlsLmxvZwAuY3N2===").unwrap()).unwrap(),
125        "2email.log\0.csv"
126    );
127}