diff --git a/Cargo.lock b/Cargo.lock index e815e220b..2c6aca48e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,7 +286,7 @@ name = "async-jsonrpc-client" version = "0.1.0" source = "git+https://github.com/witnet/async-jsonrpc-client?branch=fix-tcp-leak#600a2d696af265511868f77c01e448c1d678650e" dependencies = [ - "error-chain", + "error-chain 0.12.4", "futures 0.1.31", "jsonrpc-core 10.1.0", "log 0.4.27", @@ -553,6 +553,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.0" @@ -1178,6 +1187,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "error-chain" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" +dependencies = [ + "backtrace", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -2373,6 +2391,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonpath" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8061db09019f1879ba586685694fe18279f597b1b3a9dd308f35e596be6cdf7d" +dependencies = [ + "error-chain 0.11.0", + "pest", + "pest_derive", + "serde", + "serde_json", +] + [[package]] name = "jsonrpc-core" version = "10.1.0" @@ -3436,6 +3467,23 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fce5d8b5cc33983fc74f78ad552b5522ab41442c4ca91606e4236eb4b5ceefc" + +[[package]] +name = "pest_derive" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3294f437119209b084c797604295f40227cffa35c57220b1e99a6ff3bf8ee4" +dependencies = [ + "pest", + "quote 0.3.15", + "syn 0.11.11", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3706,6 +3754,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + [[package]] name = "quote" version = "0.6.13" @@ -4672,6 +4726,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slicestring" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84214dff6bab2662cd094e247e6b73728ff0e23111d78124146801ca18dd47b4" + [[package]] name = "smallvec" version = "0.6.14" @@ -4821,6 +4881,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid 0.0.4", +] + [[package]] name = "syn" version = "0.15.44" @@ -4869,6 +4940,15 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid 0.0.4", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -5637,6 +5717,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -6683,20 +6769,25 @@ name = "witnet_rad" version = "0.3.2" dependencies = [ "anyhow", + "base64 0.22.1", + "bs58", "cbor-codec", "futures 0.3.31", "hex", "http 0.2.12", "if_rust_version", + "jsonpath", "log 0.4.27", "minidom", "num_enum", "ordered-float 3.9.2", "rand 0.7.3", + "regex", "reqwest 0.12.23", "serde", "serde_cbor", "serde_json", + "slicestring", "thiserror 2.0.16", "tokio 1.47.1", "url", diff --git a/rad/Cargo.toml b/rad/Cargo.toml index 1caca822d..7bd68b53e 100644 --- a/rad/Cargo.toml +++ b/rad/Cargo.toml @@ -14,10 +14,13 @@ tokio = "1.44.1" [dependencies] anyhow = "1.0.98" +base64 = "0.22.1" +bs58 = "0.5.1" cbor-codec = { git = "https://github.com/witnet/cbor-codec.git", branch = "feat/ldexpf-shim" } futures = "0.3.31" hex = "0.4.1" if_rust_version = "1.0.0" +jsonpath = "0.1.1" # the http crate is used to perform additional validations before passing arguments to the surf http client # the version of http must be kept in sync with the version used by surf http = "0.2.1" @@ -26,10 +29,12 @@ minidom = { git = "https://github.com/witnet/xmpp-rs", rev = "bc8a33ff5da95ee403 num_enum = "0.7.3" ordered-float = "3.9.2" rand = "0.7.3" +regex = "1.4.2" reqwest = { version = "0.12.15", features = ["socks"] } serde = "1.0.111" serde_cbor = "0.11.2" serde_json = "1.0.96" +slicestring = "0.3.2" thiserror = "2.0.12" # the url crate is used to perform additional validations before passing arguments to the surf http client # the version of url must be kept in sync with the version used by surf in the `witnet_net` crate diff --git a/rad/src/error.rs b/rad/src/error.rs index 8cfeaba45..ac479374e 100644 --- a/rad/src/error.rs +++ b/rad/src/error.rs @@ -35,6 +35,12 @@ pub enum RadError { /// Failed to parse an object from a JSON buffer #[error("Failed to parse an object from a JSON buffer: {description:?}")] JsonParse { description: String }, + /// The given JSON path is not present in a JSON-stringified object + #[error("Failed to find JSON path `{path}` from RadonString")] + JsonPathNotFound { path: String }, + /// Failed to parse a JSON path selector from a string value + #[error("Failed to parse a JSON path from a string value: {description:?}")] + JsonPathParse { description: String }, /// Failed to parse an object from a XML buffer #[error("Failed to parse an object from a XML buffer: {description:?}")] XmlParse { description: String }, diff --git a/rad/src/operators/bytes.rs b/rad/src/operators/bytes.rs index f8b1ecb32..701606fd3 100644 --- a/rad/src/operators/bytes.rs +++ b/rad/src/operators/bytes.rs @@ -1,14 +1,31 @@ +use base64::Engine; use serde_cbor::value::{Value, from_value}; use std::convert::TryFrom; use crate::{ error::RadError, hash_functions::{self, RadonHashFunctions}, - types::{RadonType, bytes::RadonBytes, string::RadonString}, + types::{ + RadonType, + bytes::{RadonBytes, RadonBytesEncoding}, + integer::RadonInteger, + string::RadonString, + }, }; -pub fn to_string(input: &RadonBytes) -> Result { - RadonString::try_from(Value::Text(hex::encode(input.value()))) +pub fn as_integer(input: &RadonBytes) -> Result { + let input_value_len = input.value().len(); + match input_value_len { + 1..=16 => { + let mut bytes_array = [0u8; 16]; + bytes_array[16 - input_value_len..].copy_from_slice(&input.value()); + Ok(RadonInteger::from(i128::from_be_bytes(bytes_array))) + } + 17.. => Err(RadError::ParseInt { + message: "Input buffer too big".to_string(), + }), + _ => Err(RadError::EmptyArray), + } } pub fn hash(input: &RadonBytes, args: &[Value]) -> Result { @@ -27,6 +44,62 @@ pub fn hash(input: &RadonBytes, args: &[Value]) -> Result Ok(RadonBytes::from(digest)) } + +pub fn length(input: &RadonBytes) -> RadonInteger { + RadonInteger::from(input.value().len() as i128) +} + +pub fn slice(input: &RadonBytes, args: &[Value]) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "BytesSlice".to_string(), + args: args.to_vec(), + }; + let end_index = input.value().len(); + if end_index > 0 { + let start_index = from_value::(args[0].clone()) + .unwrap_or_default() + .rem_euclid(end_index as i64) as usize; + let mut slice = input.value().as_slice().split_at(start_index).1.to_vec(); + if args.len() == 2 { + let end_index = from_value::(args[1].clone()) + .unwrap_or_default() + .rem_euclid(end_index as i64) as usize; + slice.truncate(end_index - start_index); + } + Ok(RadonBytes::from(slice)) + } else { + Err(wrong_args()) + } +} + +pub fn to_string(input: &RadonBytes, args: &Option>) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "Stringify".to_string(), + args: args.to_owned().unwrap_or_default().to_vec(), + }; + let mut bytes_encoding = RadonBytesEncoding::Hex; + if let Some(args) = args { + if !args.is_empty() { + let arg = args.first().ok_or_else(wrong_args)?.to_owned(); + let bytes_encoding_u8 = from_value::(arg).map_err(|_| wrong_args())?; + bytes_encoding = + RadonBytesEncoding::try_from(bytes_encoding_u8).map_err(|_| wrong_args())?; + } + } + match bytes_encoding { + RadonBytesEncoding::Hex => RadonString::try_from(Value::Text(hex::encode(input.value()))), + RadonBytesEncoding::Base58 => RadonString::try_from(Value::Text(bs58::encode(input.value()).into_string())), + RadonBytesEncoding::Base64 => RadonString::try_from(Value::Text( + base64::engine::general_purpose::STANDARD.encode(input.value()), + )), + RadonBytesEncoding::Utf8 => Ok(RadonString::from( + String::from_utf8(input.value().to_vec()).unwrap_or_default(), + )), + } +} + #[cfg(test)] mod tests { use super::*; @@ -34,7 +107,8 @@ mod tests { #[test] fn test_bytes_to_string() { let input = RadonBytes::from(vec![0x01, 0x02, 0x03]); - let output = to_string(&input).unwrap().value(); + let valid_args = Some(vec![Value::from(0x00)]); + let output = to_string(&input, &valid_args).unwrap().value(); let valid_expected = "010203".to_string(); diff --git a/rad/src/operators/mod.rs b/rad/src/operators/mod.rs index f57c5b9b7..9c5c670d2 100644 --- a/rad/src/operators/mod.rs +++ b/rad/src/operators/mod.rs @@ -49,8 +49,12 @@ pub enum RadonOpCodes { BooleanNegate = 0x22, /////////////////////////////////////////////////////////////////////// // Bytes operator codes (start at 0x30) - BytesAsString = 0x30, + BytesToString = 0x30, BytesHash = 0x31, + BytesAsInteger = 0x32, + BytesLength = 0x34, + BytesSlice = 0x3C, + /////////////////////////////////////////////////////////////////////// // Integer operator codes (start at 0x40) IntegerAbsolute = 0x40, @@ -96,7 +100,7 @@ pub enum RadonOpCodes { /////////////////////////////////////////////////////////////////////// // String operator codes (start at 0x70) StringAsBoolean = 0x70, - // StringAsBytes = 0x71, + StringAsBytes = 0x71, StringAsFloat = 0x72, StringAsInteger = 0x73, StringLength = 0x74, @@ -106,6 +110,9 @@ pub enum RadonOpCodes { StringParseXMLMap = 0x78, StringToLowerCase = 0x79, StringToUpperCase = 0x7A, + StringReplace = 0x7B, + StringSlice = 0x7C, + StringSplit = 0x7D, } impl fmt::Display for RadonOpCodes { diff --git a/rad/src/operators/string.rs b/rad/src/operators/string.rs index f9bc351e4..6ce4da832 100644 --- a/rad/src/operators/string.rs +++ b/rad/src/operators/string.rs @@ -4,15 +4,25 @@ use std::{ str::FromStr, }; +use base64::Engine; +use jsonpath::Selector; +use regex::Regex; use serde_cbor::value::{Value, from_value}; use serde_json::Value as JsonValue; +use slicestring::Slice; use crate::{ error::RadError, hash_functions::{self, RadonHashFunctions}, types::{ - RadonType, RadonTypes, array::RadonArray, boolean::RadonBoolean, bytes::RadonBytes, - float::RadonFloat, integer::RadonInteger, map::RadonMap, string::RadonString, + RadonType, RadonTypes, + array::RadonArray, + boolean::RadonBoolean, + bytes::{RadonBytes, RadonBytesEncoding}, + float::RadonFloat, + integer::RadonInteger, + map::RadonMap, + string::RadonString, }, }; @@ -20,6 +30,108 @@ const MAX_DEPTH: u8 = 20; const DEFAULT_THOUSANDS_SEPARATOR: &str = ","; const DEFAULT_DECIMAL_SEPARATOR: &str = "."; +pub fn as_bool(input: &RadonString) -> Result { + let str_value = radon_trim(input); + bool::from_str(&str_value) + .map(RadonBoolean::from) + .map_err(Into::into) +} + +pub fn as_bytes(input: &RadonString, args: &Option>) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "AsBytes".to_string(), + args: args.to_owned().unwrap_or_default().to_vec(), + }; + let mut input_string = input.value(); + if input_string.starts_with("0x") { + input_string = input_string.slice(2..); + } + if input_string.len() % 2 != 0 { + input_string.insert(0, '0'); + } + let mut bytes_encoding = RadonBytesEncoding::Hex; + if let Some(args) = args { + if !args.is_empty() { + let arg = args.first().ok_or_else(wrong_args)?.to_owned(); + let bytes_encoding_u8 = from_value::(arg).map_err(|_| wrong_args())?; + bytes_encoding = + RadonBytesEncoding::try_from(bytes_encoding_u8).map_err(|_| wrong_args())?; + } + } + match bytes_encoding { + RadonBytesEncoding::Hex => Ok(RadonBytes::from( + hex::decode(input_string.as_str()).map_err(|_err| RadError::Decode { + from: "RadonString", + to: "RadonBytes", + })?, + )), + RadonBytesEncoding::Base58 => Ok(RadonBytes::from( + bs58::decode(input.value()) + .into_vec() + .map_err(|_err| RadError::Decode { + from: "RadonString", + to: "RadonBytes", + })?, + )), + RadonBytesEncoding::Base64 => Ok(RadonBytes::from( + base64::engine::general_purpose::STANDARD + .decode(input.value()) + .map_err(|_err| RadError::Decode { + from: "RadonString", + to: "RadonBytes", + })?, + )), + RadonBytesEncoding::Utf8 => Ok(RadonBytes::from(input.value().as_bytes().to_vec())), + } +} + +/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents +/// a valid floating point number. +pub fn as_float(input: &RadonString, args: &Option>) -> Result { + f64::from_str(&to_numeric_string( + input, + args.as_deref().unwrap_or_default(), + )) + .map(RadonFloat::from) + .map_err(Into::into) +} + +/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents +/// a valid integer number. +pub fn as_integer( + input: &RadonString, + args: &Option>, +) -> Result { + i128::from_str(&to_numeric_string( + input, + args.as_deref().unwrap_or_default(), + )) + .map(RadonInteger::from) + .map_err(Into::into) +} + +pub fn hash(input: &RadonString, args: &[Value]) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "Hash".to_string(), + args: args.to_vec(), + }; + + let input_string = input.value(); + let input_bytes = input_string.as_bytes(); + + let arg = args.first().ok_or_else(wrong_args)?.to_owned(); + let hash_function_integer = from_value::(arg).map_err(|_| wrong_args())?; + let hash_function_code = + RadonHashFunctions::try_from(hash_function_integer).map_err(|_| wrong_args())?; + + let digest = hash_functions::hash(input_bytes, hash_function_code)?; + let hex_string = hex::encode(digest); + + Ok(RadonString::from(hex_string)) +} + /// Parse `RadonTypes` from a JSON-encoded `RadonString`. pub fn parse_json(input: &RadonString) -> Result { let json_value: JsonValue = @@ -30,14 +142,90 @@ pub fn parse_json(input: &RadonString) -> Result { RadonTypes::try_from(json_value) } -pub fn parse_json_map(input: &RadonString) -> Result { - let item = parse_json(input)?; - item.try_into() +pub fn parse_json_array(input: &RadonString, args: &Option>) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "ParseJsonArray".to_string(), + args: args.to_owned().unwrap_or_default(), + }; + + let json_input: JsonValue = serde_json::from_str(&input.value()) + .map_err(|err| RadError::JsonParse { + description: err.to_string(), + })?; + + match args.to_owned().unwrap_or_default().first() { + Some(Value::Array(values)) => { + let mut items: Vec = vec![]; + for path in values { + if let Value::Text(json_path) = path { + let selector = Selector::new(json_path.as_str()) + .map_err(|err| RadError::JsonPathParse { + description: err.to_string(), + })?; + let mut subitems: Vec = selector.find(&json_input) + .map(into_radon_types) + .collect(); + if subitems.len() > 1 { + items.insert(items.len(), RadonArray::from(subitems).into()); + } else { + items.append(subitems.as_mut()); + } + } else { + return Err(wrong_args()); + } + } + Ok(RadonArray::from(items)) + } + Some(Value::Text(json_path)) => { + let selector = Selector::new(json_path.as_str()) + .map_err(|err| RadError::JsonPathParse { + description: err.to_string(), + })?; + let items: Vec = selector.find(&json_input) + .map(into_radon_types) + .collect(); + Ok(RadonArray::from(items)) + } + None => { + RadonTypes::try_from(json_input)?.try_into() + } + _ => Err(wrong_args()) + } } -pub fn parse_json_array(input: &RadonString) -> Result { - let item = parse_json(input)?; - item.try_into() +pub fn parse_json_map(input: &RadonString, args: &Option>) -> Result { + let not_found = |json_path: &str| RadError::JsonPathNotFound { + path: String::from(json_path) + }; + + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "ParseJsonMap".to_string(), + args: args.to_owned().unwrap_or_default(), + }; + + let json_input: JsonValue = serde_json::from_str(&input.value()) + .map_err(|err| RadError::JsonParse { + description: err.to_string(), + })?; + + match args.to_owned().unwrap_or_default().first() { + Some(Value::Text(json_path)) => { + let selector = Selector::new(json_path.as_str()) + .map_err(|err| RadError::JsonPathParse { + description: err.to_string(), + })?; + let item = selector.find(&json_input) + .next() + .ok_or_else(|| not_found(json_path.as_str()))?; + RadonTypes::try_from(item.to_owned())?.try_into() + }, + None => { + RadonTypes::try_from(json_input)?.try_into() + }, + _ => Err(wrong_args()) + } } fn add_children( @@ -78,6 +266,35 @@ fn add_children( } } +fn into_radon_types(value: &serde_json::Value) -> RadonTypes { + match value { + serde_json::Value::Number(value) => { + if value.is_f64() { + RadonTypes::from(RadonFloat::from(value.as_f64().unwrap_or_default())) + } else { + RadonTypes::from(RadonInteger::from(value.as_i64().unwrap_or_default() as i128)) + } + }, + serde_json::Value::Bool(value) => RadonTypes::from(RadonBoolean::from(*value)), + serde_json::Value::String(value) => RadonTypes::from(RadonString::from(value.clone())), + serde_json::Value::Object(entries) => { + let mut object: BTreeMap = BTreeMap::new(); + for (key, value) in entries { + object.insert(key.clone(), into_radon_types(value)); + } + RadonTypes::from(RadonMap::from(object)) + } + serde_json::Value::Array(values) => { + let items: Vec = values + .iter() + .map(into_radon_types) + .collect(); + RadonTypes::from(RadonArray::from(items)) + } + serde_json::Value::Null => RadonTypes::from(RadonString::default()), + } +} + fn parse_element_map(input: &minidom::Element, depth: u8) -> Result { if depth > MAX_DEPTH { return Err(RadError::XmlParseOverflow); @@ -142,6 +359,10 @@ pub fn parse_xml_map(input: &RadonString) -> Result { } } +pub fn length(input: &RadonString) -> RadonInteger { + RadonInteger::from(input.value().len() as i128) +} + pub fn radon_trim(input: &RadonString) -> String { if input.value().ends_with('\n') { input.value()[..input.value().len() - 1].to_string() @@ -150,84 +371,10 @@ pub fn radon_trim(input: &RadonString) -> String { } } -pub fn to_bool(input: &RadonString) -> Result { - let str_value = radon_trim(input); - bool::from_str(&str_value) - .map(RadonBoolean::from) - .map_err(Into::into) -} - -/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents -/// a valid floating point number. -pub fn as_float(input: &RadonString, args: &Option>) -> Result { - f64::from_str(&as_numeric_string( - input, - args.as_deref().unwrap_or_default(), - )) - .map(RadonFloat::from) - .map_err(Into::into) -} - -/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents -/// a valid integer number. -pub fn as_integer( - input: &RadonString, - args: &Option>, -) -> Result { - i128::from_str(&as_numeric_string( - input, - args.as_deref().unwrap_or_default(), - )) - .map(RadonInteger::from) - .map_err(Into::into) -} - -/// Converts a `RadonString` into a `String` containing a numeric value, provided that the input -/// string actually represents a valid number. -pub fn as_numeric_string(input: &RadonString, args: &[Value]) -> String { - let str_value = radon_trim(input); - let (thousands_separator, decimal_separator) = read_separators_from_args(args); - - replace_separators(str_value, thousands_separator, decimal_separator) -} - -pub fn length(input: &RadonString) -> RadonInteger { - RadonInteger::from(input.value().len() as i128) -} - -pub fn to_lowercase(input: &RadonString) -> RadonString { - RadonString::from(input.value().as_str().to_lowercase()) -} - -pub fn to_uppercase(input: &RadonString) -> RadonString { - RadonString::from(input.value().as_str().to_uppercase()) -} - -pub fn hash(input: &RadonString, args: &[Value]) -> Result { - let wrong_args = || RadError::WrongArguments { - input_type: RadonString::radon_type_name(), - operator: "Hash".to_string(), - args: args.to_vec(), - }; - - let input_string = input.value(); - let input_bytes = input_string.as_bytes(); - - let arg = args.first().ok_or_else(wrong_args)?.to_owned(); - let hash_function_integer = from_value::(arg).map_err(|_| wrong_args())?; - let hash_function_code = - RadonHashFunctions::try_from(hash_function_integer).map_err(|_| wrong_args())?; - - let digest = hash_functions::hash(input_bytes, hash_function_code)?; - let hex_string = hex::encode(digest); - - Ok(RadonString::from(hex_string)) -} - pub fn string_match(input: &RadonString, args: &[Value]) -> Result { let wrong_args = || RadError::WrongArguments { input_type: RadonString::radon_type_name(), - operator: "String match".to_string(), + operator: "StringMatch".to_string(), args: args.to_vec(), }; @@ -281,6 +428,66 @@ pub fn string_match(input: &RadonString, args: &[Value]) -> Result Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "StringReplace".to_string(), + args: args.to_vec(), + }; + let regex = RadonString::try_from(args.first().ok_or_else(wrong_args)?.to_owned())?; + let replacement = RadonString::try_from(args.get(1).ok_or_else(wrong_args)?.to_owned())?; + Ok(RadonString::from(input.value().as_str().replace( + regex.value().as_str(), + replacement.value().as_str(), + ))) +} + +pub fn string_slice(input: &RadonString, args: &[Value]) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "StringSlice".to_string(), + args: args.to_vec(), + }; + let mut end_index: usize = input.value().len(); + match args.len() { + 2 => { + let start_index = from_value::(args[0].clone()) + .unwrap_or_default() + .rem_euclid(end_index as i64) as usize; + end_index = from_value::(args[1].clone()) + .unwrap_or_default() + .rem_euclid(end_index as i64) as usize; + Ok(RadonString::from( + input.value().as_str().slice(start_index..end_index), + )) + } + 1 => { + let start_index = from_value::(args[0].clone()) + .unwrap_or_default() + .rem_euclid(end_index as i64) as usize; + Ok(RadonString::from( + input.value().as_str().slice(start_index..end_index), + )) + } + _ => Err(wrong_args()), + } +} + +pub fn string_split(input: &RadonString, args: &[Value]) -> Result { + let wrong_args = || RadError::WrongArguments { + input_type: RadonString::radon_type_name(), + operator: "StringSplit".to_string(), + args: args.to_vec(), + }; + let pattern = RadonString::try_from(args.first().ok_or_else(wrong_args)?.to_owned())?; + let parts: Vec = Regex::new(pattern.value().as_str()) + .unwrap() + .split(input.value().as_str()) + .map(|part| RadonTypes::from(RadonString::from(part))) + .collect(); + Ok(RadonArray::from(parts)) +} + /// Replace thousands and decimals separators in a `String`. #[inline] pub fn replace_separators( @@ -321,6 +528,23 @@ fn default_decimal_separator(_: T) -> String { String::from(DEFAULT_DECIMAL_SEPARATOR) } +pub fn to_lowercase(input: &RadonString) -> RadonString { + RadonString::from(input.value().as_str().to_lowercase()) +} + +/// Converts a `RadonString` into a `String` containing a numeric value, provided that the input +/// string actually represents a valid number. +pub fn to_numeric_string(input: &RadonString, args: &[Value]) -> String { + let str_value = radon_trim(input); + let (thousands_separator, decimal_separator) = read_separators_from_args(args); + + replace_separators(str_value, thousands_separator, decimal_separator) +} + +pub fn to_uppercase(input: &RadonString) -> RadonString { + RadonString::from(input.value().as_str().to_uppercase()) +} + /// This module was introduced for encapsulating the interim legacy logic before WIP-0024 is /// introduced, for the sake of maintainability. /// @@ -357,7 +581,7 @@ mod tests { #[test] fn test_parse_json_map() { let json_map = RadonString::from(r#"{ "Hello": "world" }"#); - let output = parse_json_map(&json_map).unwrap(); + let output = parse_json_map(&json_map, &None).unwrap(); let key = "Hello"; let value = RadonTypes::String(RadonString::from("world")); @@ -517,7 +741,7 @@ mod tests { fn test_parse_json_map_with_null_entries() { // When parsing a JSON map, any keys with value `null` are ignored let json_map = RadonString::from(r#"{ "Hello": "world", "Bye": null }"#); - let output = parse_json_map(&json_map).unwrap(); + let output = parse_json_map(&json_map, &None).unwrap(); let key = "Hello"; let value = RadonTypes::String(RadonString::from("world")); @@ -531,7 +755,7 @@ mod tests { #[test] fn test_parse_json_map_fail() { let invalid_json = RadonString::from(r#"{ "Hello": }"#); - let output = parse_json_map(&invalid_json).unwrap_err(); + let output = parse_json_map(&invalid_json, &None).unwrap_err(); let expected_err = RadError::JsonParse { description: "expected value at line 1 column 13".to_string(), @@ -539,7 +763,7 @@ mod tests { assert_eq!(output, expected_err); let json_array = RadonString::from(r#"[1,2,3]"#); - let output = parse_json_map(&json_array).unwrap_err(); + let output = parse_json_map(&json_array, &None).unwrap_err(); let expected_err = RadError::Decode { from: "cbor::value::Value", to: RadonMap::radon_type_name(), @@ -550,7 +774,7 @@ mod tests { #[test] fn test_parse_json_array() { let json_array = RadonString::from(r#"[1,2,3]"#); - let output = parse_json_array(&json_array).unwrap(); + let output = parse_json_array(&json_array, &None).unwrap(); let expected_output = RadonArray::from(vec![ RadonTypes::Integer(RadonInteger::from(1)), @@ -565,7 +789,7 @@ mod tests { fn test_parse_json_array_with_null_entries() { // When parsing a JSON array, any elements with value `null` are ignored let json_array = RadonString::from(r#"[null, 1, null, null, 2, 3, null]"#); - let output = parse_json_array(&json_array).unwrap(); + let output = parse_json_array(&json_array, &None).unwrap(); let expected_output = RadonArray::from(vec![ RadonTypes::Integer(RadonInteger::from(1)), @@ -579,7 +803,7 @@ mod tests { #[test] fn test_parse_json_array_fail() { let invalid_json = RadonString::from(r#"{ "Hello": }"#); - let output = parse_json_array(&invalid_json).unwrap_err(); + let output = parse_json_array(&invalid_json, &None).unwrap_err(); let expected_err = RadError::JsonParse { description: "expected value at line 1 column 13".to_string(), @@ -587,7 +811,7 @@ mod tests { assert_eq!(output, expected_err); let json_map = RadonString::from(r#"{ "Hello": "world" }"#); - let output = parse_json_array(&json_map).unwrap_err(); + let output = parse_json_array(&json_map, &None).unwrap_err(); let expected_err = RadError::Decode { from: "cbor::value::Value", to: RadonArray::radon_type_name(), @@ -731,7 +955,7 @@ mod tests { let rad_float = RadonBoolean::from(false); let rad_string: RadonString = RadonString::from("false"); - assert_eq!(to_bool(&rad_string).unwrap(), rad_float); + assert_eq!(as_bool(&rad_string).unwrap(), rad_float); } #[test] @@ -1117,7 +1341,7 @@ mod tests { let result = string_match(&input_key, &args); assert_eq!( result.unwrap_err().to_string(), - "Wrong `RadonString::String match()` arguments: `[Map({Text(\"key1\"): Float(1.0), Text(\"key2\"): Float(2.0)})]`" + "Wrong `RadonString::StringMatch()` arguments: `[Map({Text(\"key1\"): Float(1.0), Text(\"key2\"): Float(2.0)})]`" ); } diff --git a/rad/src/types/bytes.rs b/rad/src/types/bytes.rs index 3a894564d..5d30db05f 100644 --- a/rad/src/types/bytes.rs +++ b/rad/src/types/bytes.rs @@ -4,6 +4,8 @@ use crate::{ script::RadonCall, types::{RadonType, RadonTypes}, }; +use num_enum::TryFromPrimitive; +use serde::Serialize; use serde_cbor::value::Value; use std::{ convert::{TryFrom, TryInto}, @@ -13,6 +15,17 @@ use witnet_data_structures::radon_report::ReportContext; const RADON_BYTES_TYPE_NAME: &str = "RadonBytes"; +/// List of support string-encoding algorithms for buffers +#[derive(Debug, Default, PartialEq, Eq, Serialize, TryFromPrimitive)] +#[repr(u8)] +pub enum RadonBytesEncoding { + #[default] + Hex = 0x00, + Base58 = 0x10, + Base64 = 0x11, + Utf8 = 0x80, +} + #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct RadonBytes { value: Vec, @@ -84,12 +97,19 @@ impl Operable for RadonBytes { match call { // Identity (RadonOpCodes::Identity, None) => identity(RadonTypes::from(self.clone())), - (RadonOpCodes::BytesAsString, None) => { - bytes_operators::to_string(self).map(RadonTypes::from) + (RadonOpCodes::BytesAsInteger, None) => bytes_operators::as_integer(self) + .map(RadonTypes::from), + (RadonOpCodes::BytesLength, None) => { + Ok(RadonTypes::from(bytes_operators::length(self))) } (RadonOpCodes::BytesHash, Some(args)) => { bytes_operators::hash(self, args.as_slice()).map(RadonTypes::from) } + (RadonOpCodes::BytesSlice, Some(args)) => bytes_operators::slice(self, args.as_slice()) + .map(RadonTypes::from), + (RadonOpCodes::BytesToString, args) => bytes_operators::to_string(self, args) + .map(RadonTypes::from), + // Unsupported / unimplemented (op_code, args) => Err(RadError::UnsupportedOperator { input_type: RADON_BYTES_TYPE_NAME.to_string(), diff --git a/rad/src/types/string.rs b/rad/src/types/string.rs index 03531dc5f..df658ba9f 100644 --- a/rad/src/types/string.rs +++ b/rad/src/types/string.rs @@ -118,29 +118,41 @@ impl Operable for RadonString { } .map(RadonTypes::from), (RadonOpCodes::StringAsBoolean, None) => { - string_operators::to_bool(self).map(RadonTypes::from) + string_operators::as_bool(self).map(RadonTypes::from) } - (RadonOpCodes::StringParseJSONArray, None) => { - string_operators::parse_json_array(self).map(RadonTypes::from) - } - (RadonOpCodes::StringParseJSONMap, None) => { - string_operators::parse_json_map(self).map(RadonTypes::from) + (RadonOpCodes::StringAsBytes, args) => string_operators::as_bytes(self, args) + .map(RadonTypes::from), + (RadonOpCodes::StringLength, None) => { + Ok(RadonTypes::from(string_operators::length(self))) } (RadonOpCodes::StringMatch, Some(args)) => { string_operators::string_match(self, args.as_slice()) } - (RadonOpCodes::StringLength, None) => { - Ok(RadonTypes::from(string_operators::length(self))) + (RadonOpCodes::StringParseJSONArray, args) => { + string_operators::parse_json_array(self, args).map(RadonTypes::from) + } + (RadonOpCodes::StringParseJSONMap, args) => { + string_operators::parse_json_map(self, args).map(RadonTypes::from) } + (RadonOpCodes::StringParseXMLMap, None) => { + string_operators::parse_xml_map(self).map(RadonTypes::from) + } + (RadonOpCodes::StringReplace, Some(args)) => { + string_operators::string_replace(self, args.as_slice()) + .map(RadonTypes::from)} + (RadonOpCodes::StringSlice, Some(args)) => { + string_operators::string_slice(self, args.as_slice()) + .map(RadonTypes::from)} + (RadonOpCodes::StringSplit, Some(args)) => { + string_operators::string_split(self, args.as_slice()) + .map(RadonTypes::from)} (RadonOpCodes::StringToLowerCase, None) => { Ok(RadonTypes::from(string_operators::to_lowercase(self))) } (RadonOpCodes::StringToUpperCase, None) => { Ok(RadonTypes::from(string_operators::to_uppercase(self))) } - (RadonOpCodes::StringParseXMLMap, None) => { - string_operators::parse_xml_map(self).map(RadonTypes::from) - } + (op_code, args) => Err(RadError::UnsupportedOperator { input_type: RADON_STRING_TYPE_NAME.to_string(), operator: op_code.to_string(),