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}