use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::{Arc, Mutex, OnceLock};
use rand::SeedableRng;
use rand::rngs::StdRng;
use crate::codec::{self, DataMode};
use crate::generator::data::{
load_payload_words_for_wordlist, build_pos_mapping_for_wordlist,
load_cover_words_by_pos_for_wordlist, detect_dialect,
default_wordlist,
};
use crate::generator::types::{PayloadTok, Lexicon, GenerationMode, SentenceLengthMode};
use crate::generator::core::{generate_text_with_original_payload, generate_text_best_of};
use crate::grammar::{Grammar, DialectConfig};
use crate::merkle::WordlistTree;
use crate::types::Pos;
static PAYLOAD_TREE_CACHE: OnceLock<Mutex<HashMap<String, Arc<WordlistTree>>>> = OnceLock::new();
pub fn cached_payload_tree(language: &str, wordlist: &str) -> Result<Arc<WordlistTree>, PipelineError> {
let key = format!("{}:{}", language, wordlist);
let cache = PAYLOAD_TREE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(tree) = cache.lock().unwrap().get(&key) {
return Ok(tree.clone());
}
let words = load_payload_words_for_wordlist(language, wordlist)
.map_err(|e| PipelineError::DecodeError(e))?;
let tree = Arc::new(WordlistTree::new(words));
cache.lock().unwrap().insert(key, tree.clone());
Ok(tree)
}
#[derive(Clone, Debug, PartialEq)]
pub enum Endpoint {
Language {
language: String,
wordlist: String,
dialect: String,
},
Format(DataMode),
Auto,
}
impl Endpoint {
pub fn language(name: &str) -> Self {
Endpoint::Language {
language: name.to_string(),
wordlist: "default".to_string(),
dialect: "body".to_string(),
}
}
pub fn language_with_dialect(name: &str, dialect: &str) -> Self {
Endpoint::Language {
language: name.to_string(),
wordlist: "default".to_string(),
dialect: dialect.to_string(),
}
}
pub fn language_full(name: &str, wordlist: &str, dialect: &str) -> Self {
Endpoint::Language {
language: name.to_string(),
wordlist: wordlist.to_string(),
dialect: dialect.to_string(),
}
}
}
impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Endpoint::Language { language, wordlist, dialect } => {
write!(f, "{}/{}/{}", language, wordlist, dialect)
}
Endpoint::Format(mode) => write!(f, "{}", mode),
Endpoint::Auto => write!(f, "auto"),
}
}
}
#[derive(Debug, Clone)]
pub enum PipelineError {
ParseError(String),
InvalidPipeline(String),
EncodeError(String),
DecodeError(String),
DetectionFailed(String),
}
impl fmt::Display for PipelineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PipelineError::ParseError(msg) => write!(f, "parse error: {}", msg),
PipelineError::InvalidPipeline(msg) => write!(f, "invalid pipeline: {}", msg),
PipelineError::EncodeError(msg) => write!(f, "encode error: {}", msg),
PipelineError::DecodeError(msg) => write!(f, "decode error: {}", msg),
PipelineError::DetectionFailed(msg) => write!(f, "detection failed: {}", msg),
}
}
}
impl std::error::Error for PipelineError {}
pub struct PipelineResult {
pub output: String,
pub payload_words: Vec<String>,
pub data_mode: Option<DataMode>,
pub stats: Option<PipelineStats>,
pub resolved_source: Option<Endpoint>,
}
pub struct PipelineStats {
pub payload_count: usize,
pub cover_count: usize,
pub total_words: usize,
pub ratio: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Verb {
#[default]
Unspecified,
Encode,
Decode,
Transcode,
}
fn classify_verb(word: &str) -> Option<Verb> {
match word {
"encode" => Some(Verb::Encode),
"decode" => Some(Verb::Decode),
"transcode" | "translate" => Some(Verb::Transcode),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct Pipeline {
pub source: Endpoint,
pub target: Endpoint,
pub verb: Verb,
pub seed: u64,
pub verbose: bool,
pub html: bool,
pub length_mode: Option<SentenceLengthMode>,
pub variations: Option<usize>,
pub k_max: Option<usize>,
pub k_min: Option<usize>,
}
fn detect_crypto_format(input: &str) -> Option<(&str, &str, &str)> {
let lower = input.to_lowercase();
if lower.starts_with("bc1q") { return Some(("crypto/btc", "bip173", &input[4..])); }
if lower.starts_with("bc1p") { return Some(("crypto/btc", "bip350", &input[4..])); }
if lower.starts_with("tb1q") { return Some(("crypto/btc", "bip173-test", &input[4..])); }
if lower.starts_with("tb1p") { return Some(("crypto/btc", "bip350-test", &input[4..])); }
if lower.starts_with("npub1") { return Some(("crypto/nostr", "pub", &input[5..])); }
if lower.starts_with("nsec1") { return Some(("crypto/nostr", "sec", &input[5..])); }
if lower.starts_with("note1") { return Some(("crypto/nostr", "note", &input[5..])); }
if lower.starts_with("lnbc") { return Some(("crypto/ln", "main", &input[4..])); }
if lower.starts_with("lntb") { return Some(("crypto/ln", "test", &input[4..])); }
if input.starts_with("cashuA") { return Some(("crypto/cashu", "a", &input[6..])); }
if input.starts_with("cashuB") { return Some(("crypto/cashu", "b", &input[6..])); }
None
}
fn crypto_payload_type(language: &str, dialect: &str) -> &'static str {
if language == "crypto/btc" && (dialect == "p2pkh" || dialect == "p2pkh-test") {
return "base58";
}
match language {
"crypto/btc" | "crypto/nostr" | "crypto/ln" => "bech32",
"crypto/cashu" => "base64url",
_ => "bech32",
}
}
fn crypto_dialect_prefix(language: &str, dialect: &str) -> &'static str {
match (language, dialect) {
("crypto/btc", "bip173") => "bc1q",
("crypto/btc", "bip173-test") => "tb1q",
("crypto/btc", "bip350") => "bc1p",
("crypto/btc", "bip350-test") => "tb1p",
("crypto/btc", "p2pkh") => "",
("crypto/btc", "p2pkh-test") => "",
("crypto/nostr", "pub") => "npub1",
("crypto/nostr", "sec") => "nsec1",
("crypto/nostr", "note") => "note1",
("crypto/ln", "main") => "lnbc",
("crypto/ln", "test") => "lntb",
("crypto/cashu", "a") => "cashuA",
("crypto/cashu", "b") => "cashuB",
_ => "",
}
}
fn meta_word_to_format(word: &str) -> Option<DataMode> {
match word {
"hex" => Some(DataMode::Hex),
"base64" => Some(DataMode::Base64),
"ascii7" => Some(DataMode::Ascii7),
"bytes" => Some(DataMode::Bytes8),
"bits" => Some(DataMode::Bytes8), _ => None,
}
}
fn meta_word_to_language(word: &str) -> Option<(&str, &str)> {
match word {
"english" => Some(("english", "body")),
"latin" => Some(("latin", "body")),
"czech" => Some(("czech", "body")),
"nostr" => Some(("cs", "nip04")),
"pgp" => Some(("cs", "pgp")),
"primes" => Some(("math", "body")),
"image" => Some(("image", "voronoi")),
"bitcoin" | "btc" => Some(("crypto/btc", "bip173")),
"npub" => Some(("crypto/nostr", "pub")),
"nsec" => Some(("crypto/nostr", "sec")),
"lightning" | "ln" => Some(("crypto/ln", "main")),
"cashu" => Some(("crypto/cashu", "a")),
"email" => Some(("cs", "email")),
"bip39" => Some(("cs", "sig_bip39")),
_ => None,
}
}
fn meta_word_to_palette(word: &str) -> Option<&str> {
match word {
"viridis" => Some("viridis"),
"plasma" => Some("plasma"),
"inferno" => Some("inferno"),
"magma" => Some("magma"),
"cividis" => Some("cividis"),
"turbo" => Some("turbo"),
"mako" => Some("mako"),
"rocket" => Some("rocket"),
_ => None,
}
}
fn meta_word_to_dialect(word: &str) -> Option<&str> {
match word {
"body" => Some("body"),
"subject" => Some("subject"),
"prose" => Some("prose"),
"spells" => Some("spells"),
"voronoi" => Some("voronoi"),
"grid" => Some("grid"),
"mosaic" => Some("mosaic"),
"constellation" => Some("constellation"),
"patches" => Some("patches"),
"raw" => Some("raw"),
"sig" => Some("sig_nostr"),
"seal" => Some("seal_nostr"),
"sig_bip39" => Some("sig_bip39"),
"sig_latin" => Some("sig_latin"),
"nip04_bip39" => Some("nip04_bip39"),
"nip44_bip39" => Some("nip44_bip39"),
"bip39" => Some("bip39"),
"email_alt" => Some("email_alt"),
"email_mime" => Some("email_mime"),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Role {
Source,
Target,
}
fn classify_preposition(word: &str) -> Option<Role> {
match word {
"from" => Some(Role::Source),
"into" | "as" | "to" => Some(Role::Target),
_ => None,
}
}
impl Pipeline {
pub fn from_meta(meta_sentence: &str) -> Result<Self, PipelineError> {
let tokens: Vec<&str> = meta_sentence
.split_whitespace()
.map(|t| t.trim_matches(|c: char| !c.is_alphanumeric()))
.filter(|t| !t.is_empty())
.collect();
if tokens.is_empty() {
return Err(PipelineError::ParseError(
"empty meta instruction".to_string(),
));
}
let meta_payload: HashSet<&str> = [
"latin", "english", "czech", "hex", "base64", "base58", "ascii7", "bits",
"bytes", "nostr", "pgp", "prose", "body", "subject", "spells", "bip39", "sig", "seal",
"primes", "merkle", "image", "voronoi", "grid", "mosaic",
"constellation", "patches", "raw",
"bitcoin", "btc", "npub", "nsec", "lightning", "ln", "cashu",
"email", "email_alt", "email_mime",
"viridis", "plasma", "inferno", "magma", "cividis", "turbo", "mako", "rocket",
]
.iter()
.copied()
.collect();
let meta_adverbs: HashSet<&str> = [
"compactly", "efficiently", "naturally", "minimally",
"optimally", "thoroughly",
]
.iter()
.copied()
.collect();
let mut current_role: Option<Role> = None;
let mut verb = Verb::Unspecified;
let mut source: Option<Endpoint> = None;
let mut target: Option<Endpoint> = None;
let mut pending_dialect: Option<String> = None;
let mut pending_wordlist: Option<String> = None;
let mut unscoped_endpoints: Vec<Endpoint> = Vec::new();
let mut source_explicit = false;
let mut target_explicit = false;
let mut html = false;
let mut length_mode: Option<SentenceLengthMode> = None;
let mut variations: Option<usize> = None;
let mut k_max: Option<usize> = None;
let mut k_min: Option<usize> = None;
for &token in &tokens {
let lower = token.to_lowercase();
let lower = lower.as_str();
if let Some(role) = classify_preposition(lower) {
current_role = Some(role);
continue;
}
if lower == "html" {
html = true;
continue;
}
if meta_adverbs.contains(lower) {
match lower {
"compactly" => {
length_mode = Some(SentenceLengthMode::Compact);
}
"efficiently" => {
length_mode = Some(SentenceLengthMode::Compact);
k_max = Some(20);
}
"naturally" => {
length_mode = Some(SentenceLengthMode::Natural);
}
"minimally" => {
length_mode = Some(SentenceLengthMode::Compact);
k_min = Some(2);
k_max = Some(12);
}
"optimally" => {
variations = Some(50);
}
"thoroughly" => {
variations = Some(100);
}
_ => {}
}
continue;
}
if token.contains('/') {
let parts: Vec<&str> = token.split('/').collect();
let endpoint = match parts.len() {
3 => Endpoint::language_full(parts[0], parts[1], parts[2]),
4 => Endpoint::Language {
language: format!("{}/{}", parts[0], parts[1]),
wordlist: parts[2].to_string(),
dialect: parts[3].to_string(),
},
2 => Endpoint::Language {
language: parts[0].to_string(),
wordlist: parts[1].to_string(),
dialect: "body".to_string(),
},
_ => continue,
};
match current_role {
Some(Role::Source) => {
source = Some(endpoint);
source_explicit = true;
current_role = None;
}
Some(Role::Target) => {
target = Some(endpoint);
target_explicit = true;
current_role = None;
}
None => {
unscoped_endpoints.push(endpoint);
}
}
continue;
}
if let Some(v) = classify_verb(lower) {
if verb == Verb::Unspecified {
verb = v;
}
continue;
}
if !meta_payload.contains(lower) {
continue;
}
if let Some(palette) = meta_word_to_palette(lower) {
pending_wordlist = Some(palette.to_string());
continue;
}
if let Some(dialect) = meta_word_to_dialect(lower) {
pending_dialect = Some(dialect.to_string());
continue;
}
let endpoint = if let Some(mode) = meta_word_to_format(lower) {
Endpoint::Format(mode)
} else if let Some((lang, default_dialect)) = meta_word_to_language(lower) {
let dialect = pending_dialect.take()
.unwrap_or_else(|| default_dialect.to_string());
let wordlist = pending_wordlist.take()
.unwrap_or_else(|| "default".to_string());
Endpoint::language_full(lang, &wordlist, &dialect)
} else {
continue;
};
match current_role {
Some(Role::Source) => {
source = Some(endpoint);
source_explicit = true;
current_role = None;
}
Some(Role::Target) => {
target = Some(endpoint);
target_explicit = true;
current_role = None;
}
None => {
unscoped_endpoints.push(endpoint);
}
}
if let Some(dialect) = pending_dialect.take() {
apply_dialect_modifier(&mut source, &mut target, &mut unscoped_endpoints, &dialect);
}
if let Some(wl) = pending_wordlist.take() {
apply_wordlist_modifier(&mut source, &mut target, &mut unscoped_endpoints, &wl);
}
}
if let Some(dialect) = pending_dialect {
apply_dialect_modifier(&mut source, &mut target, &mut unscoped_endpoints, &dialect);
}
if let Some(wl) = pending_wordlist {
apply_wordlist_modifier(&mut source, &mut target, &mut unscoped_endpoints, &wl);
}
if source_explicit && !target_explicit && target.is_none() && !unscoped_endpoints.is_empty() {
target = Some(unscoped_endpoints.remove(0));
} else if target_explicit && !source_explicit && source.is_none() && !unscoped_endpoints.is_empty() {
source = Some(unscoped_endpoints.remove(0));
} else {
if source.is_none() && !unscoped_endpoints.is_empty() {
source = Some(unscoped_endpoints.remove(0));
}
if target.is_none() && !unscoped_endpoints.is_empty() {
target = Some(unscoped_endpoints.remove(0));
}
}
let (source, target) = match (source, target) {
(Some(s), Some(t)) => (s, t),
(Some(s), None) if source_explicit => (s, Endpoint::Auto),
(Some(s), None) => (Endpoint::Auto, s),
(None, Some(t)) => (Endpoint::Auto, t),
(None, None) => {
return Err(PipelineError::ParseError(
"no dialect identifiers found in meta instruction".to_string(),
));
}
};
let source = resolve_dialect_wordlist(source);
let target = resolve_dialect_wordlist(target);
Ok(Pipeline {
source,
target,
verb,
seed: 0,
verbose: false,
html,
length_mode,
variations,
k_max,
k_min,
})
}
pub fn from_params(source: Endpoint, target: Endpoint) -> Self {
let source = resolve_dialect_wordlist(source);
let target = resolve_dialect_wordlist(target);
Pipeline {
source,
target,
verb: Verb::Unspecified,
seed: 0,
verbose: false,
html: false,
length_mode: None,
variations: None,
k_max: None,
k_min: None,
}
}
pub fn with_verb(mut self, verb: Verb) -> Self {
self.verb = verb;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn with_html(mut self, html: bool) -> Self {
self.html = html;
self
}
pub fn with_length_mode(mut self, mode: SentenceLengthMode) -> Self {
self.length_mode = Some(mode);
self
}
pub fn with_variations(mut self, count: usize) -> Self {
self.variations = Some(count);
self
}
pub fn with_k_max(mut self, k_max: usize) -> Self {
self.k_max = Some(k_max);
self
}
pub fn with_k_min(mut self, k_min: usize) -> Self {
self.k_min = Some(k_min);
self
}
fn cover_override(&self) -> Option<&str> {
if self.html { Some("html") } else { None }
}
pub fn execute(&self, input: &str) -> Result<String, PipelineError> {
let source = self.resolve_source(input)?;
let target = &self.target;
match (&source, target) {
(Endpoint::Format(_) | Endpoint::Auto, Endpoint::Language { .. }) => {
self.do_encode(input, target)
}
(Endpoint::Language { .. }, Endpoint::Format(_) | Endpoint::Auto) => {
self.do_decode(input, &source)
}
(Endpoint::Language { .. }, Endpoint::Language { .. }) => {
self.do_transcode(input, &source, target)
}
(Endpoint::Format(_), Endpoint::Format(_)) => {
Err(PipelineError::InvalidPipeline(
"format-to-format: use a language as intermediary".to_string(),
))
}
_ => Err(PipelineError::InvalidPipeline(format!(
"unsupported pipeline: {} -> {}",
source, target
))),
}
}
fn detect_source_language(&self, input: &str) -> Option<Endpoint> {
let words: Vec<String> = input
.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_string())
.filter(|w| !w.is_empty())
.collect();
if words.is_empty() {
return None;
}
let matches = detect_dialect(&words);
let best = matches.first()?;
if best.hit_rate > 0.3 {
if self.verbose {
eprintln!(
"Auto-detected source: {}/{} ({:.0}% hit rate)",
best.language, best.wordlist, best.hit_rate * 100.0
);
}
let dialect = best.dialects.first()
.cloned()
.unwrap_or_else(|| "body".to_string());
Some(Endpoint::Language {
language: best.language.clone(),
wordlist: best.wordlist.clone(),
dialect,
})
} else {
None
}
}
fn resolve_source(&self, input: &str) -> Result<Endpoint, PipelineError> {
match &self.source {
Endpoint::Auto => {
if matches!(&self.target, Endpoint::Language { .. }) {
if !matches!(&self.target, Endpoint::Language { language, .. } if language.starts_with("crypto/")) {
if let Some((language, dialect, _)) = detect_crypto_format(input) {
if self.verbose {
eprintln!("Source auto-detected as {}/{}", language, dialect);
}
return Ok(Endpoint::Language {
language: language.to_string(),
wordlist: crypto_payload_type(language, dialect).to_string(),
dialect: dialect.to_string(),
});
}
}
if self.verb == Verb::Transcode {
if let Some(src) = self.detect_source_language(input) {
return Ok(src);
}
return Err(PipelineError::InvalidPipeline(
"transcode: could not detect the source encoding of the input; \
name it explicitly, e.g. `transcode from english into <target>`"
.to_string(),
));
}
let (mode, _) = codec::detect_mode(input);
if self.verbose {
eprintln!("Source auto-detected as format: {} (target is Language)", mode);
}
return Ok(Endpoint::Format(mode));
}
if let Some(src) = self.detect_source_language(input) {
return Ok(src);
}
let (mode, _) = codec::detect_mode(input);
Ok(Endpoint::Format(mode))
}
other => Ok(other.clone()),
}
}
fn do_encode(&self, input: &str, target: &Endpoint) -> Result<String, PipelineError> {
let (language, wordlist, dialect) = match target {
Endpoint::Language { language, wordlist, dialect } => {
(language.as_str(), wordlist.as_str(), dialect.as_str())
}
_ => return Err(PipelineError::InvalidPipeline(
"encode target must be a Language".to_string(),
)),
};
if language.starts_with("crypto/") {
return encode_crypto(input, language, dialect, self.verbose);
}
if dialect == "raw" {
let wordlist_name = if wordlist == "default" {
let dw = crate::generator::default_wordlist(language);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
};
let payload_words = load_payload_words_for_wordlist(language, wordlist_name)
.map_err(|e| PipelineError::EncodeError(e))?;
let payload_tree = WordlistTree::new(payload_words);
let (_, data) = codec::detect_mode(input);
let words = codec::encode_base_n(&data, &payload_tree, "base_n")
.map_err(|e| PipelineError::EncodeError(format!("{}", e)))?;
return Ok(words.join(" "));
}
let (text, _, _, _) = encode_into_language(
input, language, wordlist, dialect, None, self.seed, self.verbose,
self.cover_override(),
self.length_mode,
self.k_min,
self.k_max,
)?;
Ok(text)
}
fn do_encode_rich(&self, input: &str, target: &Endpoint) -> Result<PipelineResult, PipelineError> {
let (language, wordlist, dialect) = match target {
Endpoint::Language { language, wordlist, dialect } => {
(language.as_str(), wordlist.as_str(), dialect.as_str())
}
_ => return Err(PipelineError::InvalidPipeline(
"encode target must be a Language".to_string(),
)),
};
if dialect == "raw" {
let wordlist_name = if wordlist == "default" {
let dw = crate::generator::default_wordlist(language);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
};
let payload_words = load_payload_words_for_wordlist(language, wordlist_name)
.map_err(|e| PipelineError::EncodeError(e))?;
let payload_tree = WordlistTree::new(payload_words);
let (_, data) = codec::detect_mode(input);
let words = codec::encode_base_n(&data, &payload_tree, "base_n")
.map_err(|e| PipelineError::EncodeError(format!("{}", e)))?;
let output = words.join(" ");
let payload_count = words.len();
return Ok(PipelineResult {
output,
payload_words: words,
data_mode: None,
stats: Some(PipelineStats {
payload_count,
cover_count: 0,
total_words: payload_count,
ratio: 1.0,
}),
resolved_source: None,
});
}
if language.starts_with("crypto/") {
let text = encode_crypto(input, language, dialect, self.verbose)?;
let prefix = crypto_dialect_prefix(language, dialect);
let data_chars: Vec<String> = text[prefix.len()..].chars()
.map(|c: char| c.to_string())
.collect();
let payload_count = data_chars.len();
return Ok(PipelineResult {
output: text,
payload_words: data_chars,
data_mode: None,
stats: Some(PipelineStats {
payload_count,
cover_count: if prefix.is_empty() { 0 } else { 1 },
total_words: payload_count + if prefix.is_empty() { 0 } else { 1 },
ratio: 1.0,
}),
resolved_source: None,
});
}
let (text, _payload_set, encoded_words, data_mode) = encode_into_language(
input, language, wordlist, dialect, None, self.seed, self.verbose,
self.cover_override(),
self.length_mode,
self.k_min,
self.k_max,
)?;
let payload_count = encoded_words.len();
let total_words = text.split_whitespace().count();
let cover_count = total_words.saturating_sub(payload_count);
let ratio = if total_words > 0 {
payload_count as f64 / total_words as f64
} else {
0.0
};
Ok(PipelineResult {
output: text,
payload_words: encoded_words,
data_mode: Some(data_mode),
stats: Some(PipelineStats {
payload_count,
cover_count,
total_words,
ratio,
}),
resolved_source: None,
})
}
fn decode_raw_words(
&self,
input: &str,
language: &str,
wordlist: &str,
) -> Result<(String, Vec<String>), PipelineError> {
let wordlist_name = if wordlist == "default" {
let dw = crate::generator::default_wordlist(language);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
};
let payload_words = load_payload_words_for_wordlist(language, wordlist_name)
.map_err(|e| PipelineError::DecodeError(e))?;
let payload_tree = WordlistTree::new(payload_words);
let words: Vec<String> = input
.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
.filter(|w| !w.is_empty() && payload_tree.contains(w))
.collect();
if words.is_empty() {
return Err(PipelineError::DecodeError(
"no payload words found in input".to_string(),
));
}
if self.verbose {
eprintln!("Raw decode: {} payload words via base-N", words.len());
}
let bytes = codec::decode_base_n(&words, &payload_tree, "base_n")
.map_err(|e| PipelineError::DecodeError(format!("{}", e)))?;
let output = match self.target_format() {
Some(fmt) => render_decoded_bytes(&bytes, Some(fmt)),
None => codec::hex_encode(&bytes),
};
Ok((output, words))
}
fn target_format(&self) -> Option<DataMode> {
match &self.target {
Endpoint::Format(mode) => Some(*mode),
_ => None,
}
}
fn do_decode(&self, input: &str, source: &Endpoint) -> Result<String, PipelineError> {
let (language, wordlist, dialect) = match source {
Endpoint::Language { language, wordlist, dialect } => {
(language.as_str(), wordlist.as_str(), dialect.as_str())
}
_ => return Err(PipelineError::InvalidPipeline(
"decode source must be a Language".to_string(),
)),
};
if language.starts_with("crypto/") {
return decode_crypto(input, language, dialect, self.verbose);
}
if dialect == "raw" {
let (output, _) = self.decode_raw_words(input, language, wordlist)?;
return Ok(output);
}
let payload_lang = DialectConfig::from_language_dialect(language, dialect)
.map(|c| c.payload_language().to_string())
.unwrap_or_else(|_| language.to_string());
decode_from_language_with_payload_lang(input, language, &payload_lang, wordlist, self.verbose, dialect, self.target_format())
}
fn do_transcode(
&self,
input: &str,
source: &Endpoint,
target: &Endpoint,
) -> Result<String, PipelineError> {
let raw = self.do_decode(input, source)?;
if self.verbose {
eprintln!("Transcode intermediate: {} bytes", raw.len());
}
self.do_encode(&raw, target)
}
pub fn execute_rich(&self, input: &str) -> Result<PipelineResult, PipelineError> {
let source = self.resolve_source(input)?;
let target = &self.target;
let mut result = match (&source, target) {
(Endpoint::Format(_) | Endpoint::Auto, Endpoint::Language { .. }) => {
self.do_encode_rich(input, target)
}
(Endpoint::Language { .. }, Endpoint::Format(_) | Endpoint::Auto) => {
self.do_decode_rich(input, &source)
}
(Endpoint::Language { .. }, Endpoint::Language { .. }) => {
self.do_transcode_rich(input, &source, target)
}
(Endpoint::Format(_), Endpoint::Format(_)) => {
Err(PipelineError::InvalidPipeline(
"format-to-format: use a language as intermediary".to_string(),
))
}
_ => Err(PipelineError::InvalidPipeline(format!(
"unsupported pipeline: {} -> {}",
source, target
))),
}?;
result.resolved_source = Some(source);
Ok(result)
}
fn do_decode_rich(&self, input: &str, source: &Endpoint) -> Result<PipelineResult, PipelineError> {
let (language, wordlist, dialect) = match source {
Endpoint::Language { language, wordlist, dialect } => {
(language.as_str(), wordlist.as_str(), dialect.as_str())
}
_ => return Err(PipelineError::InvalidPipeline(
"decode source must be a Language".to_string(),
)),
};
if language.starts_with("crypto/") {
let decoded = decode_crypto(input, language, dialect, self.verbose)?;
let data_part = if let Some((_, _, data)) = detect_crypto_format(input) {
data
} else {
input
};
let extracted: Vec<String> = data_part.chars()
.map(|c| c.to_lowercase().to_string())
.collect();
return Ok(PipelineResult {
output: decoded,
payload_words: extracted,
data_mode: None,
stats: None,
resolved_source: None,
});
}
if dialect == "raw" {
let (output, words) = self.decode_raw_words(input, language, wordlist)?;
let payload_count = words.len();
return Ok(PipelineResult {
output,
payload_words: words,
data_mode: None,
stats: Some(PipelineStats {
payload_count,
cover_count: 0,
total_words: payload_count,
ratio: 1.0,
}),
resolved_source: None,
});
}
let payload_lang = DialectConfig::from_language_dialect(language, dialect)
.map(|c| c.payload_language().to_string())
.unwrap_or_else(|_| language.to_string());
let (decoded, extracted) = decode_from_language_rich_with_payload_lang(
input, language, &payload_lang, wordlist, self.verbose, dialect, self.target_format())?;
Ok(PipelineResult {
output: decoded,
payload_words: extracted,
data_mode: None,
stats: None,
resolved_source: None,
})
}
fn do_transcode_rich(
&self,
input: &str,
source: &Endpoint,
target: &Endpoint,
) -> Result<PipelineResult, PipelineError> {
let raw = self.do_decode(input, source)?;
if self.verbose {
eprintln!("Transcode intermediate: {} bytes", raw.len());
}
self.do_encode_rich(&raw, target)
}
}
fn apply_dialect_modifier(
source: &mut Option<Endpoint>,
target: &mut Option<Endpoint>,
unscoped: &mut Vec<Endpoint>,
dialect: &str,
) {
let ep = if target.is_some() {
target.as_mut()
} else if source.is_some() {
source.as_mut()
} else {
unscoped.last_mut()
};
if let Some(Endpoint::Language { dialect: ref mut d, .. }) = ep {
*d = dialect.to_string();
}
}
fn apply_wordlist_modifier(
source: &mut Option<Endpoint>,
target: &mut Option<Endpoint>,
unscoped: &mut Vec<Endpoint>,
wordlist: &str,
) {
let ep = if target.is_some() {
target.as_mut()
} else if source.is_some() {
source.as_mut()
} else {
unscoped.last_mut()
};
if let Some(Endpoint::Language { wordlist: ref mut w, .. }) = ep {
*w = wordlist.to_string();
}
}
fn resolve_dialect_wordlist(ep: Endpoint) -> Endpoint {
match ep {
Endpoint::Language { ref language, ref wordlist, ref dialect } if wordlist == "default" => {
if let Ok(config) = DialectConfig::from_language_dialect(language, dialect) {
let declared = config.payload_wordlist();
if declared != "default" {
return Endpoint::Language {
language: language.clone(),
wordlist: declared.to_string(),
dialect: dialect.clone(),
};
}
}
ep
}
other => other,
}
}
fn resolve_wordlist_name<'a>(language: &str, wordlist: &'a str) -> &'a str {
if wordlist == "default" {
let dw = default_wordlist(language);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
}
}
fn prepare_payload(
input: &str,
language: &str,
wordlist: &str,
dialect: &str,
forced_data_mode: Option<DataMode>,
) -> Result<(Vec<PayloadTok>, Vec<String>, DataMode, HashSet<String>), PipelineError> {
let payload_lang = DialectConfig::from_language_dialect(language, dialect)
.map(|c| c.payload_language().to_string())
.unwrap_or_else(|_| language.to_string());
let payload_words = load_payload_words_for_wordlist(&payload_lang, wordlist)
.map_err(|e| PipelineError::EncodeError(e))?;
let payload_tree = WordlistTree::new(payload_words.clone());
let grammar = Grammar::from_language_dialect(language, dialect)
.map_err(|e| PipelineError::EncodeError(format!("Failed to load grammar: {}", e)))?;
let (encoded_words, data_mode) = if let Some(mode) = forced_data_mode {
let data = match mode {
DataMode::Hex => codec::hex_decode(input)
.ok_or_else(|| PipelineError::EncodeError("invalid hex input".into()))?,
DataMode::Base64 => codec::base64_decode(input)
.ok_or_else(|| PipelineError::EncodeError("invalid base64 input".into()))?,
DataMode::Ascii7 | DataMode::Bytes8 => input.as_bytes().to_vec(),
};
let words = codec::encode_base_n(&data, &payload_tree, grammar.codec())
.map_err(|e| PipelineError::EncodeError(format!("{}", e)))?;
(words, mode)
} else {
codec::encode_str_base_n(input, &payload_tree, grammar.codec())
.map_err(|e| PipelineError::EncodeError(format!("{}", e)))?
};
let is_cs_grammar = !grammar.dot_is_punctuation();
let pos_mapping = if is_cs_grammar {
HashMap::new() } else {
build_pos_mapping_for_wordlist(&payload_lang, wordlist)
.map_err(|e| PipelineError::EncodeError(e))?
};
let payload_toks: Vec<PayloadTok> = encoded_words
.iter()
.map(|word| {
let allowed = if is_cs_grammar {
vec![Pos::N]
} else {
pos_mapping
.get(&word.to_lowercase())
.cloned()
.unwrap_or_default()
};
PayloadTok::new(word.clone(), &allowed)
})
.collect();
let wordlist_set: HashSet<String> = payload_words.iter().map(|w| w.to_lowercase()).collect();
Ok((payload_toks, encoded_words, data_mode, wordlist_set))
}
struct ZoneGenerator {
lex: Lexicon,
mode: GenerationMode,
k_min: usize,
k_max: usize,
length_mode: SentenceLengthMode,
}
fn build_zone_generator(
zone_len: usize,
language: &str,
dialect: &str,
payload_word_set: &HashSet<String>,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
) -> Result<ZoneGenerator, PipelineError> {
let dialect_config = crate::grammar::DialectConfig::from_language_dialect(language, dialect)
.map_err(|e| PipelineError::EncodeError(format!("Dialect config error: {}", e)))?;
let grammar = Grammar::from_language_dialect(language, dialect)
.map_err(|e| PipelineError::EncodeError(format!("Grammar error: {}", e)))?;
let cover_wl = cover_override.unwrap_or_else(|| dialect_config.cover_wordlist());
let skip_filter = !grammar.dot_is_punctuation()
&& dialect_config.payload_language() != language;
if verbose {
eprintln!("Cover filter: dot_is_punct={}, payload_lang={}, grammar_lang={}, skip_filter={}",
grammar.dot_is_punctuation(), dialect_config.payload_language(), language, skip_filter);
}
if verbose && skip_filter {
eprintln!("Cover filter: using empty filter set (cross-language payload)");
}
let cover_filter_set = if skip_filter {
HashSet::new()
} else {
payload_word_set.clone()
};
let (cover_by_pos, refined_cover) =
load_cover_words_by_pos_for_wordlist(&cover_filter_set, language, cover_wl);
let mut lex = Lexicon::new(cover_filter_set.clone(), cover_filter_set.clone());
for (pos, words) in cover_by_pos {
lex = lex.with_words(pos, &words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
}
lex = lex.with_refined_cover(refined_cover);
if let Some(model) = crate::generator::data::load_semantics(language) {
lex = lex.with_semantics(std::sync::Arc::new(model));
}
let min_k = grammar.min_sentence_length().unwrap_or(5);
let is_cs_grammar = !grammar.dot_is_punctuation();
let (k_min, k_max) = if is_cs_grammar {
let k = min_k + zone_len.saturating_sub(1);
(k, k)
} else {
let default_k_min = 5;
let default_k_max = 12;
(k_min_override.unwrap_or(default_k_min), k_max_override.unwrap_or(default_k_max))
};
let mode = if dialect.starts_with("subject") {
GenerationMode::Subject
} else {
GenerationMode::Body
};
let length_mode = length_mode.unwrap_or_else(|| match mode {
GenerationMode::Subject => SentenceLengthMode::Compact,
GenerationMode::Body => SentenceLengthMode::Natural,
GenerationMode::PayloadOnly => SentenceLengthMode::Compact,
});
Ok(ZoneGenerator { lex, mode, k_min, k_max, length_mode })
}
fn generate_prose_for_zone(
payload_toks: &[PayloadTok],
word_range: std::ops::Range<usize>,
language: &str,
_wordlist: &str,
dialect: &str,
payload_word_set: &HashSet<String>,
seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
) -> Result<(String, HashSet<String>), PipelineError> {
let zone_toks = &payload_toks[word_range];
let gen = build_zone_generator(
zone_toks.len(), language, dialect, payload_word_set, verbose,
cover_override, length_mode, k_min_override, k_max_override,
)?;
let mut rng = StdRng::seed_from_u64(seed);
let (text, payload_set) = generate_text_with_original_payload(
&mut rng,
&gen.lex,
zone_toks,
None,
verbose,
gen.mode,
language,
Some(dialect),
gen.k_min,
gen.k_max,
gen.length_mode,
" ",
);
Ok((text, payload_set))
}
#[allow(clippy::too_many_arguments)]
fn generate_prose_for_zone_best_of(
payload_toks: &[PayloadTok],
word_range: std::ops::Range<usize>,
language: &str,
_wordlist: &str,
dialect: &str,
payload_word_set: &HashSet<String>,
base_seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
n_candidates: usize,
) -> Result<(String, HashSet<String>), PipelineError> {
let zone_toks = &payload_toks[word_range];
let gen = build_zone_generator(
zone_toks.len(), language, dialect, payload_word_set, verbose,
cover_override, length_mode, k_min_override, k_max_override,
)?;
let (text, payload_set) = generate_text_best_of(
base_seed,
n_candidates,
&gen.lex,
zone_toks,
None,
verbose,
gen.mode,
language,
Some(dialect),
gen.k_min,
gen.k_max,
gen.length_mode,
" ",
);
Ok((text, payload_set))
}
fn is_prose_email_dialect(dialect: &str) -> bool {
matches!(dialect, "email" | "email_alt" | "email_mime")
}
fn compute_subject_word_count(total: usize) -> usize {
if total <= 6 { total } else { 6 }
}
fn assemble_email(dialect: &str, subject: &str, body: &str) -> String {
match dialect {
"email_alt" => format!(
"From: <sender@glossia.local>\r\n\
To: <recipient@glossia.local>\r\n\
Date: Thu, 01 Jan 2026 00:00:00 +0000\r\n\
Subject: {subject}\r\n\
MIME-Version: 1.0\r\n\
Content-Type: multipart/alternative; boundary=\"glossia-alt\"\r\n\
\r\n\
--glossia-alt\r\n\
Content-Type: text/plain; charset=\"UTF-8\"\r\n\
\r\n\
{body}\r\n\
\r\n\
--glossia-alt\r\n\
Content-Type: text/html; charset=\"UTF-8\"\r\n\
\r\n\
<html><body></body></html>\r\n\
\r\n\
--glossia-alt--"
),
"email_mime" => format!(
"From: <sender@glossia.local>\r\n\
To: <recipient@glossia.local>\r\n\
Date: Thu, 01 Jan 2026 00:00:00 +0000\r\n\
Subject: {subject}\r\n\
MIME-Version: 1.0\r\n\
Content-Type: multipart/mixed; boundary=\"glossia\"\r\n\
\r\n\
--glossia\r\n\
Content-Type: text/plain; charset=\"UTF-8\"\r\n\
\r\n\
{body}\r\n\
\r\n\
--glossia\r\n\
Content-Type: application/octet-stream\r\n\
Content-Disposition: attachment; filename=\"payload.bin\"\r\n\
\r\n\
\r\n\
--glossia--"
),
_ => format!(
"From: <sender@glossia.local>\r\n\
To: <recipient@glossia.local>\r\n\
Date: Thu, 01 Jan 2026 00:00:00 +0000\r\n\
Subject: {subject}\r\n\
Content-Type: text/plain; charset=\"UTF-8\"\r\n\
\r\n\
{body}"
),
}
}
fn compose_email(
input: &str,
language: &str,
wordlist: &str,
dialect: &str,
forced_data_mode: Option<DataMode>,
seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
) -> Result<(String, HashSet<String>, Vec<String>, DataMode), PipelineError> {
let (payload_toks, encoded_words, data_mode, wordlist_set) =
prepare_payload(input, language, wordlist, "body", forced_data_mode)?;
let total = payload_toks.len();
let subject_count = compute_subject_word_count(total);
let (subject_text, subject_set) = generate_prose_for_zone(
&payload_toks,
0..subject_count,
language,
wordlist,
"subject",
&wordlist_set,
seed,
verbose,
cover_override,
Some(SentenceLengthMode::Compact),
k_min_override,
k_max_override,
)?;
let (body_text, body_set) = if subject_count < total {
generate_prose_for_zone(
&payload_toks,
subject_count..total,
language,
wordlist,
"body",
&wordlist_set,
seed.wrapping_add(1),
verbose,
cover_override,
length_mode,
k_min_override,
k_max_override,
)?
} else {
(String::new(), HashSet::new())
};
let email = assemble_email(dialect, &subject_text, &body_text);
let mut payload_set = subject_set;
payload_set.extend(body_set);
Ok((email, payload_set, encoded_words, data_mode))
}
pub fn encode_into_language(
input: &str,
language: &str,
wordlist: &str,
dialect: &str,
forced_data_mode: Option<DataMode>,
seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
) -> Result<(String, HashSet<String>, Vec<String>, DataMode), PipelineError> {
let wordlist = resolve_wordlist_name(language, wordlist);
if is_prose_email_dialect(dialect) && language != "cs" {
return compose_email(
input, language, wordlist, dialect, forced_data_mode,
seed, verbose, cover_override, length_mode,
k_min_override, k_max_override,
);
}
let (input, dialect) = if dialect == "subject" {
let trimmed = input.trim_start();
if trimmed.to_lowercase().starts_with("re:") {
(&trimmed[3..].trim_start() as &str, "subject_re")
} else if trimmed.to_lowercase().starts_with("fwd:") {
(&trimmed[4..].trim_start() as &str, "subject_fwd")
} else {
(input, dialect)
}
} else {
(input, dialect)
};
let (payload_toks, encoded_words, data_mode, wordlist_set) =
prepare_payload(input, language, wordlist, dialect, forced_data_mode)?;
let (text, payload_set) = generate_prose_for_zone(
&payload_toks,
0..payload_toks.len(),
language,
wordlist,
dialect,
&wordlist_set,
seed,
verbose,
cover_override,
length_mode,
k_min_override,
k_max_override,
)?;
Ok((text, payload_set, encoded_words, data_mode))
}
#[allow(clippy::too_many_arguments)]
pub fn encode_into_language_best_of(
input: &str,
language: &str,
wordlist: &str,
dialect: &str,
forced_data_mode: Option<DataMode>,
base_seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
n_candidates: usize,
) -> Result<(String, HashSet<String>, Vec<String>, DataMode), PipelineError> {
let n = n_candidates.max(1);
if n == 1 || crate::generator::data::load_semantics(language).is_none() {
return encode_into_language(
input, language, wordlist, dialect, forced_data_mode, base_seed, verbose,
cover_override, length_mode, k_min_override, k_max_override,
);
}
let wordlist = resolve_wordlist_name(language, wordlist);
if is_prose_email_dialect(dialect) && language != "cs" {
return encode_into_language_best_of_via_pipeline(
input, language, wordlist, dialect, forced_data_mode, base_seed, verbose,
cover_override, length_mode, k_min_override, k_max_override, n,
);
}
let (input, dialect) = if dialect == "subject" {
let trimmed = input.trim_start();
if trimmed.to_lowercase().starts_with("re:") {
(&trimmed[3..].trim_start() as &str, "subject_re")
} else if trimmed.to_lowercase().starts_with("fwd:") {
(&trimmed[4..].trim_start() as &str, "subject_fwd")
} else {
(input, dialect)
}
} else {
(input, dialect)
};
let (payload_toks, encoded_words, data_mode, wordlist_set) =
prepare_payload(input, language, wordlist, dialect, forced_data_mode)?;
let (text, payload_set) = generate_prose_for_zone_best_of(
&payload_toks,
0..payload_toks.len(),
language,
wordlist,
dialect,
&wordlist_set,
base_seed,
verbose,
cover_override,
length_mode,
k_min_override,
k_max_override,
n,
)?;
Ok((text, payload_set, encoded_words, data_mode))
}
#[allow(clippy::too_many_arguments)]
fn encode_into_language_best_of_via_pipeline(
input: &str,
language: &str,
wordlist: &str,
dialect: &str,
forced_data_mode: Option<DataMode>,
base_seed: u64,
verbose: bool,
cover_override: Option<&str>,
length_mode: Option<SentenceLengthMode>,
k_min_override: Option<usize>,
k_max_override: Option<usize>,
n: usize,
) -> Result<(String, HashSet<String>, Vec<String>, DataMode), PipelineError> {
const DENSITY_TOL: f64 = 0.02;
let model = crate::generator::data::load_semantics(language)
.expect("caller ensures a semantic model exists for best-of selection");
let mut candidates: Vec<(f64, f64, (String, HashSet<String>, Vec<String>, DataMode))> =
Vec::with_capacity(n);
for k in 0..n {
let seed = base_seed.wrapping_add(k as u64);
let res = encode_into_language(
input, language, wordlist, dialect, forced_data_mode, seed, verbose,
cover_override, length_mode, k_min_override, k_max_override,
)?;
let total = res.0.split_whitespace().count().max(1);
let density = res.2.len() as f64 / total as f64;
let coherence = model.coherence_score(&res.0);
candidates.push((density, coherence, res));
}
let max_density = candidates.iter().map(|c| c.0).fold(f64::MIN, f64::max);
let best = candidates
.into_iter()
.filter(|c| c.0 >= max_density - DENSITY_TOL)
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.expect("at least one candidate generated");
Ok(best.2)
}
fn strip_email_header_labels(text: &str) -> String {
let first_line = text.lines().next().unwrap_or("");
let first_lower = first_line.to_lowercase();
if !first_lower.starts_with("from:")
&& !first_lower.starts_with("to:")
&& !first_lower.starts_with("date:")
&& !first_lower.starts_with("received:")
{
return text.to_string();
}
let mut result = String::new();
let mut in_headers = true;
for line in text.lines() {
if in_headers {
if line.trim().is_empty() {
in_headers = false;
result.push('\n');
} else if line.starts_with(' ') || line.starts_with('\t') {
result.push_str(line.trim_start());
result.push('\n');
} else if let Some(colon_pos) = line.find(':') {
let value = line[colon_pos + 1..].trim_start();
if !value.is_empty() {
result.push_str(value);
result.push('\n');
}
}
} else {
result.push_str(line);
result.push('\n');
}
}
result
}
fn render_decoded_bytes(bytes: &[u8], format: Option<DataMode>) -> String {
match format {
Some(DataMode::Hex) => codec::hex_encode(bytes),
Some(DataMode::Base64) => codec::base64_encode(bytes),
Some(DataMode::Ascii7) | Some(DataMode::Bytes8) => {
String::from_utf8_lossy(bytes).into_owned()
}
None => String::from_utf8(bytes.to_vec()).unwrap_or_else(|_| codec::hex_encode(bytes)),
}
}
pub fn decode_from_language(
text: &str,
language: &str,
wordlist: &str,
verbose: bool,
) -> Result<String, PipelineError> {
decode_from_language_with_payload_lang(text, language, language, wordlist, verbose, "body", None)
}
fn decode_extracted_words(
extracted: &[String],
tree: &WordlistTree,
grammar_codec: &str,
all_payload: bool,
) -> Result<Vec<u8>, codec::DecodeError> {
if grammar_codec == "bitpack" && all_payload && !extracted.is_empty() {
codec::decode_base_n(extracted, tree, "bitpack")
.or_else(|_| codec::decode_base_n(extracted, tree, "base_n"))
} else {
codec::decode_base_n(extracted, tree, grammar_codec)
}
}
fn alnum_token_count(text: &str) -> usize {
text.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()))
.filter(|w| !w.is_empty())
.count()
}
fn decode_from_language_with_payload_lang(
text: &str,
grammar_lang: &str,
payload_lang: &str,
wordlist: &str,
verbose: bool,
grammar_dialect: &str,
format: Option<DataMode>,
) -> Result<String, PipelineError> {
let text = strip_email_header_labels(text);
let text = text.as_str();
let wordlist = if wordlist == "default" {
let dw = default_wordlist(payload_lang);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
};
let payload_tree = cached_payload_tree(payload_lang, wordlist)?;
let grammar = Grammar::from_language_dialect(grammar_lang, grammar_dialect)
.map_err(|e| PipelineError::DecodeError(format!("Grammar error: {}", e)))?;
let payload_separator = grammar.payload_separator();
let text = if !grammar.dot_is_punctuation() && !payload_separator.is_empty() {
let stripped: String = text.lines()
.filter(|line| !line.trim_start().starts_with("-----"))
.collect::<Vec<_>>()
.join("\n");
std::borrow::Cow::Owned(stripped)
} else {
std::borrow::Cow::Borrowed(text)
};
let text: &str = &text;
let extracted: Vec<String> = if payload_separator.is_empty() {
let payload_set: HashSet<String> = payload_tree.words().iter()
.map(|w| w.to_lowercase())
.collect();
text.split_whitespace()
.flat_map(|token| {
let trimmed = token.trim_matches(|c: char| {
!c.is_alphanumeric() && !payload_set.contains(&c.to_lowercase().to_string())
});
let all_in_payload = !trimmed.is_empty() && trimmed.chars()
.all(|c| payload_set.contains(&c.to_lowercase().to_string()));
if all_in_payload {
trimmed.chars()
.map(|c| c.to_lowercase().to_string())
.collect::<Vec<_>>()
} else {
vec![]
}
})
.collect()
} else {
text.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
.filter(|w| payload_tree.contains(w))
.collect()
};
if extracted.is_empty() {
return Err(PipelineError::DecodeError(
"no payload words found in input".to_string(),
));
}
if verbose {
eprintln!("Extracted {} payload words", extracted.len());
}
let all_payload = !payload_separator.is_empty() && alnum_token_count(text) == extracted.len();
let bytes = decode_extracted_words(&extracted, &*payload_tree, grammar.codec(), all_payload)
.map_err(|e| PipelineError::DecodeError(format!("{}", e)))?;
Ok(render_decoded_bytes(&bytes, format))
}
pub fn decode_from_language_rich(
text: &str,
language: &str,
wordlist: &str,
verbose: bool,
) -> Result<(String, Vec<String>), PipelineError> {
decode_from_language_rich_with_payload_lang(text, language, language, wordlist, verbose, "body", None)
}
fn decode_from_language_rich_with_payload_lang(
text: &str,
grammar_lang: &str,
payload_lang: &str,
wordlist: &str,
verbose: bool,
grammar_dialect: &str,
format: Option<DataMode>,
) -> Result<(String, Vec<String>), PipelineError> {
let text = strip_email_header_labels(text);
let text = text.as_str();
let wordlist = if wordlist == "default" {
let dw = default_wordlist(payload_lang);
if dw == "default" { wordlist } else { dw }
} else {
wordlist
};
let payload_tree = cached_payload_tree(payload_lang, wordlist)?;
let grammar = Grammar::from_language_dialect(grammar_lang, grammar_dialect)
.map_err(|e| PipelineError::DecodeError(format!("Grammar error: {}", e)))?;
let payload_separator = grammar.payload_separator();
let text = if !grammar.dot_is_punctuation() && !payload_separator.is_empty() {
let stripped: String = text.lines()
.filter(|line| !line.trim_start().starts_with("-----"))
.collect::<Vec<_>>()
.join("\n");
std::borrow::Cow::Owned(stripped)
} else {
std::borrow::Cow::Borrowed(text)
};
let text: &str = &text;
let extracted: Vec<String> = if payload_separator.is_empty() {
let payload_set: HashSet<String> = payload_tree.words().iter()
.map(|w| w.to_lowercase())
.collect();
text.split_whitespace()
.flat_map(|token| {
let trimmed = token.trim_matches(|c: char| {
!c.is_alphanumeric() && !payload_set.contains(&c.to_lowercase().to_string())
});
let all_in_payload = !trimmed.is_empty() && trimmed.chars()
.all(|c| payload_set.contains(&c.to_lowercase().to_string()));
if all_in_payload {
trimmed.chars()
.map(|c| c.to_lowercase().to_string())
.collect::<Vec<_>>()
} else {
vec![]
}
})
.collect()
} else {
text.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
.filter(|w| payload_tree.contains(w))
.collect()
};
if extracted.is_empty() {
return Err(PipelineError::DecodeError(
"no payload words found in input".to_string(),
));
}
if verbose {
eprintln!("Extracted {} payload words", extracted.len());
}
let all_payload = !payload_separator.is_empty() && alnum_token_count(text) == extracted.len();
let bytes = decode_extracted_words(&extracted, &*payload_tree, grammar.codec(), all_payload)
.map_err(|e| PipelineError::DecodeError(format!("{}", e)))?;
let decoded = render_decoded_bytes(&bytes, format);
Ok((decoded, extracted))
}
const BECH32_CHARSET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
fn bech32_char_to_value(c: char) -> Option<u8> {
let lower = c.to_ascii_lowercase() as u8;
BECH32_CHARSET.iter().position(|&b| b == lower).map(|i| i as u8)
}
fn bech32_value_to_char(v: u8) -> Option<char> {
BECH32_CHARSET.get(v as usize).map(|&b| b as char)
}
fn pack_5bit_values(values: &[u8]) -> Vec<u8> {
let total_bits = values.len() * 5;
let num_data_bytes = (total_bits + 7) / 8;
let mut packed = vec![0u8; 1 + num_data_bytes];
packed[0] = values.len() as u8;
let mut bit_pos = 0usize;
for &v in values {
for bit_offset in (0..5).rev() {
let byte_idx = 1 + bit_pos / 8;
let bit_idx = 7 - (bit_pos % 8);
if (v >> bit_offset) & 1 == 1 {
packed[byte_idx] |= 1 << bit_idx;
}
bit_pos += 1;
}
}
packed
}
fn unpack_5bit_values(packed: &[u8]) -> Result<Vec<u8>, PipelineError> {
if packed.is_empty() {
return Err(PipelineError::EncodeError("empty packed data".into()));
}
let num_chars = packed[0] as usize;
let data = &packed[1..];
let mut values = Vec::with_capacity(num_chars);
let mut bit_pos = 0usize;
for _ in 0..num_chars {
let mut value: u8 = 0;
for bit_offset in (0..5).rev() {
let byte_idx = bit_pos / 8;
let bit_idx = 7 - (bit_pos % 8);
if byte_idx < data.len() && (data[byte_idx] >> bit_idx) & 1 == 1 {
value |= 1 << bit_offset;
}
bit_pos += 1;
}
values.push(value);
}
Ok(values)
}
fn decode_crypto(
input: &str,
language: &str,
dialect: &str,
verbose: bool,
) -> Result<String, PipelineError> {
let data_part = if let Some((_, _, data)) = detect_crypto_format(input) {
data
} else {
input
};
if data_part.is_empty() {
return Err(PipelineError::DecodeError(
"empty data portion after prefix removal".to_string(),
));
}
let wl = crypto_payload_type(language, dialect);
if verbose {
eprintln!("Crypto decode: {} data chars from {}/{}", data_part.len(), language, dialect);
}
match wl {
"bech32" => {
let values: Vec<u8> = data_part.to_lowercase().chars()
.map(|c| bech32_char_to_value(c)
.ok_or_else(|| PipelineError::DecodeError(
format!("invalid bech32 char: {}", c))))
.collect::<Result<_, _>>()?;
let packed = pack_5bit_values(&values);
Ok(packed.iter().map(|b| format!("{:02x}", b)).collect())
}
"base58" | "base64url" => {
Ok(data_part.to_string())
}
_ => Ok(data_part.to_string()),
}
}
fn encode_crypto(
input: &str,
language: &str,
dialect: &str,
verbose: bool,
) -> Result<String, PipelineError> {
let wl = crypto_payload_type(language, dialect);
let prefix = crypto_dialect_prefix(language, dialect);
if verbose {
eprintln!("Crypto encode: input {} chars into {}/{}", input.len(), language, dialect);
}
match wl {
"bech32" => {
let packed = codec::hex_decode(input)
.ok_or_else(|| PipelineError::EncodeError(
"crypto encode: expected hex input for bech32 dialect".into()))?;
let values = unpack_5bit_values(&packed)?;
let chars: String = values.iter()
.map(|&v| bech32_value_to_char(v)
.ok_or_else(|| PipelineError::EncodeError(
format!("invalid bech32 5-bit value: {}", v))))
.collect::<Result<_, _>>()?;
Ok(format!("{}{}", prefix, chars))
}
"base58" | "base64url" => {
Ok(format!("{}{}", prefix, input))
}
_ => Ok(format!("{}{}", prefix, input)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cached_payload_tree_returns_shared_instance() {
let wl = crate::generator::data::default_wordlist("english");
let a = cached_payload_tree("english", wl).unwrap();
let b = cached_payload_tree("english", wl).unwrap();
assert!(Arc::ptr_eq(&a, &b));
}
#[test]
fn test_parse_from_into() {
let p = Pipeline::from_meta("translate from english into latin").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_parse_into_only() {
let p = Pipeline::from_meta("encode into latin").unwrap();
assert_eq!(p.source, Endpoint::Auto);
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_parse_format_source_unscoped() {
let p = Pipeline::from_meta("encode hex into english").unwrap();
assert!(matches!(p.source, Endpoint::Format(DataMode::Hex)),
"Expected hex source, got {:?}", p.source);
assert_eq!(p.target, Endpoint::language("english"));
}
#[test]
fn test_parse_from_format() {
let p = Pipeline::from_meta("encode from hex into english").unwrap();
assert!(matches!(p.source, Endpoint::Format(DataMode::Hex)));
assert_eq!(p.target, Endpoint::language("english"));
}
#[test]
fn test_parse_dialect_modifier_on_target() {
let p = Pipeline::from_meta("translate from english into latin prose").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language_with_dialect("latin", "prose"));
}
#[test]
fn test_parse_spells_resolves_hp_wordlist() {
let p = Pipeline::from_meta("encode into latin spells").unwrap();
assert_eq!(p.target, Endpoint::language_full("latin", "hp", "spells"),
"spells dialect should resolve payload_wordlist to 'hp' from grammar.yaml");
}
#[test]
fn test_parse_single_word_is_target() {
let p = Pipeline::from_meta("latin").unwrap();
assert_eq!(p.source, Endpoint::Auto);
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_parse_no_prepositions_fallback_order() {
let p = Pipeline::from_meta("english latin").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_parse_decode_from_keeps_source() {
let p = Pipeline::from_meta("decode from english").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::Auto);
}
#[test]
fn test_parse_empty_fails() {
assert!(Pipeline::from_meta("").is_err());
}
#[test]
fn test_parse_no_payload_words_fails() {
assert!(Pipeline::from_meta("translate from into").is_err());
}
#[test]
fn test_parse_format_to_language() {
let p = Pipeline::from_meta("from ascii7 into english").unwrap();
assert!(matches!(p.source, Endpoint::Format(DataMode::Ascii7)));
assert_eq!(p.target, Endpoint::language("english"));
}
#[test]
fn test_from_params_roundtrip_types() {
let p = Pipeline::from_params(
Endpoint::language("english"),
Endpoint::language("latin"),
);
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_encode_decode_english_round_trip() {
let input = "deadbeef";
let pipeline_encode = Pipeline::from_params(
Endpoint::Format(DataMode::Hex),
Endpoint::language_full("english", "bip39", "body"),
).with_seed(42);
let encoded = pipeline_encode.execute(input).unwrap();
assert!(!encoded.is_empty(), "Encoded text should not be empty");
let pipeline_decode = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::Auto,
);
let decoded = pipeline_decode.execute(&encoded).unwrap();
assert_eq!(decoded, input, "Round-trip should recover original hex string");
}
#[test]
fn test_transcode_english_to_latin() {
let input = "cafe";
let encode_pipeline = Pipeline::from_params(
Endpoint::Format(DataMode::Hex),
Endpoint::language_full("english", "bip39", "body"),
).with_seed(42);
let english_prose = encode_pipeline.execute(input).unwrap();
let transcode_pipeline = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::language("latin"),
).with_seed(42);
let latin_prose = transcode_pipeline.execute(&english_prose).unwrap();
assert!(!latin_prose.is_empty(), "Latin prose should not be empty");
let decode_pipeline = Pipeline::from_params(
Endpoint::language("latin"),
Endpoint::Auto,
);
let decoded = decode_pipeline.execute(&latin_prose).unwrap();
assert_eq!(decoded, input, "Transcode round-trip should recover original");
}
#[test]
fn test_cover_hidden_payload_round_trip() {
let enc = Pipeline::from_meta("encode into english naturally").unwrap();
let dec = Pipeline::from_meta("transcode from english/bip39/body into ascii7").unwrap();
let dec_hex = Pipeline::from_meta("transcode from english/bip39/body into hex").unwrap();
let tree = cached_payload_tree("english", "bip39").unwrap();
let strip_cover = |prose: &str| -> String {
prose.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()))
.filter(|w| !w.is_empty() && tree.contains(&w.to_lowercase()))
.collect::<Vec<_>>()
.join(" ")
};
let key = "sk-1SYMaH9HEJ5CHFMDQd5GoBtjRQzwOPQK";
let prose = enc.execute(key).unwrap();
assert_eq!(dec.execute(&prose).unwrap(), key, "full prose must round-trip");
assert_eq!(dec.execute(&strip_cover(&prose)).unwrap(), key, "cover-hidden must round-trip");
let hex = "00abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456700";
let enc_hex = Pipeline::from_meta("transcode from hex into english/bip39/body").unwrap();
let hprose = enc_hex.execute(hex).unwrap();
assert_eq!(dec_hex.execute(&hprose).unwrap(), hex, "leading-zero hex must round-trip");
assert_eq!(dec_hex.execute(&strip_cover(&hprose)).unwrap(), hex, "cover-hidden leading-zero hex");
}
#[test]
fn test_bip39_dialect_arbitrary_key_round_trip() {
let key = "sk-1SYMaH9HEJ5CHFMDQd5GoBtjRQzwOPQK";
let enc = Pipeline::from_meta("encode into english bip39").unwrap();
let prose = enc.execute(key).unwrap();
let dec = Pipeline::from_meta("transcode from english/bip39/bip39 into ascii7").unwrap();
assert_eq!(dec.execute(&prose).unwrap(), key, "full prose must round-trip");
let tree = cached_payload_tree("english", "bip39").unwrap();
let bare: String = prose
.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()))
.filter(|w| !w.is_empty() && tree.contains(&w.to_lowercase()))
.collect::<Vec<_>>()
.join(" ");
assert_eq!(dec.execute(&bare).unwrap(), key, "cover-hidden payload must round-trip");
}
#[test]
fn test_encode_into_language_treats_payload_words_as_text() {
let enc = Pipeline::from_meta("encode into english naturally").unwrap();
let dec = Pipeline::from_meta("decode from english").unwrap();
let cases = [
"absorb absurd access",
"zoo zone zero",
"abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon about",
];
for text in cases {
let rich = enc.execute_rich(text).unwrap();
assert!(
matches!(rich.resolved_source, Some(Endpoint::Format(_))),
"input must resolve to a data Format, not a seed/prose source: {:?}",
rich.resolved_source
);
let prose = enc.execute(text).unwrap();
let back = dec.execute(&prose).unwrap();
assert_eq!(back, text, "payload-word input must round-trip as text, got {:?}", back);
}
}
#[test]
fn test_explicit_raw_source_still_transcodes_seed() {
let mnemonic = "abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon about";
let to_latin = Pipeline::from_meta("transcode from english/bip39/raw into latin/default/raw").unwrap();
let back = Pipeline::from_meta("transcode from latin/default/raw into english/bip39/raw").unwrap();
let latin = to_latin.execute(mnemonic).unwrap();
assert_eq!(
back.execute(&latin).unwrap(),
mnemonic,
"explicit raw transcode must round-trip the seed words"
);
}
#[test]
fn test_verb_parsed_from_meta() {
assert_eq!(Pipeline::from_meta("encode into latin").unwrap().verb, Verb::Encode);
assert_eq!(Pipeline::from_meta("transcode from english into latin").unwrap().verb, Verb::Transcode);
assert_eq!(Pipeline::from_meta("translate from english into latin").unwrap().verb, Verb::Transcode);
assert_eq!(Pipeline::from_meta("decode from english").unwrap().verb, Verb::Decode);
assert_eq!(Pipeline::from_meta("latin").unwrap().verb, Verb::Unspecified);
}
#[test]
fn test_transcode_verb_auto_detects_source() {
let mnemonic = "abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon about";
let to_latin = Pipeline::from_meta("transcode into latin").unwrap();
let latin = to_latin.execute(mnemonic).unwrap();
let back = Pipeline::from_meta("transcode from latin into english/bip39/raw").unwrap();
assert_eq!(
back.execute(&latin).unwrap(),
mnemonic,
"transcode-verb auto-detect must round-trip the seed words"
);
}
#[test]
fn test_transcode_verb_errors_without_detectable_source() {
let pipe = Pipeline::from_meta("transcode into english").unwrap();
let err = pipe.execute("xyzzy plugh frobnicate").unwrap_err();
assert!(
matches!(err, PipelineError::InvalidPipeline(ref m) if m.contains("could not detect")),
"expected a 'could not detect source' error, got {:?}",
err
);
}
#[test]
fn test_raw_encode_decode_round_trip() {
let input = "cafe";
let encode = Pipeline::from_params(
Endpoint::Format(DataMode::Hex),
Endpoint::language_full("english", "bip39", "raw"),
);
let bare_words = encode.execute(input).unwrap();
assert_eq!(
bare_words.split_whitespace().count(),
2,
"raw output must be bare data words with no padding header, got: {bare_words:?}"
);
let decode = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "raw"),
Endpoint::Format(DataMode::Hex),
);
let decoded = decode.execute(&bare_words).unwrap();
assert_eq!(decoded, input, "raw encode/decode must round-trip");
}
#[test]
fn test_raw_decode_rich_returns_stats() {
let bare = "add garlic";
let pipeline = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "raw"),
Endpoint::Format(DataMode::Hex),
);
let plain = pipeline.execute(bare).unwrap();
let rich = pipeline.execute_rich(bare).unwrap();
assert_eq!(rich.output, plain, "rich and plain raw decode must agree");
assert_eq!(rich.output, "cafe");
assert_eq!(rich.payload_words, vec!["add".to_string(), "garlic".to_string()]);
let stats = rich.stats.expect("raw decode should report stats");
assert_eq!(stats.payload_count, 2);
assert_eq!(stats.cover_count, 0);
assert_eq!(stats.total_words, 2);
assert_eq!(stats.ratio, 1.0);
}
#[test]
fn test_english_bip39_dialect_preserves_seed_words() {
let seed = "abandon ability able about above absent absorb access accident account accuse achieve";
let encode = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "raw"),
Endpoint::language_full("english", "bip39", "bip39"),
)
.with_seed(42);
let prose = encode.execute(seed).unwrap();
for w in seed.split_whitespace() {
assert!(
prose.split_whitespace().any(|t| {
t.trim_matches(|c: char| !c.is_alphanumeric())
.eq_ignore_ascii_case(w)
}),
"seed word {w:?} missing from english bip39 prose: {prose:?}"
);
}
let recovered = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "bip39"),
Endpoint::language_full("english", "bip39", "raw"),
)
.execute(&prose)
.unwrap();
assert_eq!(
recovered.split_whitespace().collect::<Vec<_>>(),
seed.split_whitespace().collect::<Vec<_>>(),
"english bip39 dialect must round-trip to the original seed"
);
}
#[test]
fn test_decode_honors_target_format() {
let key = "sk-test-ABC123xyz";
let prose = Pipeline::from_params(
Endpoint::Auto,
Endpoint::language_full("english", "bip39", "body"),
)
.with_seed(42)
.execute(key)
.unwrap();
let decode = |fmt: DataMode| {
Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::Format(fmt),
)
.execute(&prose)
.unwrap()
};
let hex = decode(DataMode::Hex);
assert_eq!(
codec::hex_decode(&hex).expect("valid hex"),
key.as_bytes(),
"into hex must yield hex of the bytes, got {hex:?}"
);
let b64 = decode(DataMode::Base64);
assert_eq!(
codec::base64_decode(&b64).expect("valid base64"),
key.as_bytes(),
"into base64 must yield base64 of the bytes, got {b64:?}"
);
assert_eq!(decode(DataMode::Ascii7), key, "into ascii must yield the string");
let auto = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::Auto,
)
.execute(&prose)
.unwrap();
assert_eq!(auto, key, "Auto decode must be unchanged");
}
#[test]
fn test_transcode_bare_mnemonic_english_to_latin() {
let bare = "abandon ability able about";
let transcode = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::language("latin"),
)
.with_seed(42);
let latin = transcode
.execute(bare)
.expect("bare-word transcode should succeed, not error on byte alignment");
assert!(!latin.is_empty(), "Latin prose should not be empty");
let from_english = Pipeline::from_params(
Endpoint::language_full("english", "bip39", "body"),
Endpoint::Auto,
)
.execute(bare)
.unwrap();
let from_latin = Pipeline::from_params(
Endpoint::language("latin"),
Endpoint::Auto,
)
.execute(&latin)
.unwrap();
assert_eq!(
from_english, from_latin,
"bare-word transcode must be byte-lossless"
);
}
#[test]
fn test_btc_encode_decode_rich_round_trip() {
let btc_addr = "bc1q29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz";
let encode_pipeline = Pipeline::from_meta("encode into english naturally")
.unwrap()
.with_seed(42);
let encode_result = encode_pipeline.execute_rich(btc_addr).unwrap();
let prose = &encode_result.output;
assert!(!prose.is_empty());
let resolved = encode_result.resolved_source.as_ref().unwrap();
assert!(format!("{}", resolved).starts_with("crypto/btc"),
"resolved_source should be crypto/btc, got: {}", resolved);
let decode_pipeline = Pipeline::from_meta(
"translate from english naturally into crypto/btc/bech32/bip173"
).unwrap().with_seed(42);
let decode_result = decode_pipeline.execute_rich(prose).unwrap();
assert_eq!(decode_result.output, btc_addr,
"Round-trip should recover original bc1q address");
}
#[test]
fn test_meta_parse_encode_into_english() {
let p = Pipeline::from_meta("encode into english").unwrap();
assert_eq!(p.source, Endpoint::Auto);
assert_eq!(p.target, Endpoint::language("english"));
}
#[test]
fn test_meta_transcode_parse() {
let p = Pipeline::from_meta("translate from english into latin").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language("latin"));
}
#[test]
fn test_parse_html_flag() {
let p = Pipeline::from_meta("encode into pgp html").unwrap();
assert!(p.html, "html flag should be true");
assert_eq!(p.target, Endpoint::language_full("cs", "base58", "pgp"));
}
#[test]
fn test_parse_html_flag_order_independent() {
let p = Pipeline::from_meta("encode into html pgp").unwrap();
assert!(p.html, "html flag should be true regardless of order");
assert_eq!(p.target, Endpoint::language_full("cs", "base58", "pgp"));
}
#[test]
fn test_parse_no_html_flag() {
let p = Pipeline::from_meta("encode into pgp").unwrap();
assert!(!p.html, "html flag should be false when not specified");
}
#[test]
fn test_cover_override_with_html() {
let p = Pipeline::from_meta("encode into pgp html").unwrap();
assert_eq!(p.cover_override(), Some("html"));
}
#[test]
fn test_cover_override_without_html() {
let p = Pipeline::from_meta("encode into pgp").unwrap();
assert_eq!(p.cover_override(), None);
}
#[test]
fn test_pgp_routes_to_cs_pgp_dialect() {
let p = Pipeline::from_meta("encode into pgp").unwrap();
assert_eq!(p.target, Endpoint::language_full("cs", "base58", "pgp"),
"pgp should route to cs language with pgp dialect and base58 wordlist");
}
#[test]
fn test_nostr_routes_to_cs_nip04_dialect() {
let p = Pipeline::from_meta("encode into nostr").unwrap();
assert_eq!(p.target, Endpoint::language_full("cs", "base64", "nip04"),
"nostr should route to cs language with nip04 dialect and base64 wordlist");
}
#[test]
fn test_adverb_compactly() {
let p = Pipeline::from_meta("encode hex into english compactly").unwrap();
assert_eq!(p.length_mode, Some(SentenceLengthMode::Compact),
"compactly should set length_mode to Compact");
}
#[test]
fn test_adverb_efficiently() {
let p = Pipeline::from_meta("translate from hex into latin efficiently").unwrap();
assert_eq!(p.length_mode, Some(SentenceLengthMode::Compact),
"efficiently should set length_mode to Compact");
assert_eq!(p.k_max, Some(20),
"efficiently should set k_max to 20");
}
#[test]
fn test_adverb_naturally() {
let p = Pipeline::from_meta("encode hex into english naturally").unwrap();
assert_eq!(p.length_mode, Some(SentenceLengthMode::Natural),
"naturally should set length_mode to Natural");
}
#[test]
fn test_adverb_minimally() {
let p = Pipeline::from_meta("encode into latin minimally").unwrap();
assert_eq!(p.length_mode, Some(SentenceLengthMode::Compact),
"minimally should set length_mode to Compact");
assert_eq!(p.k_min, Some(2),
"minimally should set k_min to 2");
assert_eq!(p.k_max, Some(12),
"minimally should set k_max to 12");
}
#[test]
fn test_adverb_optimally() {
let p = Pipeline::from_meta("encode hex into english optimally").unwrap();
assert_eq!(p.variations, Some(50),
"optimally should set variations to 50");
}
#[test]
fn test_adverb_thoroughly() {
let p = Pipeline::from_meta("translate from hex into latin thoroughly").unwrap();
assert_eq!(p.variations, Some(100),
"thoroughly should set variations to 100");
}
#[test]
fn test_multiple_adverbs() {
let p = Pipeline::from_meta("encode hex into english compactly optimally").unwrap();
assert_eq!(p.length_mode, Some(SentenceLengthMode::Compact),
"compactly should set length_mode");
assert_eq!(p.variations, Some(50),
"optimally should set variations");
}
#[test]
fn test_adverb_with_dialect_modifier() {
let p = Pipeline::from_meta("encode into latin prose compactly").unwrap();
assert_eq!(p.target, Endpoint::language_with_dialect("latin", "prose"),
"prose dialect should be preserved");
assert_eq!(p.length_mode, Some(SentenceLengthMode::Compact),
"compactly should set length_mode");
}
#[test]
fn test_image_default_dialect_is_voronoi() {
let p = Pipeline::from_meta("encode into image").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "voronoi"),
"image should default to voronoi dialect with viridis wordlist");
}
#[test]
fn test_image_grid_dialect() {
let p = Pipeline::from_meta("encode into image grid").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "grid"),
"grid dialect modifier should override default voronoi");
}
#[test]
fn test_image_mosaic_dialect() {
let p = Pipeline::from_meta("encode into image mosaic").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "mosaic"));
}
#[test]
fn test_image_constellation_dialect() {
let p = Pipeline::from_meta("encode into image constellation").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "constellation"));
}
#[test]
fn test_image_raw_dialect() {
let p = Pipeline::from_meta("encode into image raw").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "raw"));
}
#[test]
fn test_image_patches_dialect() {
let p = Pipeline::from_meta("encode into image patches").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "patches"));
}
#[test]
fn test_image_transcode_from_english() {
let p = Pipeline::from_meta("translate from english into image voronoi").unwrap();
assert_eq!(p.source, Endpoint::language("english"));
assert_eq!(p.target, Endpoint::language_full("image", "viridis", "voronoi"));
}
#[test]
fn test_image_palette_plasma_voronoi() {
let p = Pipeline::from_meta("encode into image plasma voronoi").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "plasma", "voronoi"));
}
#[test]
fn test_image_palette_rocket_grid() {
let p = Pipeline::from_meta("encode into image rocket grid").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "rocket", "grid"));
}
#[test]
fn test_image_palette_without_dialect() {
let p = Pipeline::from_meta("encode into image magma").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "magma", "voronoi"),
"palette without dialect should default to voronoi");
}
#[test]
fn test_image_palette_order_dialect_first() {
let p = Pipeline::from_meta("encode into image voronoi plasma").unwrap();
assert_eq!(p.target, Endpoint::language_full("image", "plasma", "voronoi"));
}
#[test]
fn test_btc_routes_to_crypto_btc_bip173() {
let p = Pipeline::from_meta("encode into btc").unwrap();
assert_eq!(p.target, Endpoint::language_with_dialect("crypto/btc", "bip173"),
"btc should route to crypto/btc language with bip173 dialect");
}
#[test]
fn test_bitcoin_routes_to_crypto_btc_bip173() {
let p = Pipeline::from_meta("encode into bitcoin").unwrap();
assert_eq!(p.target, Endpoint::language_with_dialect("crypto/btc", "bip173"));
}
#[test]
fn test_npub_routes_to_crypto_nostr_pub() {
let p = Pipeline::from_meta("encode into npub").unwrap();
assert_eq!(p.target, Endpoint::language_with_dialect("crypto/nostr", "pub"));
}
#[test]
fn test_lightning_routes_to_crypto_ln_main() {
let p = Pipeline::from_meta("encode into lightning").unwrap();
assert_eq!(p.target, Endpoint::language_with_dialect("crypto/ln", "main"));
}
#[test]
fn test_crypto_detect_btc_bip173() {
assert_eq!(
detect_crypto_format("bc1q29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz"),
Some(("crypto/btc", "bip173", "29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz"))
);
}
#[test]
fn test_crypto_detect_btc_bip350() {
assert_eq!(
detect_crypto_format("bc1p29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz"),
Some(("crypto/btc", "bip350", "29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz"))
);
}
#[test]
fn test_crypto_detect_npub() {
let npub = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsutdnz";
assert_eq!(
detect_crypto_format(npub).map(|(lang, _, _)| lang),
Some("crypto/nostr")
);
}
#[test]
fn test_crypto_detect_sha256_no_longer_matched() {
let hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert!(detect_crypto_format(hash).is_none());
}
#[test]
fn test_crypto_payload_type_mapping() {
assert_eq!(crypto_payload_type("crypto/btc", "bip173"), "bech32");
assert_eq!(crypto_payload_type("crypto/btc", "bip350"), "bech32");
assert_eq!(crypto_payload_type("crypto/nostr", "pub"), "bech32");
assert_eq!(crypto_payload_type("crypto/btc", "p2pkh"), "base58");
assert_eq!(crypto_payload_type("crypto/btc", "p2pkh-test"), "base58");
assert_eq!(crypto_payload_type("crypto/cashu", "a"), "base64url");
assert_eq!(crypto_payload_type("crypto/ln", "main"), "bech32");
}
#[test]
fn test_crypto_dialect_prefix_mapping() {
assert_eq!(crypto_dialect_prefix("crypto/btc", "bip173"), "bc1q");
assert_eq!(crypto_dialect_prefix("crypto/btc", "bip173-test"), "tb1q");
assert_eq!(crypto_dialect_prefix("crypto/btc", "bip350"), "bc1p");
assert_eq!(crypto_dialect_prefix("crypto/btc", "bip350-test"), "tb1p");
assert_eq!(crypto_dialect_prefix("crypto/btc", "p2pkh"), "");
assert_eq!(crypto_dialect_prefix("crypto/nostr", "pub"), "npub1");
assert_eq!(crypto_dialect_prefix("crypto/cashu", "a"), "cashuA");
}
#[test]
fn test_crypto_auto_detect_btc_into_english() {
let p = Pipeline::from_meta("encode into english").unwrap();
let input = "bc1q29zsu7g0auf4f40avkcych9zzycd4ep3dvdxdgz";
let source = p.resolve_source(input).unwrap();
assert!(matches!(source, Endpoint::Language { ref language, ref dialect, .. }
if language == "crypto/btc" && dialect == "bip173"),
"Bitcoin address should auto-detect as crypto/btc/bip173, got {:?}", source);
}
#[test]
fn test_crypto_btc_address_round_trip() {
let btc_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
let hex_data = decode_crypto(btc_address, "crypto/btc", "bip173", false).unwrap();
let re_encoded = encode_crypto(&hex_data, "crypto/btc", "bip173", false).unwrap();
assert_eq!(re_encoded, btc_address.to_lowercase(),
"Bech32 decode → encode should recover original address");
}
#[test]
fn test_crypto_npub_round_trip() {
let npub = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsutdnz";
let hex_data = decode_crypto(npub, "crypto/nostr", "pub", false).unwrap();
let re_encoded = encode_crypto(&hex_data, "crypto/nostr", "pub", false).unwrap();
assert_eq!(re_encoded, npub.to_lowercase());
}
#[test]
fn test_crypto_bech32_packing() {
let values = vec![0, 14, 20, 15, 7, 13, 26, 0]; let packed = pack_5bit_values(&values);
let unpacked = unpack_5bit_values(&packed).unwrap();
assert_eq!(values, unpacked, "Pack/unpack should be identity");
}
#[test]
fn test_crypto_btc_address_decode_strips_prefix() {
let btc = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
let hex_data = decode_crypto(btc, "crypto/btc", "bip173", false).unwrap();
assert!(!hex_data.starts_with("bc"), "Prefix should be stripped");
}
}