mod map;
pub use map::{
generate_map, analyze_map, get_expansion_ratio,
GenerateMapOptions, TemperatureConfig, MapAnalysis,
get_temperature_profile, get_config_from_temperature,
};
use crate::Result;
use alloc::{
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use serde::{Deserialize, Serialize};
pub type ObfuscationMap = BTreeMap<char, Vec<String>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObfuscationResult {
pub obfuscated: String,
pub stats: ObfuscationStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObfuscationStats {
pub original_length: usize,
pub obfuscated_length: usize,
pub expansion_ratio: f64,
pub unique_chars_obfuscated: usize,
pub mappings_used: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SelectionStrategy {
#[default]
Random,
RoundRobin,
Shortest,
Longest,
}
#[derive(Debug, Clone)]
pub struct ObfuscationOptions {
pub seed: String,
pub strategy: SelectionStrategy,
}
impl Default for ObfuscationOptions {
fn default() -> Self {
Self {
seed: "default-seed".to_string(),
strategy: SelectionStrategy::Random,
}
}
}
pub const DELIMITER: char = '\x1F';
struct SeededRandom {
state: u64,
}
impl SeededRandom {
fn new(seed: &str) -> Self {
let mut hash: u64 = 0;
for (i, byte) in seed.bytes().enumerate() {
hash = hash.wrapping_add((byte as u64).wrapping_mul(31u64.wrapping_pow(i as u32)));
}
Self { state: hash.max(1) }
}
fn next(&mut self) -> u64 {
self.state = self.state.wrapping_mul(9301).wrapping_add(49297) % 233280;
self.state
}
fn next_usize(&mut self, max: usize) -> usize {
(self.next() as usize) % max
}
}
pub fn obfuscate(
text: &str,
map: &ObfuscationMap,
options: Option<ObfuscationOptions>,
) -> ObfuscationResult {
let opts = options.unwrap_or_default();
let _rng = SeededRandom::new(&opts.seed);
let mut tokens: Vec<String> = Vec::with_capacity(text.len());
let mut unique_chars = std::collections::HashSet::new();
let mut mappings_used = 0;
for (i, char) in text.chars().enumerate() {
if let Some(mappings) = map.get(&char) {
if !mappings.is_empty() {
unique_chars.insert(char);
let selected = match opts.strategy {
SelectionStrategy::Random => {
let entropy = (char as u64).wrapping_add(i as u64);
let mut local_rng = SeededRandom::new(&format!("{}{}", opts.seed, entropy));
&mappings[local_rng.next_usize(mappings.len())]
}
SelectionStrategy::RoundRobin => {
&mappings[i % mappings.len()]
}
SelectionStrategy::Shortest => {
mappings.iter().min_by_key(|s| s.len()).unwrap()
}
SelectionStrategy::Longest => {
mappings.iter().max_by_key(|s| s.len()).unwrap()
}
};
tokens.push(selected.clone());
mappings_used += 1;
continue;
}
}
if char == DELIMITER {
tokens.push("_DELIM_".to_string());
} else {
tokens.push(char.to_string());
}
}
let obfuscated = tokens.join(&DELIMITER.to_string());
let original_length = text.len();
let obfuscated_length = obfuscated.len();
ObfuscationResult {
obfuscated,
stats: ObfuscationStats {
original_length,
obfuscated_length,
expansion_ratio: obfuscated_length as f64 / original_length as f64,
unique_chars_obfuscated: unique_chars.len(),
mappings_used,
},
}
}
pub fn deobfuscate(obfuscated: &str, map: &ObfuscationMap) -> String {
let mut reverse_map: BTreeMap<&str, char> = BTreeMap::new();
for (char, mappings) in map.iter() {
for mapping in mappings {
reverse_map.insert(mapping.as_str(), *char);
}
}
obfuscated
.split(DELIMITER)
.map(|token| {
if token == "_DELIM_" {
return DELIMITER.to_string();
}
if let Some(&original) = reverse_map.get(token) {
original.to_string()
} else {
token.to_string()
}
})
.collect()
}
pub fn test_roundtrip(
text: &str,
map: &ObfuscationMap,
options: Option<ObfuscationOptions>,
) -> Result<bool> {
let result = obfuscate(text, map, options);
let deobfuscated = deobfuscate(&result.obfuscated, map);
Ok(text == deobfuscated)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_map() -> ObfuscationMap {
let mut map = ObfuscationMap::new();
map.insert('a', vec!["alpha".to_string(), "apple".to_string()]);
map.insert('b', vec!["bravo".to_string(), "banana".to_string()]);
map.insert('c', vec!["charlie".to_string()]);
map.insert(' ', vec!["_space_".to_string()]);
map
}
#[test]
fn test_obfuscate_deobfuscate() {
let map = create_test_map();
let text = "abc";
let result = obfuscate(text, &map, None);
let deobfuscated = deobfuscate(&result.obfuscated, &map);
assert_eq!(text, deobfuscated);
}
#[test]
fn test_deterministic_with_seed() {
let map = create_test_map();
let text = "abc";
let options = ObfuscationOptions {
seed: "test-seed".to_string(),
strategy: SelectionStrategy::Random,
};
let result1 = obfuscate(text, &map, Some(options.clone()));
let result2 = obfuscate(text, &map, Some(options));
assert_eq!(result1.obfuscated, result2.obfuscated);
}
#[test]
fn test_different_seeds() {
let map = create_test_map();
let text = "aaa";
let result1 = obfuscate(text, &map, Some(ObfuscationOptions {
seed: "seed1".to_string(),
strategy: SelectionStrategy::Random,
}));
let result2 = obfuscate(text, &map, Some(ObfuscationOptions {
seed: "seed2".to_string(),
strategy: SelectionStrategy::Random,
}));
assert_eq!(deobfuscate(&result1.obfuscated, &map), text);
assert_eq!(deobfuscate(&result2.obfuscated, &map), text);
}
#[test]
fn test_selection_strategies() {
let map = create_test_map();
let text = "a";
let shortest = obfuscate(text, &map, Some(ObfuscationOptions {
seed: "test".to_string(),
strategy: SelectionStrategy::Shortest,
}));
let longest = obfuscate(text, &map, Some(ObfuscationOptions {
seed: "test".to_string(),
strategy: SelectionStrategy::Longest,
}));
assert_eq!(deobfuscate(&shortest.obfuscated, &map), "a");
assert_eq!(deobfuscate(&longest.obfuscated, &map), "a");
}
#[test]
fn test_unmapped_characters() {
let map = create_test_map();
let text = "a1b2c";
let result = obfuscate(text, &map, None);
let deobfuscated = deobfuscate(&result.obfuscated, &map);
assert_eq!(text, deobfuscated);
}
#[test]
fn test_roundtrip_fn() {
let map = create_test_map();
let text = "abc test";
assert!(super::test_roundtrip(text, &map, None).unwrap());
}
}