use crate::{raw, ValkeyString};
use std::cmp::Ordering;
use std::os::raw::c_char;
use std::ptr::null_mut;
use std::str::FromStr;
use std::sync::Arc;
type ShimString = Vec<u8>;
impl ValkeyString {
pub fn test<T: Into<Vec<u8>>>(data: T) -> ValkeyString {
super::setup_test_shims();
let inner = into_raw_string(data.into());
Self::from_redis_module_string(null_mut(), inner)
}
}
pub(super) extern "C" fn string_ptr_len(
string: *const raw::RedisModuleString,
len: *mut usize,
) -> *const c_char {
let data = unsafe { string_data(string) };
unsafe {
*len = data.len();
}
data.as_ptr().cast::<c_char>()
}
pub(super) extern "C" fn free_string(
_ctx: *mut raw::RedisModuleCtx,
string: *mut raw::RedisModuleString,
) {
if string.is_null() {
return;
}
unsafe {
Arc::decrement_strong_count(string.cast::<ShimString>());
}
}
pub(super) extern "C" fn retain_string(
_ctx: *mut raw::RedisModuleCtx,
string: *mut raw::RedisModuleString,
) {
if string.is_null() {
return;
}
unsafe {
Arc::increment_strong_count(string.cast::<ShimString>());
}
}
pub(super) extern "C" fn string_to_longlong(
string: *const raw::RedisModuleString,
value: *mut i64,
) -> libc::c_int {
parse_string(string, value)
}
pub(super) extern "C" fn string_to_ulonglong(
string: *const raw::RedisModuleString,
value: *mut libc::c_ulonglong,
) -> libc::c_int {
parse_string(string, value)
}
pub(super) extern "C" fn string_to_double(
string: *const raw::RedisModuleString,
value: *mut f64,
) -> libc::c_int {
parse_string(string, value)
}
pub(super) extern "C" fn create_string(
_ctx: *mut raw::RedisModuleCtx,
ptr: *const c_char,
len: usize,
) -> *mut raw::RedisModuleString {
if ptr.is_null() {
return if len == 0 {
into_raw_string(Vec::new())
} else {
null_mut()
};
}
let data = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
into_raw_string(data.to_vec())
}
pub(super) extern "C" fn create_string_from_string(
_ctx: *mut raw::RedisModuleCtx,
string: *const raw::RedisModuleString,
) -> *mut raw::RedisModuleString {
let data = unsafe { string_data(string) };
into_raw_string(data.to_vec())
}
pub(super) extern "C" fn string_compare(
left: *const raw::RedisModuleString,
right: *const raw::RedisModuleString,
) -> libc::c_int {
let left = unsafe { string_data(left) };
let right = unsafe { string_data(right) };
match left.cmp(right) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
fn parse_string<T: FromStr>(string: *const raw::RedisModuleString, value: *mut T) -> libc::c_int {
let data = unsafe { string_data(string) };
let Ok(data) = std::str::from_utf8(data) else {
return raw::Status::Err as libc::c_int;
};
let Ok(data) = data.parse::<T>() else {
return raw::Status::Err as libc::c_int;
};
unsafe {
*value = data;
}
raw::Status::Ok as libc::c_int
}
unsafe fn string_data<'a>(string: *const raw::RedisModuleString) -> &'a [u8] {
unsafe { (&*string.cast::<ShimString>()).as_slice() }
}
fn into_raw_string(data: ShimString) -> *mut raw::RedisModuleString {
Arc::into_raw(Arc::new(data))
.cast_mut()
.cast::<raw::RedisModuleString>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructs_reads_and_drops_string() {
let string = ValkeyString::test("value");
assert_eq!(string.as_slice(), b"value");
assert_eq!(string.len(), 5);
drop(string);
}
#[test]
fn preserves_arbitrary_binary_data() {
let bytes = vec![0x00, 0xff, b'a'];
let string = ValkeyString::test(bytes.clone());
assert_eq!(string.as_slice(), bytes);
}
#[test]
fn context_creates_string() {
let context = crate::Context::test();
let data = "é日";
let string = context.create_string(data);
assert_eq!(string.as_slice(), data.as_bytes());
assert_eq!(string.len(), data.len());
}
#[test]
fn create_string_copies_binary_input() {
let data = [0x00, 0xff, b'a'];
let inner = create_string(null_mut(), data.as_ptr().cast::<c_char>(), data.len());
let string = ValkeyString::from_redis_module_string(null_mut(), inner);
assert_eq!(string.as_slice(), data);
}
#[test]
fn create_string_accepts_null_for_empty_input() {
let inner = create_string(null_mut(), std::ptr::null(), 0);
let string = ValkeyString::from_redis_module_string(null_mut(), inner);
assert!(string.is_empty());
}
#[test]
fn create_string_rejects_null_for_nonempty_input() {
assert!(create_string(null_mut(), std::ptr::null(), 1).is_null());
}
#[test]
fn reports_multibyte_utf8_length_in_bytes() {
let data = "é日";
let string = ValkeyString::test(data);
assert_eq!(string.len(), data.len());
assert_eq!(string.as_slice(), data.as_bytes());
}
#[test]
fn retained_string_survives_original_drop() {
let original = ValkeyString::test("value");
let retained = ValkeyString::new(None, original.inner);
drop(original);
assert_eq!(retained.as_slice(), b"value");
}
#[test]
fn original_string_survives_retained_drop() {
let original = ValkeyString::test("value");
let retained = ValkeyString::new(None, original.inner);
drop(retained);
assert_eq!(original.as_slice(), b"value");
}
#[test]
fn clone_has_independent_allocation() {
let original = ValkeyString::test("value");
let cloned = original.clone();
assert_ne!(original.inner, cloned.inner);
drop(original);
assert_eq!(cloned.as_slice(), b"value");
}
#[test]
fn compares_strings_by_binary_value() {
let lower = ValkeyString::test([0x00, 0xff]);
let equal = ValkeyString::test([0x00, 0xff]);
let greater = ValkeyString::test([0x01]);
assert_eq!(lower, equal);
assert!(lower < greater);
assert!(greater > lower);
}
#[test]
fn parses_signed_integer() {
let string = ValkeyString::test("-42");
assert_eq!(string.parse_integer().expect("integer should parse"), -42);
}
#[test]
fn parses_full_unsigned_integer_range() {
let string = ValkeyString::test(u64::MAX.to_string());
let mut value = 0;
let status =
unsafe { raw::RedisModule_StringToULongLong.unwrap()(string.inner, &mut value) };
assert_eq!(status, raw::Status::Ok as libc::c_int);
assert_eq!(value, u64::MAX);
}
#[test]
fn parses_double() {
let string = ValkeyString::test("42.5");
assert_eq!(string.parse_float().expect("double should parse"), 42.5);
}
#[test]
fn parse_failure_leaves_output_unchanged() {
let string = ValkeyString::test([0xff]);
let mut value = 7;
let status =
unsafe { raw::RedisModule_StringToLongLong.unwrap()(string.inner, &mut value) };
assert_eq!(status, raw::Status::Err as libc::c_int);
assert_eq!(value, 7);
}
#[test]
fn invalid_numeric_syntax_leaves_output_unchanged() {
let string = ValkeyString::test("not-a-number");
let mut value = 7;
let status =
unsafe { raw::RedisModule_StringToLongLong.unwrap()(string.inner, &mut value) };
assert_eq!(status, raw::Status::Err as libc::c_int);
assert_eq!(value, 7);
}
#[test]
fn free_string_accepts_null() {
free_string(null_mut(), null_mut());
}
#[test]
fn retain_string_accepts_null() {
retain_string(null_mut(), null_mut());
}
}