diff --git a/Cargo.lock b/Cargo.lock index e815e220b..c76afade9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6683,6 +6683,7 @@ name = "witnet_rad" version = "0.3.2" dependencies = [ "anyhow", + "base64 0.22.1", "cbor-codec", "futures 0.3.31", "hex", diff --git a/rad/Cargo.toml b/rad/Cargo.toml index 1caca822d..bbaa0e442 100644 --- a/rad/Cargo.toml +++ b/rad/Cargo.toml @@ -14,6 +14,7 @@ tokio = "1.44.1" [dependencies] anyhow = "1.0.98" +base64 = "0.22.1" cbor-codec = { git = "https://github.com/witnet/cbor-codec.git", branch = "feat/ldexpf-shim" } futures = "0.3.31" hex = "0.4.1" diff --git a/rad/src/lib.rs b/rad/src/lib.rs index ca91167c4..7ccf2e205 100644 --- a/rad/src/lib.rs +++ b/rad/src/lib.rs @@ -267,6 +267,9 @@ async fn http_response( })? }; + // Start timer for measuring resolution time + let start_ts = std::time::SystemTime::now(); + // Use the provided HTTP client, or instantiate a new one if none let client = match client { Some(client) => client, @@ -373,7 +376,20 @@ async fn http_response( message: x.to_string(), })?; - let result = run_retrieval_with_data_report(retrieve, &response_string, context, settings); + let result = run_retrieval_with_data_report(retrieve, &response_string, context, settings) + // override running_time within the final report including + .map(|report| { + let completion_ts = std::time::SystemTime::now(); + RadonReport { + context: ReportContext { + start_time: Some(start_ts), + completion_time: Some(completion_ts), + ..report.context + }, + running_time: completion_ts.duration_since(start_ts).unwrap_or_default(), + ..report + } + }); match &result { Ok(report) => { diff --git a/rad/src/operators/bytes.rs b/rad/src/operators/bytes.rs index f8b1ecb32..ca1074486 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,61 @@ 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::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 +106,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..907081ce8 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, diff --git a/rad/src/types/bytes.rs b/rad/src/types/bytes.rs index 3a894564d..e2985161e 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,16 @@ 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 = 0, + Base64 = 1, + Utf8 = 2, +} + #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct RadonBytes { value: Vec, @@ -84,12 +96,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(),