weakauras_codec/
lib.rs

1// Copyright 2020-2025 Velithris
2// SPDX-License-Identifier: MIT
3
4//! This library provides routines for decoding and encoding [WeakAuras]-compatible strings.
5//!
6//! # Decoding example
7//!
8//! This is how you can use the library to decode WeakAuras-compatible strings.
9//!
10//! ```
11//! use weakauras_codec::{DecodeError, decode};
12//!
13//! fn main() -> Result<(), DecodeError> {
14//!     let expected_value = "Hello, world!".into();
15//!
16//!     assert_eq!(
17//!         decode(b"!lodJlypsnNCYxN6sO88lkNuumU4aaa", None)?.unwrap(),
18//!         expected_value
19//!     );
20//!     assert_eq!(
21//!         decode(b"!WA:2!JXl5rQ5Kt(6Oq55xuoPOiaa", Some(1024))?.unwrap(),
22//!         expected_value
23//!     );
24//!
25//!     Ok(())
26//! }
27//! ```
28//!
29//! # Encoding example
30//!
31//! This is how you can use the library to encode data as a WeakAuras-compatible string.
32//!
33//! ```
34//! use std::error::Error;
35//! use weakauras_codec::{OutputStringVersion, decode, encode};
36//!
37//! fn main() -> Result<(), Box<dyn Error>> {
38//!     let value = "Hello, world!".into();
39//!     let encoded_value_1 = encode(&value, OutputStringVersion::Deflate)?;
40//!     let encoded_value_2 = encode(&value, OutputStringVersion::BinarySerialization)?;
41//!
42//!     assert_eq!(decode(encoded_value_1.as_bytes(), None)?.unwrap(), value);
43//!     assert_eq!(decode(encoded_value_2.as_bytes(), None)?.unwrap(), value);
44//!
45//!     Ok(())
46//! }
47//! ```
48//!
49//! # Crate features
50//!
51//! * **legacy-strings-decoding** - Enable decoding of legacy WeakAuras-compatible strings. Uses a GPL-licensed library. **Disabled** by default.
52//! * **gpl-dependencies** - Enable GPL-licensed dependencies. Currently, it enables the `legacy-strings-decoding` feature. **Disabled** by default.
53//! * **flate2-rust-backend** - Enable the `rust-backend` feature in `flate2`. **Enabled** by default.
54//! * **flate2-zlib-rs** - Enable the `zlib-rs` feature in `flate2`. **Disabled** by default.
55//! * **flate2-zlib** - Enable the `zlib` feature in `flate2`. **Disabled** by default.
56//! * **flate2-zlib-ng** - Enable the `zlib-ng` feature in `flate2`. **Disabled** by default.
57//! * **flate2-zlib-ng-compat** - Enable the `zlib-ng-compat` feature in `flate2`. **Disabled** by default.
58//! * **flate2-cloudflare-zlib** - Enable the `cloudflare_zlib` feature in `flate2`. **Disabled** by default.
59//! * **lua-value-arbitrary** - Implement `arbitrary::Arbitrary` for [`LuaValue`]. **Disabled** by default.
60//! * **lua-value-fnv** - Use `fnv` instead of `BTreeMap` as the implementation of [`LuaValue::Map`]. **Disabled** by default.
61//! * **lua-value-indexmap** - Use `indexmap` instead of `BTreeMap` as the implementation of [`LuaValue::Map`]. **Disabled** by default.
62//! * **serde** - Allow serializing and deserializing [`LuaValue`] using `serde`. **Disabled** by default.
63//!
64//! [WeakAuras]: https://weakauras.wtf
65
66#![forbid(unsafe_code)]
67#![deny(missing_docs)]
68
69/// Error types.
70pub mod error;
71pub use error::*;
72
73use weakauras_codec_ace_serialize::{
74    Deserializer as LegacyDeserializer, Serializer as LegacySerializer,
75};
76use weakauras_codec_base64::error::DecodeError as Base64DecodeError;
77use weakauras_codec_lib_serialize::{Deserializer, Serializer};
78pub use weakauras_codec_lua_value::LuaValue;
79
80#[derive(Clone, Copy, PartialEq, Eq)]
81enum StringVersion {
82    #[cfg(feature = "legacy-strings-decoding")]
83    Legacy, // base64
84    Deflate,             // ! + base64
85    BinarySerialization, // !WA:2! + base64
86}
87
88/// A version of the string to be produced by [encode].
89#[derive(Clone, Copy, PartialEq, Eq)]
90pub enum OutputStringVersion {
91    /// `!` + base64-string
92    Deflate,
93    /// `!WA:2!` + base64-string
94    BinarySerialization,
95}
96
97/// Decodes a WeakAuras-compatible string and returns a [LuaValue].
98///
99/// The second argument, `max_size`, is used as a counter-DoS measure. Since the data
100/// is compressed, it's possible to construct a payload that would consume a lot of memory
101/// after decompression. `None` is equivalent to 16 MiB.
102///
103/// # Example
104///
105/// ```
106/// use weakauras_codec::{DecodeError, decode};
107///
108/// fn main() -> Result<(), DecodeError> {
109///     let expected_value = "Hello, world!".into();
110///
111///     assert_eq!(
112///         decode(b"!lodJlypsnNCYxN6sO88lkNuumU4aaa", None)?.unwrap(),
113///         expected_value
114///     );
115///     assert_eq!(
116///         decode(b"!WA:2!JXl5rQ5Kt(6Oq55xuoPOiaa", Some(1024))?.unwrap(),
117///         expected_value
118///     );
119///
120///     Ok(())
121/// }
122/// ```
123pub fn decode(data: &[u8], max_size: Option<usize>) -> Result<Option<LuaValue>, DecodeError> {
124    let (base64_data, version) = match data {
125        [b'!', b'W', b'A', b':', b'2', b'!', rest @ ..] => {
126            (rest, StringVersion::BinarySerialization)
127        }
128        [b'!', rest @ ..] => (rest, StringVersion::Deflate),
129        _ => {
130            #[cfg(feature = "legacy-strings-decoding")]
131            {
132                (data, StringVersion::Legacy)
133            }
134
135            #[cfg(not(feature = "legacy-strings-decoding"))]
136            return Err(DecodeError::InvalidPrefix);
137        }
138    };
139
140    let compressed_data = match weakauras_codec_base64::decode_to_vec(base64_data) {
141        Ok(compressed_data) => compressed_data,
142        Err(Base64DecodeError::InvalidByte(invalid_byte_at)) => {
143            let prefix_len = base64_data.as_ptr().addr() - data.as_ptr().addr();
144
145            return Err(DecodeError::Base64DecodeError(
146                Base64DecodeError::InvalidByte(prefix_len + invalid_byte_at),
147            ));
148        }
149        Err(e) => return Err(e.into()),
150    };
151
152    let max_size = max_size.unwrap_or(16 * 1024 * 1024);
153    #[cfg(feature = "legacy-strings-decoding")]
154    {
155        if version == StringVersion::Legacy {
156            let decoded = weakauras_codec_lib_compress::decompress(&compressed_data, max_size)?;
157            return LegacyDeserializer::from_str(&String::from_utf8_lossy(&decoded))
158                .deserialize_first()
159                .map_err(Into::into);
160        }
161    }
162
163    let decoded = {
164        use flate2::read::DeflateDecoder;
165        use std::io::prelude::*;
166
167        let mut result = Vec::new();
168        let mut inflater = DeflateDecoder::new(&compressed_data[..]).take(max_size as u64);
169
170        inflater.read_to_end(&mut result)?;
171
172        #[allow(clippy::unbuffered_bytes)] // inflater wraps in-memory data
173        if result.len() == max_size && inflater.into_inner().bytes().next().is_some() {
174            return Err(DecodeError::DataExceedsMaxSize);
175        }
176
177        result
178    };
179
180    Ok(if version == StringVersion::BinarySerialization {
181        Deserializer::from_slice(&decoded).deserialize_first()?
182    } else {
183        LegacyDeserializer::from_str(&String::from_utf8_lossy(&decoded)).deserialize_first()?
184    })
185}
186
187/// Encodes a [LuaValue] into a WeakAuras-compatible string.
188///
189/// # Example
190///
191/// ```
192/// use std::error::Error;
193/// use weakauras_codec::{OutputStringVersion, decode, encode};
194///
195/// fn main() -> Result<(), Box<dyn Error>> {
196///     let value = "Hello, world!".into();
197///     let encoded_value_1 = encode(&value, OutputStringVersion::Deflate)?;
198///     let encoded_value_2 = encode(&value, OutputStringVersion::BinarySerialization)?;
199///
200///     assert_eq!(decode(encoded_value_1.as_bytes(), None)?.unwrap(), value);
201///     assert_eq!(decode(encoded_value_2.as_bytes(), None)?.unwrap(), value);
202///
203///     Ok(())
204/// }
205/// ```
206pub fn encode(
207    value: &LuaValue,
208    string_version: OutputStringVersion,
209) -> Result<String, EncodeError> {
210    let (serialized, prefix) = match string_version {
211        OutputStringVersion::Deflate => (
212            LegacySerializer::serialize_one(value, None).map(|v| v.into_bytes())?,
213            "!",
214        ),
215        OutputStringVersion::BinarySerialization => {
216            (Serializer::serialize_one(value, None)?, "!WA:2!")
217        }
218    };
219
220    let compressed = {
221        use flate2::{Compression, read::DeflateEncoder};
222        use std::io::prelude::*;
223
224        let mut result = Vec::new();
225        let mut deflater = DeflateEncoder::new(serialized.as_slice(), Compression::best());
226
227        deflater.read_to_end(&mut result)?;
228        result
229    };
230
231    Ok(weakauras_codec_base64::encode_to_string_with_prefix(
232        &compressed,
233        prefix,
234    )?)
235}