use crate::{Error, error::ContextError};
pub(crate) fn wide_to_string(ptr: *const u16, len: u32) -> Result<String, Error> {
if len == 0 || ptr.is_null() {
return Ok(String::new());
}
let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
String::from_utf16(slice)
.map_err(|_| Error::builder("WinHTTP returned invalid UTF-16 in URL component"))
}
#[cfg_attr(all(not(feature = "tracing"), not(test)), expect(dead_code))]
pub(crate) unsafe fn wide_to_string_lossy(ptr: *mut std::ffi::c_void, byte_len: u32) -> String {
if ptr.is_null() || byte_len == 0 {
return String::new();
}
let wchar_count = (byte_len as usize) / 2;
let slice = unsafe { std::slice::from_raw_parts(ptr as *const u16, wchar_count) };
let slice = match slice.iter().position(|&c| c == 0) {
Some(pos) => slice.get(..pos).unwrap_or(slice),
None => slice,
};
String::from_utf16_lossy(slice)
}
pub(crate) fn read_env_var(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
pub(crate) fn string_from_utf16(buf: &[u16], context: &'static str) -> Result<String, Error> {
String::from_utf16(buf).map_err(|e| Error::decode(ContextError::new(context, e)))
}
pub(crate) fn lock_or_clear<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
match mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => {
warn!(
"Mutex poisoned (prior panic while lock held); \
recovering -- protected data is a simple Option<T> slot"
);
mutex.clear_poison();
poisoned.into_inner()
}
}
}
pub(crate) fn widen_latin1(bytes: &[u8]) -> String {
bytes.iter().map(|&b| b as char).collect()
}
pub(crate) fn narrow_latin1(s: &str) -> Vec<u8> {
s.chars()
.map(|ch| {
debug_assert!(ch as u32 <= 0xFF, "narrow_latin1 called on non-Latin-1 char");
ch as u8
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wide_to_string_ok_cases() {
let hello: [u16; 5] = [b'H' as u16, b'e' as u16, b'l' as u16, b'l' as u16, b'o' as u16];
let cases: &[(&str, *const u16, u32, &str)] = &[
("null_ptr", std::ptr::null(), 10, ""),
("zero_len", hello.as_ptr(), 0, ""),
("valid_utf16", hello.as_ptr(), 5, "Hello"),
];
for &(label, ptr, len, expected) in cases {
let result = wide_to_string(ptr, len).expect(label);
assert_eq!(result, expected, "wide_to_string {label}");
}
}
#[test]
fn wide_to_string_unpaired_surrogate() {
let data: [u16; 1] = [0xD800];
let result = wide_to_string(data.as_ptr(), 1);
assert!(result.is_err(), "unpaired surrogate should be an error");
}
#[test]
fn wide_to_string_lossy_table() {
let ab: [u16; 2] = [b'A' as u16, b'B' as u16];
let ok_null: [u16; 3] = [b'O' as u16, b'K' as u16, 0];
let cases: Vec<(*mut std::ffi::c_void, u32, &str, &str)> = vec![
(std::ptr::null_mut(), 10, "", "null_ptr"),
(ab.as_ptr() as *mut std::ffi::c_void, 0, "", "zero_len"),
(
ok_null.as_ptr() as *mut std::ffi::c_void,
6, "OK",
"trims_trailing_null",
),
(
ab.as_ptr() as *mut std::ffi::c_void,
4, "AB",
"no_trailing_null",
),
];
for (ptr, byte_len, expected, label) in &cases {
let result = unsafe { wide_to_string_lossy(*ptr, *byte_len) };
assert_eq!(result, *expected, "wide_to_string_lossy {label}");
}
}
#[test]
fn read_env_var_unset() {
assert!(read_env_var("wrest_TEST_NONEXISTENT_VAR_12345").is_none());
}
#[test]
fn lock_or_clear_recovers_from_poison() {
use std::sync::{Arc, Mutex};
let mutex = Arc::new(Mutex::new(42_i32));
let m2 = Arc::clone(&mutex);
let _ = std::thread::spawn(move || {
let _guard = m2.lock().unwrap();
panic!("intentional panic to poison mutex");
})
.join();
assert!(mutex.lock().is_err(), "mutex should be poisoned");
#[cfg(feature = "tracing")]
let _guard = ::tracing::subscriber::set_default(crate::tracing::SinkSubscriber);
let guard = lock_or_clear(&mutex);
assert_eq!(*guard, 42);
drop(guard);
assert!(mutex.lock().is_ok(), "poison should be cleared");
}
}