diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e89aa93..43efe92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ Script-breaking changes ----------------------- * For consistency with the `import` statement, the `export` statement no longer exports multiple variables. +* Appending a BLOB to a string (via `+`, `+=`, `append` or string interpolation) now treats the BLOB as a UTF-8 encoded string. +* Appending a string/character to a BLOB (via `+=` or `append`) now adds the string/character as a UTF-8 encoded byte stream. New features ------------ @@ -52,6 +54,8 @@ Enhancements * `StmtBlock` and `Stmt::Block` now keep the position of the closing `}` as well. * `EvalAltResult::unwrap_inner` is added to access the base error inside multiple layers of wrappings (e.g. `EvalAltResult::ErrorInFunction`). * Yet another new syntax is introduced for `def_package!` that further simplifies the old syntax. +* A new method `to_blob` is added to convert a string into a BLOB as UTF-8 encoded bytes. +* A new method `to_array` is added to convert a BLOB into array of integers. REPL tool changes ----------------- diff --git a/src/func/builtin.rs b/src/func/builtin.rs index f2284fbd..1da1d250 100644 --- a/src/func/builtin.rs +++ b/src/func/builtin.rs @@ -682,19 +682,77 @@ pub fn get_builtin_op_assignment_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Optio } } - // blob op= int #[cfg(not(feature = "no_index"))] - if types_pair == (TypeId::of::(), TypeId::of::()) { - use crate::Blob; + { + // string op= blob + if types_pair == (TypeId::of::(), TypeId::of::()) { + return match op { + "+=" => Some(|_, args| { + let buf = { + let x = args[1].read_lock::().expect(BUILTIN); + if x.is_empty() { + return Ok(Dynamic::UNIT); + } + let s = args[0].read_lock::().expect(BUILTIN); + let mut buf = crate::SmartString::from(s.as_str()); + buf.push_str(&String::from_utf8_lossy(&x)); + buf + }; + let mut s = args[0].write_lock::().expect(BUILTIN); + *s = buf.into(); + Ok(Dynamic::UNIT) + }), + _ => None, + }; + } + // blob op= int + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; - return match op { - "+=" => Some(|_, args| { - let x = (args[1].as_int().expect("`INT`") & 0x000000ff) as u8; - let mut blob = args[0].write_lock::().expect(BUILTIN); - Ok(blob.push(x).into()) - }), - _ => None, - }; + return match op { + "+=" => Some(|_, args| { + let x = (args[1].as_int().expect("`INT`") & 0x000000ff) as u8; + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.push(x).into()) + }), + _ => None, + }; + } + + // blob op= char + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; + + return match op { + "+=" => Some(|_, args| { + let mut buf = [0_u8; 4]; + let x = args[1].as_char().expect("`char`").encode_utf8(&mut buf); + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.extend(x.as_bytes()).into()) + }), + _ => None, + }; + } + + // blob op= string + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; + + return match op { + "+=" => Some(|_, args| { + let s: crate::Blob = { + let s = args[1].read_lock::().expect(BUILTIN); + if s.is_empty() { + return Ok(Dynamic::UNIT); + } + s.as_bytes().into() + }; + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.extend(s).into()) + }), + _ => None, + }; + } } // No built-in op-assignments for different types. diff --git a/src/packages/blob_basic.rs b/src/packages/blob_basic.rs index 5f27bb8d..611d0b83 100644 --- a/src/packages/blob_basic.rs +++ b/src/packages/blob_basic.rs @@ -3,7 +3,7 @@ use crate::eval::{calc_index, calc_offset_len}; use crate::plugin::*; use crate::{ - def_package, Blob, Dynamic, ExclusiveRange, InclusiveRange, NativeCallContext, Position, + def_package, Array, Blob, Dynamic, ExclusiveRange, InclusiveRange, NativeCallContext, Position, RhaiResultOf, INT, }; #[cfg(feature = "no_std")] @@ -94,12 +94,36 @@ pub mod blob_functions { blob.resize(len, (value & 0x000000ff) as u8); Ok(blob) } + /// Convert the BLOB into an array of integers. + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// let x = b.to_array(); + /// + /// print(x); // prints "[66, 66, 66, 66, 66]" + /// ``` + #[rhai_fn(pure)] + pub fn to_array(blob: &mut Blob) -> Array { + blob.iter().map(|&ch| (ch as INT).into()).collect() + } /// Return the length of the BLOB. + /// + /// # Example + /// + /// ```rhai + /// let b = blob(10, 0x42); + /// + /// print(b); // prints "[4242424242424242 4242]" + /// + /// print(b.len()); // prints 10 + /// ``` #[rhai_fn(name = "len", get = "len", pure)] pub fn len(blob: &mut Blob) -> INT { blob.len() as INT } - /// Get the byte value at the `index` position in the BLOB. /// /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element). @@ -196,15 +220,49 @@ pub mod blob_functions { /// /// print(b1); // prints "[4242424242111111]" /// ``` - pub fn append(blob: &mut Blob, y: Blob) { - if !y.is_empty() { - if blob.is_empty() { - *blob = y; + pub fn append(blob1: &mut Blob, blob2: Blob) { + if !blob2.is_empty() { + if blob1.is_empty() { + *blob1 = blob2; } else { - blob.extend(y); + blob1.extend(blob2); } } } + /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// b.append("hello"); + /// + /// print(b); // prints "[424242424268656c 6c6f]" + /// ``` + #[rhai_fn(name = "+=", name = "append")] + pub fn append_str(blob: &mut Blob, string: ImmutableString) { + if !string.is_empty() { + blob.extend(string.as_bytes()); + } + } + /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// b.append('!'); + /// + /// print(b); // prints "[424242424221]" + /// ``` + #[rhai_fn(name = "+=", name = "append")] + pub fn append_char(blob: &mut Blob, character: char) { + let mut buf = [0_u8; 4]; + let x = character.encode_utf8(&mut buf); + blob.extend(x.as_bytes()); + } /// Add another BLOB to the end of the BLOB, returning it as a new BLOB. /// /// # Example diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 90fc5fb9..b8c3f4c6 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -6,6 +6,9 @@ use std::{any::TypeId, mem}; use super::string_basic::{print_with_func, FUNC_TO_STRING}; +#[cfg(not(feature = "no_index"))] +use crate::Blob; + def_package! { /// Package of additional string utilities over [`BasicStringPackage`][super::BasicStringPackage] pub MoreStringPackage(lib) { @@ -19,7 +22,7 @@ def_package! { mod string_functions { use crate::{ImmutableString, SmartString}; - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append( ctx: NativeCallContext, string: ImmutableString, @@ -33,6 +36,14 @@ mod string_functions { format!("{}{}", string, s).into() } } + #[rhai_fn(name = "+=", name = "append")] + pub fn add(ctx: NativeCallContext, string: &mut ImmutableString, mut item: Dynamic) { + let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item); + + if !s.is_empty() { + *string = format!("{}{}", string, s).into(); + } + } #[rhai_fn(name = "+", pure)] pub fn add_prepend( ctx: NativeCallContext, @@ -48,11 +59,13 @@ mod string_functions { s } - #[rhai_fn(name = "+", name = "append")] + // The following are needed in order to override the generic versions with `Dynamic` parameters. + + #[rhai_fn(name = "+")] pub fn add_append_str(string1: ImmutableString, string2: ImmutableString) -> ImmutableString { string1 + string2 } - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append_char(string: ImmutableString, character: char) -> ImmutableString { string + character } @@ -61,7 +74,7 @@ mod string_functions { format!("{}{}", character, string).into() } - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append_unit(string: ImmutableString, item: ()) -> ImmutableString { let _item = item; string @@ -71,6 +84,30 @@ mod string_functions { string } + #[cfg(not(feature = "no_index"))] + pub mod blob_functions { + #[rhai_fn(name = "+")] + pub fn add_append_blob(string: ImmutableString, utf8: Blob) -> ImmutableString { + if utf8.is_empty() { + string + } else if string.is_empty() { + String::from_utf8_lossy(&utf8).into_owned().into() + } else { + let mut s = crate::SmartString::from(string); + s.push_str(&String::from_utf8_lossy(&utf8)); + s.into() + } + } + #[rhai_fn(name = "append")] + pub fn add_blob(string: &mut ImmutableString, utf8: Blob) { + let mut s = crate::SmartString::from(string.as_str()); + if !utf8.is_empty() { + s.push_str(&String::from_utf8_lossy(&utf8)); + *string = s.into(); + } + } + } + /// Return the length of the string, in number of characters. /// /// # Example @@ -105,6 +142,25 @@ mod string_functions { string.len() as INT } } + /// Convert the string into an UTF-8 encoded byte-stream as a BLOB. + /// + /// # Example + /// + /// ```rhai + /// let text = "朝には紅顔ありて夕べには白骨となる"; + /// + /// let bytes = text.to_blob(); + /// + /// print(bytes.len()); // prints 51 + /// ``` + #[cfg(not(feature = "no_index"))] + pub fn to_blob(string: &str) -> crate::Blob { + if string.is_empty() { + crate::Blob::new() + } else { + string.as_bytes().into() + } + } /// Remove all occurrences of a sub-string from the string. /// /// # Example