Skip to content

Commit 2a43433

Browse files
authored
feat!: fallible AsBytes::try_as_bytes (#1211)
* feat!: fallible `AsBytes::try_as_bytes` This gives us another API to use to have fewer panics and more fallible FFI calls. This also adds an `FfiSafeErrorMessage` trait which is used by the return of `AsBytes::try_as_bytes`. See its docs for details. * test: try_as_slice/try_as_bytes
1 parent 25b69f1 commit 2a43433

File tree

8 files changed

+386
-34
lines changed

8 files changed

+386
-34
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ddcommon-ffi/src/slice.rs

Lines changed: 207 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use core::slice;
5+
use ddcommon::error::FfiSafeErrorMessage;
56
use serde::ser::Error;
67
use serde::Serializer;
78
use std::borrow::Cow;
@@ -11,6 +12,14 @@ use std::marker::PhantomData;
1112
use std::os::raw::c_char;
1213
use std::str::Utf8Error;
1314

15+
#[repr(C)]
16+
#[derive(Clone, Copy, Debug)]
17+
pub enum SliceConversionError {
18+
LargeLength,
19+
NullPointer,
20+
MisalignedPointer,
21+
}
22+
1423
#[repr(C)]
1524
#[derive(Copy, Clone)]
1625
pub struct Slice<'a, T: 'a> {
@@ -25,6 +34,25 @@ pub struct Slice<'a, T: 'a> {
2534
_marker: PhantomData<&'a [T]>,
2635
}
2736

37+
/// # Safety
38+
/// All strings are valid UTF-8 (enforced by using c-str literals in Rust).
39+
unsafe impl FfiSafeErrorMessage for SliceConversionError {
40+
fn as_ffi_str(&self) -> &'static std::ffi::CStr {
41+
match self {
42+
SliceConversionError::LargeLength => c"length was too large",
43+
SliceConversionError::NullPointer => c"null pointer with non-zero length",
44+
SliceConversionError::MisalignedPointer => c"pointer was not aligned for the type",
45+
}
46+
}
47+
}
48+
impl Display for SliceConversionError {
49+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50+
Display::fmt(self.as_rust_str(), f)
51+
}
52+
}
53+
54+
impl core::error::Error for SliceConversionError {}
55+
2856
impl<'a, T: 'a> core::ops::Deref for Slice<'a, T> {
2957
type Target = [T];
3058

@@ -55,18 +83,27 @@ pub type ByteSlice<'a> = Slice<'a, u8>;
5583

5684
/// This exists as an intrinsic, but it is private.
5785
pub fn is_aligned_and_not_null<T>(ptr: *const T) -> bool {
58-
!ptr.is_null() && is_aligned(ptr)
59-
}
60-
61-
#[inline]
62-
fn is_aligned<T>(ptr: *const T) -> bool {
63-
debug_assert!(!ptr.is_null());
64-
ptr as usize % std::mem::align_of::<T>() == 0
86+
!ptr.is_null() && ptr.is_aligned()
6587
}
6688

6789
pub trait AsBytes<'a> {
6890
fn as_bytes(&self) -> &'a [u8];
6991

92+
/// Tries to interpret the structure as a slice of bytes.
93+
///
94+
/// # Errors
95+
///
96+
/// - Returns [`SliceConversionError::NullPointer`] if the slice has a null pointer and a
97+
/// length other than zero. If pointer is null and length is zero, then return `Ok(&[])`
98+
/// instead.
99+
/// - Returns [`SliceConversionError::MisalignedPointer`] if the pointer is non-null and is not
100+
/// aligned correctly for the type (not generally possible with types which are inherently
101+
/// byte oriented, but is if the slice is of some other type which is being safely
102+
/// reinterpreted as bytes).
103+
/// - Returns [`SliceConversionError::LargeLength`] if the length of the slice exceeds
104+
/// [`isize::MAX`].
105+
fn try_as_bytes(&self) -> Result<&'a [u8], SliceConversionError>;
106+
70107
#[inline]
71108
fn try_to_utf8(&self) -> Result<&'a str, Utf8Error> {
72109
std::str::from_utf8(self.as_bytes())
@@ -98,12 +135,40 @@ impl<'a> AsBytes<'a> for Slice<'a, u8> {
98135
fn as_bytes(&self) -> &'a [u8] {
99136
self.as_slice()
100137
}
138+
139+
fn try_as_bytes(&self) -> Result<&'a [u8], SliceConversionError> {
140+
self.try_as_slice()
141+
}
101142
}
102143

103144
impl<'a> AsBytes<'a> for Slice<'a, i8> {
104145
fn as_bytes(&self) -> &'a [u8] {
105-
// SAFETY: safe to convert *i8 to *u8 and then read it... I think.
106-
unsafe { Slice::from_raw_parts(self.ptr.cast(), self.len) }.as_slice()
146+
#[allow(clippy::expect_used)]
147+
self.try_as_bytes()
148+
.expect("AsBytes::as_bytes failed to convert to a Rust slice")
149+
}
150+
151+
fn try_as_bytes(&self) -> Result<&'a [u8], SliceConversionError> {
152+
self.try_as_slice().map(|slice| {
153+
// SAFETY: we've gone through a successful try_as_slice, so the
154+
// enforceable characteristics such as fitting in isize::MAX are
155+
// all good. The rest is safe only if the consumer respects the
156+
// inherent safety requirements--doesn't give invalid length,
157+
// pointer to invalid memory, etc.
158+
unsafe { slice::from_raw_parts(slice.as_ptr().cast(), self.len) }
159+
})
160+
}
161+
}
162+
163+
impl<'a> AsBytes<'a> for &'a [c_char] {
164+
fn as_bytes(&self) -> &'a [u8] {
165+
// SAFETY: We're converting from &[c_char] to &[u8] which is safe since
166+
// they have the same layout and c_char has no unused bit patterns.
167+
unsafe { slice::from_raw_parts(self.as_ptr().cast(), self.len()) }
168+
}
169+
170+
fn try_as_bytes(&self) -> Result<&'a [u8], SliceConversionError> {
171+
Ok(self.as_bytes())
107172
}
108173
}
109174

@@ -146,15 +211,32 @@ impl<'a, T: 'a> Slice<'a, T> {
146211
}
147212

148213
pub fn as_slice(&self) -> &'a [T] {
149-
if !self.ptr.is_null() {
150-
// Crashing immediately is likely better than ignoring these.
151-
assert!(is_aligned(self.ptr));
152-
assert!(self.len <= isize::MAX as usize);
153-
unsafe { slice::from_raw_parts(self.ptr, self.len) }
214+
#[allow(clippy::expect_used)]
215+
self.try_as_slice()
216+
.expect("ffi Slice failed to convert to a Rust slice")
217+
}
218+
219+
/// Tries to convert the FFI slice into a standard slice.
220+
///
221+
/// # Errors
222+
///
223+
/// 1. Fails if `self.ptr` is null and `self.len` is not zero.
224+
/// 2. Fails if `self.ptr` is not null and is unaligned.
225+
/// 3. Fails if `self.len` is larger than [`isize::MAX`].
226+
pub fn try_as_slice(&self) -> Result<&'a [T], SliceConversionError> {
227+
let (ptr, len) = self.as_raw_parts();
228+
if !ptr.is_null() {
229+
if len > isize::MAX as usize {
230+
Err(SliceConversionError::LargeLength)
231+
} else if !ptr.is_aligned() {
232+
Err(SliceConversionError::MisalignedPointer)
233+
} else {
234+
Ok(unsafe { slice::from_raw_parts(ptr, len) })
235+
}
236+
} else if len != 0 {
237+
Err(SliceConversionError::NullPointer)
154238
} else {
155-
// Crashing immediately is likely better than ignoring this.
156-
assert_eq!(self.len, 0);
157-
&[]
239+
Ok(&[])
158240
}
159241
}
160242

@@ -346,4 +428,112 @@ mod tests {
346428
};
347429
_ = dangerous.as_slice();
348430
}
431+
432+
#[test]
433+
fn test_try_as_slice_success() {
434+
let data: &[u8] = b"hello world";
435+
let slice = Slice::from(data);
436+
437+
let result = slice.try_as_slice();
438+
assert_eq!(result.unwrap(), data);
439+
}
440+
441+
#[test]
442+
fn test_try_as_slice_null_zero_len() {
443+
let null_zero_len: Slice<u8> = Slice {
444+
ptr: ptr::null(),
445+
len: 0,
446+
_marker: PhantomData,
447+
};
448+
449+
let result = null_zero_len.try_as_slice();
450+
assert_eq!(result.unwrap(), &[]);
451+
}
452+
453+
#[test]
454+
fn test_try_as_slice_null_nonzero_len() {
455+
let null_nonzero: Slice<u8> = Slice {
456+
ptr: ptr::null(),
457+
len: 5,
458+
_marker: PhantomData,
459+
};
460+
461+
let result = null_nonzero.try_as_slice();
462+
assert!(matches!(
463+
result.unwrap_err(),
464+
SliceConversionError::NullPointer
465+
));
466+
}
467+
468+
#[test]
469+
fn test_try_as_slice_large_length() {
470+
let large_len: Slice<u8> = Slice {
471+
ptr: ptr::NonNull::dangling().as_ptr(),
472+
len: isize::MAX as usize + 1,
473+
_marker: PhantomData,
474+
};
475+
476+
let result = large_len.try_as_slice();
477+
assert!(matches!(
478+
result.unwrap_err(),
479+
SliceConversionError::LargeLength
480+
));
481+
}
482+
483+
#[test]
484+
fn test_try_as_slice_misaligned_pointer() {
485+
// Create a misaligned pointer for u64 by using a properly aligned
486+
// array, then offset by 1 byte.
487+
let data = [0u64; 2];
488+
let base_ptr = data.as_ptr().cast::<u8>();
489+
let misaligned_ptr = unsafe { base_ptr.add(1).cast::<u64>() };
490+
491+
// Verify the pointer is actually misaligned
492+
assert!(!misaligned_ptr.is_aligned());
493+
494+
let misaligned: Slice<u64> = Slice {
495+
ptr: misaligned_ptr,
496+
len: 1,
497+
_marker: PhantomData,
498+
};
499+
500+
let result = misaligned.try_as_slice();
501+
assert!(matches!(
502+
result.unwrap_err(),
503+
SliceConversionError::MisalignedPointer
504+
));
505+
}
506+
507+
#[test]
508+
fn test_try_as_bytes_u8_slice() {
509+
let data: &[u8] = b"test data";
510+
let slice = Slice::from(data);
511+
512+
let result = slice.try_as_bytes();
513+
assert_eq!(result.unwrap(), data);
514+
}
515+
516+
#[test]
517+
fn test_try_as_bytes_i8_slice() {
518+
let data: &[i8] = &[72, 101, 108, 108, 111]; // "Hello" in i8
519+
let slice = Slice::from(data);
520+
521+
let result = slice.try_as_bytes();
522+
assert_eq!(result.unwrap(), b"Hello");
523+
}
524+
525+
#[test]
526+
fn test_try_as_bytes_char_slice_error_propagation() {
527+
let null_nonzero: Slice<c_char> = Slice {
528+
ptr: ptr::null(),
529+
len: 3,
530+
_marker: PhantomData,
531+
};
532+
533+
let result = null_nonzero.try_as_bytes();
534+
assert!(matches!(
535+
result.unwrap_err(),
536+
SliceConversionError::NullPointer
537+
));
538+
}
349539
}

0 commit comments

Comments
 (0)