What changed, and why it matters
This commit simply moves a set of helper functions for handling strings from one Rust module to another. The actual code is copied unchanged, and all existing callers are updated to use the new location. There is no security fix or behavior change.
No security action needed; this is a routine refactoring commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates string utility functions (str_from_null_terminated, str_to_cstr_vec, truncate_str, etc.) from bitbox02::util to a new util::strings module in the util crate. The implementation is identical to the deleted file, only import paths are updated. A dependency on the zeroize crate is added to the util crate because str_to_cstr_vec_zeroizing uses it.
Changed components
src/rust/bitbox02/src/util.rs (deleted)src/rust/util/src/strings.rs (new)src/rust/util/Cargo.tomlsrc/rust/Cargo.lockInspect captured patch +202 / −194
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index c873737..5b8c489 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -1073,6 +1073,7 @@ dependencies = [
"p256",
"rtt-target",
"sha2",
+ "zeroize",
]
[[package]]
diff --git a/src/rust/bitbox02-rust/src/bip39.rs b/src/rust/bitbox02-rust/src/bip39.rs
index fd53ddd..59851bb 100644
--- a/src/rust/bitbox02-rust/src/bip39.rs
+++ b/src/rust/bitbox02-rust/src/bip39.rs
@@ -91,7 +91,7 @@ mod tests {
util::bytes::rust_util_bytes_mut(word.as_mut_ptr(), 8)
}));
assert_eq!(
- bitbox02::util::str_from_null_terminated(&word).unwrap(),
+ util::strings::str_from_null_terminated(&word).unwrap(),
"abandon"
);
let mut word = [1u8; 10];
@@ -99,7 +99,7 @@ mod tests {
util::bytes::rust_util_bytes_mut(word.as_mut_ptr(), word.len())
}));
assert_eq!(
- bitbox02::util::str_from_null_terminated(&word).unwrap(),
+ util::strings::str_from_null_terminated(&word).unwrap(),
"zoo"
);
let mut word = [1u8; 10];
@@ -107,7 +107,7 @@ mod tests {
util::bytes::rust_util_bytes_mut(word.as_mut_ptr(), word.len())
}));
assert_eq!(
- bitbox02::util::str_from_null_terminated(&word).unwrap(),
+ util::strings::str_from_null_terminated(&word).unwrap(),
"edit"
);
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/registration.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/registration.rs
index a0970d7..7958adb 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/registration.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/registration.rs
@@ -84,7 +84,7 @@ async fn get_name(
// We truncate the user input string to fit into the maximum allowed multisig
// account name length. This is not very nice, but it has to do until we have some
// sort of indication in the input component.
- bitbox02::util::truncate_str(name.as_str(), bitbox02::memory::MULTISIG_NAME_MAX_LEN).into()
+ util::strings::truncate_str(name.as_str(), bitbox02::memory::MULTISIG_NAME_MAX_LEN).into()
} else {
request.name.clone()
};
diff --git a/src/rust/bitbox02-rust/src/salt.rs b/src/rust/bitbox02-rust/src/salt.rs
index 0d290e1..95b70f5 100644
--- a/src/rust/bitbox02-rust/src/salt.rs
+++ b/src/rust/bitbox02-rust/src/salt.rs
@@ -37,7 +37,7 @@ pub unsafe extern "C" fn rust_salt_hash_data(
purpose: *const c_char,
mut hash_out: BytesMut,
) -> bool {
- let purpose_str = match unsafe { bitbox02::util::str_from_null_terminated_ptr(purpose) } {
+ let purpose_str = match unsafe { util::strings::str_from_null_terminated_ptr(purpose) } {
Ok(purpose) => purpose,
Err(()) => return false,
};
diff --git a/src/rust/bitbox02/src/lib.rs b/src/rust/bitbox02/src/lib.rs
index 9f27ba6..b21e134 100644
--- a/src/rust/bitbox02/src/lib.rs
+++ b/src/rust/bitbox02/src/lib.rs
@@ -48,15 +48,15 @@ use core::time::Duration;
pub use bitbox02_sys::buffer_t;
-#[macro_use]
-pub mod util;
-
pub fn ug_put_string(x: i16, y: i16, input: &str, inverted: bool) {
unsafe {
bitbox02_sys::UG_PutString(
x,
y,
- crate::util::str_to_cstr_vec(input).unwrap().as_ptr().cast(),
+ util::strings::str_to_cstr_vec(input)
+ .unwrap()
+ .as_ptr()
+ .cast(),
inverted,
);
}
@@ -106,7 +106,7 @@ pub fn delay(duration: Duration) {
pub fn screen_print_debug(msg: &str, duration: i32) {
unsafe {
bitbox02_sys::screen_print_debug(
- crate::util::str_to_cstr_vec(msg).unwrap().as_ptr().cast(),
+ util::strings::str_to_cstr_vec(msg).unwrap().as_ptr().cast(),
duration,
)
}
@@ -234,15 +234,20 @@ pub fn reboot_to_bootloader() -> ! {
#[cfg(any(feature = "testing", feature = "c-unit-testing"))]
pub fn print_stdout(msg: &str) {
unsafe {
- bitbox02_sys::printf(crate::util::str_to_cstr_vec(msg).unwrap().as_ptr().cast());
+ bitbox02_sys::printf(util::strings::str_to_cstr_vec(msg).unwrap().as_ptr().cast());
}
}
#[cfg(any(feature = "testing", feature = "c-unit-testing"))]
pub fn println_stdout(msg: &str) {
unsafe {
- bitbox02_sys::printf(crate::util::str_to_cstr_vec(msg).unwrap().as_ptr().cast());
- bitbox02_sys::printf(crate::util::str_to_cstr_vec("\n").unwrap().as_ptr().cast());
+ bitbox02_sys::printf(util::strings::str_to_cstr_vec(msg).unwrap().as_ptr().cast());
+ bitbox02_sys::printf(
+ util::strings::str_to_cstr_vec("\n")
+ .unwrap()
+ .as_ptr()
+ .cast(),
+ );
}
}
diff --git a/src/rust/bitbox02/src/memory.rs b/src/rust/bitbox02/src/memory.rs
index 3242e8a..9ef7be6 100644
--- a/src/rust/bitbox02/src/memory.rs
+++ b/src/rust/bitbox02/src/memory.rs
@@ -22,7 +22,7 @@ pub struct Error;
pub fn get_device_name() -> String {
let mut name = [0u8; DEVICE_NAME_MAX_LEN + 1];
unsafe { bitbox02_sys::memory_get_device_name(name.as_mut_ptr().cast()) }
- crate::util::str_from_null_terminated(&name[..])
+ util::strings::str_from_null_terminated(&name[..])
.unwrap()
.into()
}
@@ -30,7 +30,7 @@ pub fn get_device_name() -> String {
pub fn set_device_name(name: &str) -> Result<(), Error> {
match unsafe {
bitbox02_sys::memory_set_device_name(
- crate::util::str_to_cstr_vec(name)
+ util::strings::str_to_cstr_vec(name)
.or(Err(Error))?
.as_ptr()
.cast(),
@@ -194,7 +194,7 @@ pub fn multisig_set_by_hash(hash: &[u8], name: &str) -> Result<(), MemoryError>
match unsafe {
bitbox02_sys::memory_multisig_set_by_hash(
hash.as_ptr(),
- crate::util::str_to_cstr_vec(name)
+ util::strings::str_to_cstr_vec(name)
.or(Err(MemoryError::MEMORY_ERR_INVALID_INPUT))?
.as_ptr()
.cast(),
@@ -211,7 +211,7 @@ pub fn multisig_get_by_hash(hash: &[u8]) -> Option<String> {
bitbox02_sys::memory_multisig_get_by_hash(hash.as_ptr(), name.as_mut_ptr().cast())
} {
true => Some(
- crate::util::str_from_null_terminated(&name[..])
+ util::strings::str_from_null_terminated(&name[..])
.unwrap()
.into(),
),
diff --git a/src/rust/bitbox02/src/sd.rs b/src/rust/bitbox02/src/sd.rs
index 3bca63f..8b4afd6 100644
--- a/src/rust/bitbox02/src/sd.rs
+++ b/src/rust/bitbox02/src/sd.rs
@@ -4,8 +4,8 @@ extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
-use crate::util::str_to_cstr_vec;
use bitbox02_sys::SD_MAX_FILE_SIZE;
+use util::strings::str_to_cstr_vec;
#[cfg(any(feature = "testing", feature = "simulator-graphical"))]
pub fn format() -> bool {
@@ -42,7 +42,7 @@ pub fn list_subdir(subdir: Option<&str>) -> Result<Vec<String>, ()> {
true => (0..list.0.num_files)
.map(|i| unsafe {
let ptr = *list.0.files.add(i);
- crate::util::str_from_null_terminated_ptr(ptr).map(String::from)
+ util::strings::str_from_null_terminated_ptr(ptr).map(String::from)
})
.collect(),
false => Err(()),
diff --git a/src/rust/bitbox02/src/securechip.rs b/src/rust/bitbox02/src/securechip.rs
index a4f0ed5..f8d025d 100644
--- a/src/rust/bitbox02/src/securechip.rs
+++ b/src/rust/bitbox02/src/securechip.rs
@@ -87,7 +87,7 @@ pub fn init_new_password(
password: &str,
password_stretch_algo: PasswordStretchAlgo,
) -> Result<Zeroizing<Vec<u8>>, Error> {
- let password = crate::util::str_to_cstr_vec_zeroizing(password)
+ let password = util::strings::str_to_cstr_vec_zeroizing(password)
.map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_INVALID_ARGS))?;
let mut stretched = Zeroizing::new(vec![0u8; 32]);
let status = unsafe {
@@ -108,7 +108,7 @@ pub fn stretch_password(
password: &str,
password_stretch_algo: PasswordStretchAlgo,
) -> Result<Zeroizing<Vec<u8>>, Error> {
- let password = crate::util::str_to_cstr_vec_zeroizing(password)
+ let password = util::strings::str_to_cstr_vec_zeroizing(password)
.map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_INVALID_ARGS))?;
let mut stretched = Zeroizing::new(vec![0u8; 32]);
let status = unsafe {
diff --git a/src/rust/bitbox02/src/ui/types.rs b/src/rust/bitbox02/src/ui/types.rs
index 9cceff8..a2f8c2d 100644
--- a/src/rust/bitbox02/src/ui/types.rs
+++ b/src/rust/bitbox02/src/ui/types.rs
@@ -65,10 +65,10 @@ impl<'a> ConfirmParams<'a> {
// truncate and append '...'.
const TRUNCATE_SIZE: usize = MAX_LABEL_SIZE + 1;
*title_scatch =
- crate::util::str_to_cstr_vec(crate::util::truncate_str(self.title, TRUNCATE_SIZE))
+ util::strings::str_to_cstr_vec(util::strings::truncate_str(self.title, TRUNCATE_SIZE))
.unwrap();
*body_scratch =
- crate::util::str_to_cstr_vec(crate::util::truncate_str(self.body, TRUNCATE_SIZE))
+ util::strings::str_to_cstr_vec(util::strings::truncate_str(self.body, TRUNCATE_SIZE))
.unwrap();
Survive::new(bitbox02_sys::confirm_params_t {
title: title_scatch.as_ptr().cast(),
@@ -109,7 +109,7 @@ impl<'a> TrinaryInputStringParams<'a> {
const TRUNCATE_SIZE: usize = MAX_LABEL_SIZE + 1;
*title_scratch =
- crate::util::str_to_cstr_vec(crate::util::truncate_str(self.title, TRUNCATE_SIZE))
+ util::strings::str_to_cstr_vec(util::strings::truncate_str(self.title, TRUNCATE_SIZE))
.unwrap();
Survive::new(bitbox02_sys::trinary_input_string_params_t {
diff --git a/src/rust/bitbox02/src/ui/ui.rs b/src/rust/bitbox02/src/ui/ui.rs
index 00571c3..3d5c8d0 100644
--- a/src/rust/bitbox02/src/ui/ui.rs
+++ b/src/rust/bitbox02/src/ui/ui.rs
@@ -65,7 +65,7 @@ where
F2: FnMut(zeroize::Zeroizing<String>),
{
let pw: zeroize::Zeroizing<String> = zeroize::Zeroizing::new(
- unsafe { crate::util::str_from_null_terminated_ptr(password) }
+ unsafe { util::strings::str_from_null_terminated_ptr(password) }
.unwrap()
.into(),
);
@@ -156,7 +156,7 @@ pub fn screen_process() {
pub fn status_create<'a>(text: &str, status_success: bool) -> Component<'a> {
let component = unsafe {
bitbox02_sys::status_create(
- crate::util::str_to_cstr_vec(text).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(text).unwrap().as_ptr(), // copied in C
status_success,
)
};
@@ -216,7 +216,7 @@ pub fn menu_create(params: MenuParams<'_>) -> Component<'_> {
let words: Vec<Vec<core::ffi::c_char>> = params
.words
.iter()
- .map(|word| crate::util::str_to_cstr_vec(word).unwrap())
+ .map(|word| util::strings::str_to_cstr_vec(word).unwrap())
.collect();
// Step two: collect pointers. This var also has to be valid until menu_create() finishes, or
// the pointer will be invalid.
@@ -248,7 +248,7 @@ pub fn menu_create(params: MenuParams<'_>) -> Component<'_> {
};
let title = params
.title
- .map(|title| crate::util::str_to_cstr_vec(title).unwrap());
+ .map(|title| util::strings::str_to_cstr_vec(title).unwrap());
let component = unsafe {
bitbox02_sys::menu_create(
c_words.as_ptr(),
@@ -301,13 +301,13 @@ pub fn trinary_choice_create<'a>(
let chosen_user_data = Box::into_raw(Box::new(chosen_callback)) as *mut c_void;
- let label_left = label_left.map(|label| crate::util::str_to_cstr_vec(label).unwrap());
- let label_middle = label_middle.map(|label| crate::util::str_to_cstr_vec(label).unwrap());
- let label_right = label_right.map(|label| crate::util::str_to_cstr_vec(label).unwrap());
+ let label_left = label_left.map(|label| util::strings::str_to_cstr_vec(label).unwrap());
+ let label_middle = label_middle.map(|label| util::strings::str_to_cstr_vec(label).unwrap());
+ let label_right = label_right.map(|label| util::strings::str_to_cstr_vec(label).unwrap());
let component = unsafe {
bitbox02_sys::trinary_choice_create(
- crate::util::str_to_cstr_vec(message).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(message).unwrap().as_ptr(), // copied in C
// copied in C
label_left
.as_ref()
@@ -349,8 +349,8 @@ pub fn confirm_transaction_address_create<'a, 'b>(
let user_data = Box::into_raw(Box::new(callback)) as *mut c_void;
let component = unsafe {
bitbox02_sys::confirm_transaction_address_create(
- crate::util::str_to_cstr_vec(amount).unwrap().as_ptr(), // copied in C
- crate::util::str_to_cstr_vec(address).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(amount).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(address).unwrap().as_ptr(), // copied in C
Some(c_callback as _),
user_data,
)
@@ -380,8 +380,8 @@ pub fn confirm_transaction_fee_create<'a, 'b>(
let user_data = Box::into_raw(Box::new(callback)) as *mut c_void;
let component = unsafe {
bitbox02_sys::confirm_transaction_fee_create(
- crate::util::str_to_cstr_vec(amount).unwrap().as_ptr(), // copied in C
- crate::util::str_to_cstr_vec(fee).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(amount).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(fee).unwrap().as_ptr(), // copied in C
longtouch,
Some(c_callback as _),
user_data,
@@ -402,7 +402,7 @@ pub fn trinary_input_string_set_input(component: &mut Component, word: &str) {
unsafe {
bitbox02_sys::trinary_input_string_set_input(
component.component,
- crate::util::str_to_cstr_vec(word).unwrap().as_ptr(),
+ util::strings::str_to_cstr_vec(word).unwrap().as_ptr(),
)
}
}
@@ -416,7 +416,7 @@ pub fn screen_stack_pop_all() {
pub fn progress_create<'a>(title: &str) -> Component<'a> {
let component = unsafe {
bitbox02_sys::progress_create(
- crate::util::str_to_cstr_vec(title).unwrap().as_ptr(), // copied in C
+ util::strings::str_to_cstr_vec(title).unwrap().as_ptr(), // copied in C
)
};
diff --git a/src/rust/bitbox02/src/util.rs b/src/rust/bitbox02/src/util.rs
deleted file mode 100644
index 6a04e01..0000000
--- a/src/rust/bitbox02/src/util.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-extern crate alloc;
-use alloc::vec::Vec;
-
-/// Parses a utf-8 string out of a null terminated buffer. Returns `Err(())` if there
-/// is no null terminator or if the bytes before the null terminator is invalid UTF8.
-pub fn str_from_null_terminated(input: &[u8]) -> Result<&str, ()> {
- core::ffi::CStr::from_bytes_until_nul(input)
- .or(Err(()))?
- .to_str()
- .or(Err(()))
-}
-
-/// Parses a utf-8 string out of a null terminated buffer starting at `ptr`. Returns `Err(())` if
-/// the bytes before the null terminator is invalid UTF8.
-///
-/// # Safety `ptr` must be not null and be a null terminated string. The resulting string is only
-/// valid as long the memory pointed to by `ptr` is valid.
-pub unsafe fn str_from_null_terminated_ptr<'a>(
- ptr: *const core::ffi::c_char,
-) -> Result<&'a str, ()> {
- unsafe { core::ffi::CStr::from_ptr(ptr.cast()).to_str().or(Err(())) }
-}
-
-/// truncate_str truncates string `s` to `len` chars. If `s` is
-/// shorter than `len`, the string is returned unchanged (no panics).
-pub fn truncate_str(s: &str, len: usize) -> &str {
- if s.len() > len { &s[..len] } else { s }
-}
-
-/// Converts a Rust string to a null terminated C string by appending a null
-/// terminator. Returns `Err(())` if the input already contains a null byte.
-pub fn str_to_cstr_vec(input: &str) -> Result<Vec<core::ffi::c_char>, ()> {
- let cstr = alloc::ffi::CString::new(input)
- .or(Err(()))?
- .into_bytes_with_nul();
- // into_bytes_with_nul always returns Vec<u8> independent of platform. Let's cast it to c_char
- // which is platform specific (unsigned on some platforms, signed on others).
- // Implemented without unsafe on purpose.
- Ok(cstr.into_iter().map(|c| c as _).collect())
-}
-
-/// Converts a Rust string to a null terminated C string by appending a null
-/// terminator. Returns `Err(())` if the input already contains a null byte.
-pub fn str_to_cstr_vec_zeroizing(
- input: &str,
-) -> Result<zeroize::Zeroizing<Vec<core::ffi::c_char>>, ()> {
- let bytes = input.as_bytes();
- if bytes.contains(&0) {
- return Err(());
- }
- let mut result: zeroize::Zeroizing<Vec<core::ffi::c_char>> =
- zeroize::Zeroizing::new(vec![0; bytes.len() + 1]);
- for (i, b) in bytes.iter().enumerate() {
- result[i] = *b as _;
- }
- Ok(result)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_truncate_str() {
- assert_eq!(truncate_str("test", 0), "");
- assert_eq!(truncate_str("test", 1), "t");
- assert_eq!(truncate_str("test", 2), "te");
- assert_eq!(truncate_str("test", 3), "tes");
- assert_eq!(truncate_str("test", 4), "test");
- assert_eq!(truncate_str("test", 5), "test");
- assert_eq!(truncate_str("test", 6), "test");
- }
-
- #[test]
- fn test_str_from_null_terminated() {
- assert_eq!(str_from_null_terminated(b"\0"), Ok(""));
- assert_eq!(str_from_null_terminated(b"hello\0"), Ok("hello"));
- assert_eq!(str_from_null_terminated(b"hello\0world"), Ok("hello"));
- // valid utf8.
- assert_eq!(
- str_from_null_terminated(b"\xc3\xb6\xc3\xa4\xc3\xbc \xf0\x9f\x91\x8c\0world"),
- Ok("öäü 👌")
- );
- // invalid utf8 after the null terminator
- assert_eq!(str_from_null_terminated(b"hello\0\xFF"), Ok("hello"));
- // invalid utf8 before the null terminator
- assert!(str_from_null_terminated(b"\xFF\0world").is_err());
- // Not null terminated.
- assert!(str_from_null_terminated(b"").is_err());
- assert!(str_from_null_terminated(b"foo").is_err());
- }
-
- #[test]
- #[allow(clippy::manual_c_str_literals)]
- fn test_str_from_null_terminated_ptr() {
- assert_eq!(
- unsafe { str_from_null_terminated_ptr(b"\0".as_ptr().cast()) },
- Ok("")
- );
- assert_eq!(
- unsafe { str_from_null_terminated_ptr(b"hello\0".as_ptr().cast()) },
- Ok("hello")
- );
- assert_eq!(
- unsafe { str_from_null_terminated_ptr(b"hello\0world".as_ptr().cast()) },
- Ok("hello")
- );
- // valid utf8.
- assert_eq!(
- unsafe {
- str_from_null_terminated_ptr(
- b"\xc3\xb6\xc3\xa4\xc3\xbc \xf0\x9f\x91\x8c\0world"
- .as_ptr()
- .cast(),
- )
- },
- Ok("öäü 👌")
- );
- // invalid utf8 after the null terminator
- assert_eq!(
- unsafe { str_from_null_terminated_ptr(b"hello\0\xFF".as_ptr().cast()) },
- Ok("hello")
- );
- // invalid utf8 before the null terminator
- assert!(unsafe { str_from_null_terminated_ptr(b"\xFF\0world".as_ptr().cast()) }.is_err());
- }
-
- #[test]
- fn test_str_to_cstr_vec() {
- assert_eq!(str_to_cstr_vec(""), Ok(vec![0]));
- assert_eq!(
- str_to_cstr_vec("test"),
- Ok(b"test\0"
- .iter()
- .map(|c| *c as _)
- .collect::<Vec<core::ffi::c_char>>())
- );
- assert_eq!(str_to_cstr_vec("te\0st"), Err(()));
- }
-
- #[test]
- fn test_str_to_cstr_vec_zeroizing() {
- assert_eq!(str_to_cstr_vec_zeroizing("").unwrap().as_slice(), &[0]);
- assert_eq!(
- str_to_cstr_vec_zeroizing("test").unwrap(),
- b"test\0"
- .iter()
- .map(|c| *c as _)
- .collect::<Vec<core::ffi::c_char>>()
- .into(),
- );
- assert_eq!(str_to_cstr_vec_zeroizing("te\0st"), Err(()));
- }
-}
diff --git a/src/rust/util/Cargo.toml b/src/rust/util/Cargo.toml
index 7e08ad3..ed04288 100644
--- a/src/rust/util/Cargo.toml
+++ b/src/rust/util/Cargo.toml
@@ -16,6 +16,7 @@ sha2 = { workspace = true, optional = true }
p256 = { version = "0.13.2", default-features = false, features = ["arithmetic", "ecdsa"], optional = true }
bitcoin = {workspace = true}
critical-section = { version = "1.2.0", default-features = false, features = [] }
+zeroize = { workspace = true }
[dev-dependencies]
hex_lit = { workspace = true }
diff --git a/src/rust/util/src/lib.rs b/src/rust/util/src/lib.rs
index de472bf..9370d38 100644
--- a/src/rust/util/src/lib.rs
+++ b/src/rust/util/src/lib.rs
@@ -9,6 +9,7 @@ pub mod cell;
pub mod decimal;
pub mod log;
pub mod name;
+pub mod strings;
mod waker_fn;
#[cfg(feature = "p256")]
diff --git a/src/rust/util/src/strings.rs b/src/rust/util/src/strings.rs
new file mode 100644
index 0000000..6a04e01
--- /dev/null
+++ b/src/rust/util/src/strings.rs
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0
+
+extern crate alloc;
+use alloc::vec::Vec;
+
+/// Parses a utf-8 string out of a null terminated buffer. Returns `Err(())` if there
+/// is no null terminator or if the bytes before the null terminator is invalid UTF8.
+pub fn str_from_null_terminated(input: &[u8]) -> Result<&str, ()> {
+ core::ffi::CStr::from_bytes_until_nul(input)
+ .or(Err(()))?
+ .to_str()
+ .or(Err(()))
+}
+
+/// Parses a utf-8 string out of a null terminated buffer starting at `ptr`. Returns `Err(())` if
+/// the bytes before the null terminator is invalid UTF8.
+///
+/// # Safety `ptr` must be not null and be a null terminated string. The resulting string is only
+/// valid as long the memory pointed to by `ptr` is valid.
+pub unsafe fn str_from_null_terminated_ptr<'a>(
+ ptr: *const core::ffi::c_char,
+) -> Result<&'a str, ()> {
+ unsafe { core::ffi::CStr::from_ptr(ptr.cast()).to_str().or(Err(())) }
+}
+
+/// truncate_str truncates string `s` to `len` chars. If `s` is
+/// shorter than `len`, the string is returned unchanged (no panics).
+pub fn truncate_str(s: &str, len: usize) -> &str {
+ if s.len() > len { &s[..len] } else { s }
+}
+
+/// Converts a Rust string to a null terminated C string by appending a null
+/// terminator. Returns `Err(())` if the input already contains a null byte.
+pub fn str_to_cstr_vec(input: &str) -> Result<Vec<core::ffi::c_char>, ()> {
+ let cstr = alloc::ffi::CString::new(input)
+ .or(Err(()))?
+ .into_bytes_with_nul();
+ // into_bytes_with_nul always returns Vec<u8> independent of platform. Let's cast it to c_char
+ // which is platform specific (unsigned on some platforms, signed on others).
+ // Implemented without unsafe on purpose.
+ Ok(cstr.into_iter().map(|c| c as _).collect())
+}
+
+/// Converts a Rust string to a null terminated C string by appending a null
+/// terminator. Returns `Err(())` if the input already contains a null byte.
+pub fn str_to_cstr_vec_zeroizing(
+ input: &str,
+) -> Result<zeroize::Zeroizing<Vec<core::ffi::c_char>>, ()> {
+ let bytes = input.as_bytes();
+ if bytes.contains(&0) {
+ return Err(());
+ }
+ let mut result: zeroize::Zeroizing<Vec<core::ffi::c_char>> =
+ zeroize::Zeroizing::new(vec![0; bytes.len() + 1]);
+ for (i, b) in bytes.iter().enumerate() {
+ result[i] = *b as _;
+ }
+ Ok(result)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_truncate_str() {
+ assert_eq!(truncate_str("test", 0), "");
+ assert_eq!(truncate_str("test", 1), "t");
+ assert_eq!(truncate_str("test", 2), "te");
+ assert_eq!(truncate_str("test", 3), "tes");
+ assert_eq!(truncate_str("test", 4), "test");
+ assert_eq!(truncate_str("test", 5), "test");
+ assert_eq!(truncate_str("test", 6), "test");
+ }
+
+ #[test]
+ fn test_str_from_null_terminated() {
+ assert_eq!(str_from_null_terminated(b"\0"), Ok(""));
+ assert_eq!(str_from_null_terminated(b"hello\0"), Ok("hello"));
+ assert_eq!(str_from_null_terminated(b"hello\0world"), Ok("hello"));
+ // valid utf8.
+ assert_eq!(
+ str_from_null_terminated(b"\xc3\xb6\xc3\xa4\xc3\xbc \xf0\x9f\x91\x8c\0world"),
+ Ok("öäü 👌")
+ );
+ // invalid utf8 after the null terminator
+ assert_eq!(str_from_null_terminated(b"hello\0\xFF"), Ok("hello"));
+ // invalid utf8 before the null terminator
+ assert!(str_from_null_terminated(b"\xFF\0world").is_err());
+ // Not null terminated.
+ assert!(str_from_null_terminated(b"").is_err());
+ assert!(str_from_null_terminated(b"foo").is_err());
+ }
+
+ #[test]
+ #[allow(clippy::manual_c_str_literals)]
+ fn test_str_from_null_terminated_ptr() {
+ assert_eq!(
+ unsafe { str_from_null_terminated_ptr(b"\0".as_ptr().cast()) },
+ Ok("")
+ );
+ assert_eq!(
+ unsafe { str_from_null_terminated_ptr(b"hello\0".as_ptr().cast()) },
+ Ok("hello")
+ );
+ assert_eq!(
+ unsafe { str_from_null_terminated_ptr(b"hello\0world".as_ptr().cast()) },
+ Ok("hello")
+ );
+ // valid utf8.
+ assert_eq!(
+ unsafe {
+ str_from_null_terminated_ptr(
+ b"\xc3\xb6\xc3\xa4\xc3\xbc \xf0\x9f\x91\x8c\0world"
+ .as_ptr()
+ .cast(),
+ )
+ },
+ Ok("öäü 👌")
+ );
+ // invalid utf8 after the null terminator
+ assert_eq!(
+ unsafe { str_from_null_terminated_ptr(b"hello\0\xFF".as_ptr().cast()) },
+ Ok("hello")
+ );
+ // invalid utf8 before the null terminator
+ assert!(unsafe { str_from_null_terminated_ptr(b"\xFF\0world".as_ptr().cast()) }.is_err());
+ }
+
+ #[test]
+ fn test_str_to_cstr_vec() {
+ assert_eq!(str_to_cstr_vec(""), Ok(vec![0]));
+ assert_eq!(
+ str_to_cstr_vec("test"),
+ Ok(b"test\0"
+ .iter()
+ .map(|c| *c as _)
+ .collect::<Vec<core::ffi::c_char>>())
+ );
+ assert_eq!(str_to_cstr_vec("te\0st"), Err(()));
+ }
+
+ #[test]
+ fn test_str_to_cstr_vec_zeroizing() {
+ assert_eq!(str_to_cstr_vec_zeroizing("").unwrap().as_slice(), &[0]);
+ assert_eq!(
+ str_to_cstr_vec_zeroizing("test").unwrap(),
+ b"test\0"
+ .iter()
+ .map(|c| *c as _)
+ .collect::<Vec<core::ffi::c_char>>()
+ .into(),
+ );
+ assert_eq!(str_to_cstr_vec_zeroizing("te\0st"), Err(()));
+ }
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.