Spaces:
Build error
Build error
| //! LangSec formal recognizers β nom all_consuming parsers. | |
| //! All input validation is centralised here. Nothing reaches business logic | |
| //! without passing through one of these recognizers. | |
| use crate::types::{BtfsCid, EvmAddress, Isrc, ParseError, RoyaltySplit}; | |
| use nom::{ | |
| bytes::complete::{tag, take_while_m_n}, | |
| sequence::tuple, | |
| IResult, | |
| }; | |
| // ββ ISRC: CC-XXX-YY-NNNNN ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fn parse_isrc_inner(i: &str) -> IResult<&str, &str> { | |
| let (i, (_, _, _, _, _, _, _, _, _, _)) = tuple(( | |
| take_while_m_n(2, 2, |c: char| c.is_ascii_uppercase()), // CC | |
| tag("-"), | |
| take_while_m_n(3, 3, |c: char| c.is_ascii_alphanumeric()), // XXX | |
| tag("-"), | |
| take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), // YY | |
| tag("-"), | |
| take_while_m_n(5, 5, |c: char| c.is_ascii_digit()), // NNNNN | |
| nom::combinator::peek(nom::combinator::eof), | |
| nom::combinator::eof, | |
| nom::combinator::success(""), | |
| ))(i)?; | |
| Ok(("", i)) | |
| } | |
| pub fn recognize_isrc(input: &str) -> Result<Isrc, ParseError> { | |
| // CC-XXX-YY-NNNNN = 2+1+3+1+2+1+5 = 15 chars | |
| if input.len() != 15 { | |
| return Err(ParseError::InvalidLength { | |
| expected: 15, | |
| got: input.len(), | |
| }); | |
| } | |
| let parts: Vec<&str> = input.split('-').collect(); | |
| if parts.len() != 4 { | |
| return Err(ParseError::InvalidFormat(input.into())); | |
| } | |
| if parts[0].len() != 2 || !parts[0].chars().all(|c| c.is_ascii_uppercase()) { | |
| return Err(ParseError::InvalidFormat( | |
| "CC must be 2 uppercase letters".into(), | |
| )); | |
| } | |
| if parts[1].len() != 3 || !parts[1].chars().all(|c| c.is_ascii_alphanumeric()) { | |
| return Err(ParseError::InvalidFormat( | |
| "Registrant must be 3 alphanumeric".into(), | |
| )); | |
| } | |
| if parts[2].len() != 2 || !parts[2].chars().all(|c| c.is_ascii_digit()) { | |
| return Err(ParseError::InvalidFormat("Year must be 2 digits".into())); | |
| } | |
| if parts[3].len() != 5 || !parts[3].chars().all(|c| c.is_ascii_digit()) { | |
| return Err(ParseError::InvalidFormat( | |
| "Designation must be 5 digits".into(), | |
| )); | |
| } | |
| Ok(Isrc(input.to_string())) | |
| } | |
| // ββ BTFS CID βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pub fn recognize_btfs_cid(input: &str) -> Result<BtfsCid, ParseError> { | |
| // CIDv0: Qm... (46 chars base58) | |
| // CIDv1: bafy... or b... (variable, base32/base64) | |
| if input.len() < 10 { | |
| return Err(ParseError::InvalidLength { | |
| expected: 46, | |
| got: input.len(), | |
| }); | |
| } | |
| let valid = input | |
| .chars() | |
| .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='); | |
| if !valid { | |
| return Err(ParseError::InvalidFormat( | |
| "CID contains invalid characters".into(), | |
| )); | |
| } | |
| Ok(BtfsCid(input.to_string())) | |
| } | |
| // ββ EVM address ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pub fn recognize_evm_address(input: &str) -> Result<EvmAddress, ParseError> { | |
| let s = input.strip_prefix("0x").unwrap_or(input); | |
| if s.len() != 40 { | |
| return Err(ParseError::InvalidLength { | |
| expected: 40, | |
| got: s.len(), | |
| }); | |
| } | |
| if !s.chars().all(|c| c.is_ascii_hexdigit()) { | |
| return Err(ParseError::InvalidFormat("address must be hex".into())); | |
| } | |
| Ok(EvmAddress(format!("0x{}", s.to_lowercase()))) | |
| } | |
| // ββ Tx hash βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pub fn recognize_tx_hash(input: &str) -> Result<String, ParseError> { | |
| let s = input.strip_prefix("0x").unwrap_or(input); | |
| if s.len() != 64 { | |
| return Err(ParseError::InvalidLength { | |
| expected: 64, | |
| got: s.len(), | |
| }); | |
| } | |
| if !s.chars().all(|c| c.is_ascii_hexdigit()) { | |
| return Err(ParseError::InvalidFormat("tx hash must be hex".into())); | |
| } | |
| Ok(format!("0x{}", s.to_lowercase())) | |
| } | |
| // ββ Royalty splits: Vec<(address, bps)>, Ξ£bps == 10_000 βββββββββββββββββ | |
| pub fn recognize_splits(raw: &[(String, u16)]) -> Result<Vec<RoyaltySplit>, ParseError> { | |
| let mut splits = Vec::new(); | |
| let mut total = 0u32; | |
| for (addr, bps) in raw { | |
| let address = recognize_evm_address(addr)?; | |
| total += *bps as u32; | |
| splits.push(RoyaltySplit { | |
| address, | |
| bps: *bps, | |
| amount_btt: 0, | |
| }); | |
| } | |
| if total != 10_000 { | |
| return Err(ParseError::InvalidFormat(format!( | |
| "bps sum {total} β 10_000" | |
| ))); | |
| } | |
| Ok(splits) | |
| } | |