From f599ae66f4375c0c4afc3363da48d8f8c691d8a5 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 12 Mar 2021 15:57:46 +0100 Subject: [PATCH] refactor: make human readable abi parsing more robust (#225) * refactor: event parser * refactor: function parser * refactor: add constructor parser * refactor: replace parsers * style: extract event arg parsing into separate method * fix: add missing returns statement * chore(clippy): make clippy happy * fix: support unnamed event argument parsing --- ethers-core/src/abi/human_readable.rs | 414 +++++++++++++++------ ethers/examples/contract_human_readable.rs | 2 +- 2 files changed, 311 insertions(+), 105 deletions(-) diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index e4df0881..99015248 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -1,9 +1,11 @@ -use super::{ - param_type::Reader, Abi, Event, EventParam, Function, Param, ParamType, StateMutability, -}; use std::collections::HashMap; + use thiserror::Error; +use super::{ + param_type::Reader, Abi, Constructor, Event, EventParam, Function, Param, StateMutability, +}; + /// Parses a "human readable abi" string vector /// /// ``` @@ -22,140 +24,290 @@ pub fn parse(input: &[&str]) -> Result { fallback: false, }; - for line in input { - if line.contains("function") { + for mut line in input.iter().map(|s| escape_quotes(s)) { + line = line.trim_start(); + if line.starts_with("function") { let function = parse_function(&line)?; abi.functions .entry(function.name.clone()) .or_default() .push(function); - } else if line.contains("event") { - let event = parse_event(&line)?; + } else if line.starts_with("event") { + let event = parse_event(line)?; abi.events .entry(event.name.clone()) .or_default() .push(event); - } else if line.starts_with("struct") { - panic!("Got tuple"); + } else if line.starts_with("constructor") { + abi.constructor = Some(parse_constructor(line)?); } else { - panic!("unknown sig") + return Err(ParseError::ParseError(super::Error::InvalidData)); } } Ok(abi) } -fn parse_event(event: &str) -> Result { - let split: Vec<&str> = event.split("event ").collect(); - let split: Vec<&str> = split[1].split('(').collect(); - let name = split[0].trim_end(); - let rest = split[1]; - - let args = rest.replace(")", ""); - let anonymous = rest.contains("anonymous"); - - let inputs = if args.contains(',') { - let args: Vec<&str> = args.split(", ").collect(); - args.iter() - .map(|arg| parse_event_arg(arg)) - .collect::, _>>()? - } else { - vec![] - }; - - Ok(Event { - name: name.to_owned(), - anonymous, - inputs, - }) +/// Parses an identifier like event or function name +fn parse_identifier(input: &mut &str) -> Result { + let mut chars = input.trim_start().chars(); + let mut name = String::new(); + let c = chars + .next() + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + if is_first_ident_char(c) { + name.push(c); + loop { + match chars.clone().next() { + Some(c) if is_ident_char(c) => { + chars.next(); + name.push(c); + } + _ => break, + } + } + } + *input = chars.as_str(); + Ok(name) } -// Parses an event's argument as indexed if neded -fn parse_event_arg(param: &str) -> Result { - let tokens: Vec<&str> = param.split(' ').collect(); - let kind: ParamType = Reader::read(tokens[0])?; - let (name, indexed) = if tokens.len() == 2 { - (tokens[1], false) - } else { - (tokens[2], true) - }; +/// Parses a solidity event declaration from `event (args*) anonymous?` +fn parse_event(mut event: &str) -> Result { + event = event.trim(); + if !event.starts_with("event ") { + return Err(ParseError::ParseError(super::Error::InvalidData)); + } + event = &event[5..]; - Ok(EventParam { - name: name.to_owned(), - kind, - indexed, - }) + let name = parse_identifier(&mut event)?; + if name.is_empty() { + return Err(ParseError::ParseError(super::Error::InvalidName( + event.to_owned(), + ))); + } + + let mut chars = event.chars(); + + loop { + match chars.next() { + None => return Err(ParseError::ParseError(super::Error::InvalidData)), + Some('(') => { + event = chars.as_str().trim(); + let mut anonymous = false; + if event.ends_with("anonymous") { + anonymous = true; + event = event[..event.len() - 9].trim_end(); + } + event = event + .trim() + .strip_suffix(')') + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + let inputs = if event.is_empty() { + Vec::new() + } else { + event + .split(',') + .map(parse_event_arg) + .collect::, _>>()? + }; + return Ok(Event { + name, + inputs, + anonymous, + }); + } + Some(' ') | Some('\t') => { + continue; + } + _ => { + return Err(ParseError::ParseError(super::Error::InvalidData)); + } + } + } } -fn parse_function(fn_string: &str) -> Result { - let fn_string = fn_string.to_owned(); - let delim = if fn_string.starts_with("function ") { - "function " +/// Parse a single event param +fn parse_event_arg(input: &str) -> Result { + let mut iter = input.trim().rsplitn(3, is_whitespace); + let mut indexed = false; + let mut name = iter + .next() + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + if let Some(mid) = iter.next() { + let kind; + if let Some(ty) = iter.next() { + if mid != "indexed" { + return Err(ParseError::ParseError(super::Error::InvalidData)); + } + indexed = true; + kind = Reader::read(ty)?; + } else { + if name == "indexed" { + indexed = true; + name = ""; + } + kind = Reader::read(mid)?; + } + Ok(EventParam { + name: name.to_owned(), + kind, + indexed, + }) } else { - " " - }; - let split: Vec<&str> = fn_string.split(delim).collect(); - let split: Vec<&str> = split[1].split('(').collect(); + Ok(EventParam { + name: "".to_owned(), + indexed, + kind: Reader::read(name)?, + }) + } +} - // function name is the first char - let fn_name = split[0]; +fn parse_function(mut input: &str) -> Result { + input = input.trim(); + if !input.starts_with("function ") { + return Err(ParseError::ParseError(super::Error::InvalidData)); + } + input = &input[8..]; + let name = parse_identifier(&mut input)?; + if name.is_empty() { + return Err(ParseError::ParseError(super::Error::InvalidName( + input.to_owned(), + ))); + } - // internal args - let args: Vec<&str> = split[1].split(')').collect(); - let args: Vec<&str> = args[0].split(", ").collect(); + let mut iter = input.split(" returns"); - let inputs = args - .into_iter() - .filter(|x| !x.is_empty()) - .filter(|x| !x.contains("returns")) - .map(|x| parse_param(x)) - .collect::, _>>()?; + let parens = iter + .next() + .ok_or(ParseError::ParseError(super::Error::InvalidData))? + .trim_end(); - // return value - let outputs: Vec = if split.len() > 2 { - let ret = split[2].strip_suffix(")").expect("no right paren"); - let ret: Vec<&str> = ret.split(", ").collect(); + let mut parens_iter = parens.rsplitn(2, ')'); + let mut modifiers = parens_iter.next(); - ret.into_iter() - // remove modifiers etc - .filter(|x| !x.is_empty()) - .map(|x| parse_param(x)) - .collect::, _>>()? + let input_params = if let Some(args) = parens_iter.next() { + args } else { - vec![] + modifiers + .take() + .ok_or(ParseError::ParseError(super::Error::InvalidData))? + } + .trim_start() + .strip_prefix('(') + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + let inputs = input_params + .split(',') + .filter(|s| !s.is_empty()) + .map(parse_param) + .collect::, _>>()?; + + let outputs = if let Some(params) = iter.next() { + let params = params + .trim() + .strip_prefix('(') + .and_then(|s| s.strip_suffix(')')) + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + params + .split(',') + .filter(|s| !s.is_empty()) + .map(parse_param) + .collect::, _>>()? + } else { + Vec::new() }; + let state_mutability = modifiers.map(detect_state_mutability).unwrap_or_default(); + #[allow(deprecated)] Ok(Function { - name: fn_name.to_owned(), + name, inputs, outputs, - // this doesn't really matter - state_mutability: StateMutability::NonPayable, + state_mutability, constant: false, }) } -// address x -fn parse_param(param: &str) -> Result { - let mut param = param - .split(' ') - .filter(|x| !x.contains("memory") || !x.contains("calldata")); - - let kind = param.next().ok_or(ParseError::Kind)?; - let kind: ParamType = Reader::read(kind).unwrap(); - - // strip memory/calldata from the name - // e.g. uint256[] memory x - let mut name = param.next().unwrap_or_default(); - if name == "memory" || name == "calldata" { - name = param.next().unwrap_or_default(); +fn parse_constructor(mut input: &str) -> Result { + input = input.trim(); + if !input.starts_with("constructor") { + return Err(ParseError::ParseError(super::Error::InvalidData)); } + input = input[11..] + .trim_start() + .strip_prefix('(') + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; - Ok(Param { - name: name.to_owned(), - kind, - }) + let params = input + .rsplitn(2, ')') + .last() + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + let inputs = params + .split(',') + .filter(|s| !s.is_empty()) + .map(parse_param) + .collect::, _>>()?; + + Ok(Constructor { inputs }) +} + +fn detect_state_mutability(s: &str) -> StateMutability { + if s.contains("pure") { + StateMutability::Pure + } else if s.contains("view") { + StateMutability::View + } else if s.contains("payable") { + StateMutability::Payable + } else { + StateMutability::NonPayable + } +} + +fn parse_param(param: &str) -> Result { + let mut iter = param.trim().rsplitn(3, is_whitespace); + + let name = iter + .next() + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + if let Some(ty) = iter.last() { + if name == "memory" || name == "calldata" { + Ok(Param { + name: "".to_owned(), + kind: Reader::read(ty)?, + }) + } else { + Ok(Param { + name: name.to_owned(), + kind: Reader::read(ty)?, + }) + } + } else { + Ok(Param { + name: "".to_owned(), + kind: Reader::read(name)?, + }) + } +} + +fn is_first_ident_char(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '_') +} + +fn is_ident_char(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') +} + +fn is_whitespace(c: char) -> bool { + matches!(c, ' ' | '\t') +} + +fn escape_quotes(input: &str) -> &str { + input.trim_matches(is_whitespace).trim_matches('\"') } #[derive(Error, Debug)] @@ -170,6 +322,7 @@ pub enum ParseError { #[cfg(test)] mod tests { use super::*; + use crate::abi::ParamType; #[test] fn parses_approve() { @@ -207,10 +360,31 @@ mod tests { assert!(parsed.outputs.is_empty()); } + #[test] + fn parses_function_payable() { + let fn_str = "function foo() public payable"; + let parsed = parse_function(fn_str).unwrap(); + assert_eq!(parsed.state_mutability, StateMutability::Payable); + } + + #[test] + fn parses_function_view() { + let fn_str = "function foo() external view"; + let parsed = parse_function(fn_str).unwrap(); + assert_eq!(parsed.state_mutability, StateMutability::View); + } + + #[test] + fn parses_function_pure() { + let fn_str = "function foo() pure"; + let parsed = parse_function(fn_str).unwrap(); + assert_eq!(parsed.state_mutability, StateMutability::Pure); + } + #[test] fn parses_event() { assert_eq!( - parse_event("event Foo (address indexed x, uint y, bytes32[] z)").unwrap(), + parse_event(&mut "event Foo (address indexed x, uint y, bytes32[] z)").unwrap(), Event { anonymous: false, name: "Foo".to_owned(), @@ -218,7 +392,7 @@ mod tests { EventParam { name: "x".to_owned(), kind: ParamType::Address, - indexed: true + indexed: true, }, EventParam { name: "y".to_owned(), @@ -230,7 +404,7 @@ mod tests { kind: ParamType::Array(Box::new(ParamType::FixedBytes(32))), indexed: false, }, - ] + ], } ); } @@ -238,7 +412,7 @@ mod tests { #[test] fn parses_anonymous_event() { assert_eq!( - parse_event("event Foo() anonymous").unwrap(), + parse_event(&mut "event Foo() anonymous").unwrap(), Event { anonymous: true, name: "Foo".to_owned(), @@ -247,6 +421,38 @@ mod tests { ); } + #[test] + fn parses_unnamed_event() { + assert_eq!( + parse_event(&mut "event Foo(address)").unwrap(), + Event { + anonymous: false, + name: "Foo".to_owned(), + inputs: vec![EventParam { + name: "".to_owned(), + kind: ParamType::Address, + indexed: false, + }], + } + ); + } + + #[test] + fn parses_unnamed_indexed_event() { + assert_eq!( + parse_event(&mut "event Foo(address indexed)").unwrap(), + Event { + anonymous: false, + name: "Foo".to_owned(), + inputs: vec![EventParam { + name: "".to_owned(), + kind: ParamType::Address, + indexed: true, + }], + } + ); + } + #[test] fn parse_event_input() { assert_eq!( @@ -254,7 +460,7 @@ mod tests { EventParam { name: "x".to_owned(), kind: ParamType::Address, - indexed: true + indexed: true, } ); @@ -263,7 +469,7 @@ mod tests { EventParam { name: "x".to_owned(), kind: ParamType::Address, - indexed: false + indexed: false, } ); } @@ -304,7 +510,7 @@ mod tests { fn can_read_backslashes() { parse(&[ "\"function setValue(string)\"", - "\"function getValue() external view (string)\"", + "\"function getValue() external view returns(string)\"", ]) .unwrap(); } diff --git a/ethers/examples/contract_human_readable.rs b/ethers/examples/contract_human_readable.rs index 3b6b4ca3..ef3c0b52 100644 --- a/ethers/examples/contract_human_readable.rs +++ b/ethers/examples/contract_human_readable.rs @@ -11,7 +11,7 @@ abigen!( SimpleContract, r#"[ function setValue(string) - function getValue() external view (string) + function getValue() external view returns (string) event ValueChanged(address indexed author, string oldValue, string newValue) ]"#, event_derives(serde::Deserialize, serde::Serialize)