use super::registry::{NamedPredicate, PredicateRegistry};
use serde_json::Value;
use std::sync::Arc;
pub fn register_crypto_predicates(registry: &mut PredicateRegistry) {
registry.register(Arc::new(IsBase58Predicate));
registry.register(Arc::new(IsBase64Predicate));
registry.register(Arc::new(IsBitcoinAddressPredicate));
registry.register(Arc::new(IsEthereumAddressPredicate));
registry.register(Arc::new(IsSolanaAddressPredicate));
registry.register(Arc::new(IsJwtPredicate));
registry.register(Arc::new(IsHashPredicate));
}
const BASE58_CHARS: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
fn is_base58_str(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| BASE58_CHARS.contains(c))
}
struct IsBase58Predicate;
impl NamedPredicate for IsBase58Predicate {
fn name(&self) -> &str {
"is_base58"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
is_base58_str(s)
}
}
struct IsBase64Predicate;
impl NamedPredicate for IsBase64Predicate {
fn name(&self) -> &str {
"is_base64"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
let url_safe = args
.get("url_safe")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let without_pad = s.trim_end_matches('=');
let valid = if url_safe {
without_pad
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
} else {
without_pad
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/')
};
if !valid {
return false;
}
let pad_count = s.len() - without_pad.len();
if pad_count > 2 {
return false;
}
if pad_count > 0 && s.len() % 4 != 0 {
return false;
}
true
}
}
const BECH32_CHARS: &str = "023456789acdefghjklmnpqrstuvwxyz";
struct IsBitcoinAddressPredicate;
impl NamedPredicate for IsBitcoinAddressPredicate {
fn name(&self) -> &str {
"is_bitcoin_address"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
if s.starts_with('1') || s.starts_with('3') {
return (25..=34).contains(&s.len()) && is_base58_str(s);
}
if let Some(rest) = s.to_lowercase().strip_prefix("bc1") {
if rest.starts_with('q') && s.len() == 42 {
return rest.chars().all(|c| BECH32_CHARS.contains(c) || c == 'q');
}
if rest.starts_with('p') && s.len() == 62 {
return rest.chars().all(|c| BECH32_CHARS.contains(c) || c == 'p');
}
return false;
}
false
}
}
struct IsEthereumAddressPredicate;
impl NamedPredicate for IsEthereumAddressPredicate {
fn name(&self) -> &str {
"is_ethereum_address"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let hex = match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
Some(h) => h,
None => return false,
};
hex.len() == 40 && hex.chars().all(|c| c.is_ascii_hexdigit())
}
}
struct IsSolanaAddressPredicate;
impl NamedPredicate for IsSolanaAddressPredicate {
fn name(&self) -> &str {
"is_solana_address"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
(32..=44).contains(&s.len()) && is_base58_str(s)
}
}
struct IsJwtPredicate;
impl NamedPredicate for IsJwtPredicate {
fn name(&self) -> &str {
"is_jwt"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 || parts.iter().any(|p| p.is_empty()) {
return false;
}
parts.iter().all(|part| {
part.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
})
}
}
struct IsHashPredicate;
impl NamedPredicate for IsHashPredicate {
fn name(&self) -> &str {
"is_hash"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if !s.chars().all(|c| c.is_ascii_hexdigit()) {
return false;
}
let expected_len = match args
.get("algorithm")
.and_then(|v| v.as_str())
.map(|v| v.to_lowercase())
.as_deref()
{
Some("md5") => Some(32),
Some("sha1") => Some(40),
Some("sha224") => Some(56),
Some("sha256") => Some(64),
Some("sha384") => Some(96),
Some("sha512") => Some(128),
Some(_) => return false,
None => None,
};
if let Some(expected) = expected_len {
s.len() == expected
} else {
matches!(s.len(), 32 | 40 | 56 | 64 | 96 | 128)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn eval(pred: &dyn NamedPredicate, value: &str, args: Value) -> bool {
pred.evaluate(&json!(value), &args)
}
#[test]
fn test_base58_valid() {
let p = IsBase58Predicate;
assert!(eval(
&p,
"5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ",
json!({})
));
assert!(eval(
&p,
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789",
json!({})
));
}
#[test]
fn test_base58_invalid() {
let p = IsBase58Predicate;
assert!(!eval(&p, "0ABC", json!({})));
assert!(!eval(&p, "OABC", json!({})));
assert!(!eval(&p, "IABC", json!({})));
assert!(!eval(&p, "lABC", json!({})));
assert!(!eval(&p, "", json!({})));
}
#[test]
fn test_base64_valid() {
let p = IsBase64Predicate;
assert!(eval(&p, "SGVsbG8gV29ybGQ=", json!({})));
assert!(eval(&p, "dGVzdA==", json!({})));
assert!(eval(&p, "AQID", json!({})));
assert!(eval(&p, "AQIDBA", json!({})));
}
#[test]
fn test_base64_url_safe() {
let p = IsBase64Predicate;
assert!(eval(&p, "abc-def_ghi", json!({"url_safe": true})));
assert!(!eval(&p, "abc+def/ghi", json!({"url_safe": true})));
}
#[test]
fn test_base64_invalid() {
let p = IsBase64Predicate;
assert!(!eval(&p, "", json!({})));
assert!(!eval(&p, "Hello World!", json!({})));
assert!(!eval(&p, "A===", json!({})));
}
#[test]
fn test_bitcoin_p2pkh() {
let p = IsBitcoinAddressPredicate;
assert!(eval(&p, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", json!({})));
assert!(eval(&p, "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", json!({})));
}
#[test]
fn test_bitcoin_p2sh() {
let p = IsBitcoinAddressPredicate;
assert!(eval(&p, "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", json!({})));
}
#[test]
fn test_bitcoin_bech32() {
let p = IsBitcoinAddressPredicate;
assert!(eval(
&p,
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
json!({})
));
assert!(eval(
&p,
"bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3s7a",
json!({})
));
}
#[test]
fn test_bitcoin_invalid() {
let p = IsBitcoinAddressPredicate;
assert!(!eval(&p, "1A1z", json!({})));
assert!(!eval(&p, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfOa", json!({})));
assert!(!eval(
&p,
"0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
json!({})
));
assert!(!eval(&p, "", json!({})));
}
#[test]
fn test_ethereum_valid() {
let p = IsEthereumAddressPredicate;
assert!(eval(
&p,
"0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
json!({})
));
assert!(eval(
&p,
"0x0000000000000000000000000000000000000000",
json!({})
));
assert!(eval(
&p,
"0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
json!({})
));
assert!(eval(
&p,
"0xDE0B295669A9FD93D5F28D9EC85E40F4CB697BAE",
json!({})
));
}
#[test]
fn test_ethereum_invalid() {
let p = IsEthereumAddressPredicate;
assert!(!eval(
&p,
"742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
json!({})
));
assert!(!eval(&p, "0x742d35Cc", json!({})));
assert!(!eval(
&p,
"0xZZZd35Cc6634C0532925a3b844Bc9e7595f2bD18",
json!({})
));
}
#[test]
fn test_solana_valid() {
let p = IsSolanaAddressPredicate;
assert!(eval(
&p,
"7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
json!({})
));
assert!(eval(
&p,
"DRpbCBMxVnDK7maPGv5ZoMSGn3BbiTSx2CLEQE8VBUQP",
json!({})
));
}
#[test]
fn test_solana_invalid() {
let p = IsSolanaAddressPredicate;
assert!(!eval(&p, "7EcDhSYGxXyscszYEp35KHN8vvw", json!({})));
assert!(!eval(
&p,
"0EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
json!({})
));
assert!(!eval(
&p,
"0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
json!({})
));
}
#[test]
fn test_jwt_valid() {
let p = IsJwtPredicate;
assert!(eval(
&p,
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
json!({})
));
}
#[test]
fn test_jwt_invalid() {
let p = IsJwtPredicate;
assert!(!eval(&p, "abc.def", json!({})));
assert!(!eval(&p, "abc.def.ghi.jkl", json!({})));
assert!(!eval(&p, "abc.def.g$%", json!({})));
}
#[test]
fn test_hash_valid() {
let p = IsHashPredicate;
assert!(eval(
&p,
"d41d8cd98f00b204e9800998ecf8427e",
json!({"algorithm":"md5"})
));
assert!(eval(
&p,
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
json!({"algorithm":"sha256"})
));
assert!(eval(
&p,
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
json!({})
));
}
#[test]
fn test_hash_invalid() {
let p = IsHashPredicate;
assert!(!eval(
&p,
"2cf24dba5fb0a30e26e83b2ac5b9e29e",
json!({"algorithm":"sha256"})
));
assert!(!eval(&p, "not-a-hash", json!({})));
assert!(!eval(
&p,
"d41d8cd98f00b204e9800998ecf8427e",
json!({"algorithm":"unknown"})
));
}
}