Skip to content

Commit

Permalink
Add CFData helpers
Browse files Browse the repository at this point in the history
Part of #692.
  • Loading branch information
madsmtm committed Jan 11, 2025
1 parent d39968e commit 5efb6eb
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 9 deletions.
2 changes: 1 addition & 1 deletion crates/objc2/src/topics/about_generated/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
no longer need to use `mut` to mutate them, but it also means that they are
no longer `Send + Sync`.
* **BREAKING**: Renamed the `bytes[_mut]` methods on `NSData` to
`as[_mut]_slice_unchecked`, and made them `unsafe`, since the data can no
`as[_mut]_bytes_unchecked`, and made them `unsafe`, since the data can no
longer ensure that it is not mutated while the bytes are in use.
* No longer require `Eq + Hash` for `NSDictionary` keys and `NSSet` values,
since it was overly restrictive.
Expand Down
106 changes: 106 additions & 0 deletions framework-crates/objc2-core-foundation/src/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use core::slice;

#[cfg(feature = "alloc")]
use alloc::vec::Vec;

use crate::{CFData, CFDataGetBytePtr, CFDataGetLength};

impl CFData {
/// Creates a new `CFData` from a byte slice.
#[inline]
#[cfg(feature = "CFBase")]
#[doc(alias = "CFDataCreate")]
pub fn from_bytes(bytes: &[u8]) -> crate::CFRetained<Self> {
let len = bytes.len().try_into().expect("buffer too large");
unsafe { crate::CFDataCreate(None, bytes.as_ptr(), len) }.expect("failed creating CFData")
}

/// Creates a new `CFData` from a `'static` byte slice.
///
/// This may be slightly more efficient than [`CFData::from_bytes`], as it
/// may be able to re-use the existing buffer (since we know it won't be
/// deallocated).
#[inline]
#[cfg(feature = "CFBase")]
#[doc(alias = "CFDataCreateWithBytesNoCopy")]
pub fn from_static_bytes(bytes: &'static [u8]) -> crate::CFRetained<Self> {
let len = bytes.len().try_into().expect("buffer too large");
// SAFETY: Same as `CFString::from_static_str`.
unsafe {
crate::CFDataCreateWithBytesNoCopy(None, bytes.as_ptr(), len, crate::kCFAllocatorNull)
}
.expect("failed creating CFData")
}

/// The number of bytes contained by the `CFData`.
#[inline]
#[doc(alias = "CFDataGetLength")]
pub fn len(&self) -> usize {
unsafe { CFDataGetLength(self) as _ }
}

/// Whether the `CFData` is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// The underlying bytes in the `CFData`.
///
///
/// # Safety
///
/// The `CFData` must not be mutated for the lifetime of the returned
/// string. Consider using [`to_vec`] instead if this requirement is a bit
/// difficult to uphold.
///
/// [`to_vec`]: Self::to_vec
#[inline]
#[doc(alias = "CFDataGetBytePtr")]
pub unsafe fn as_bytes_unchecked(&self) -> &[u8] {
let ptr = unsafe { CFDataGetBytePtr(self) };
if !ptr.is_null() {
// SAFETY: The pointer is valid, and caller ensures that the
// `CFData` is not mutated for the lifetime of it.
//
// Same as
unsafe { slice::from_raw_parts(ptr, self.len()) }
} else {
// The bytes pointer may be null for length zero
&[]
}
}

/// Copy the contents of the `CFData` into a new [`Vec`].
#[cfg(feature = "alloc")]
#[doc(alias = "CFDataGetBytePtr")]
pub fn to_vec(&self) -> Vec<u8> {
// NOTE: We don't do `Vec::from`, as that will call the allocator
// while the buffer is active, and we don't know if that allocator
// uses a CFMutableData under the hood (though very theoretical).

let mut vec = Vec::with_capacity(self.len());
// SAFETY: We've pre-allocated the Vec, so it won't call the allocator
// while the byte slice is alive (and hence it won't ).
vec.extend_from_slice(unsafe { self.as_bytes_unchecked() });
vec
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn roundtrip() {
let data = CFData::from_bytes(&[1, 2, 3]);
assert_eq!(data.to_vec(), [1, 2, 3]);
}

#[test]
fn empty() {
let data = CFData::from_bytes(&[]);
assert!(data.is_empty());
assert_eq!(data.to_vec(), []);
}
}
2 changes: 2 additions & 0 deletions framework-crates/objc2-core-foundation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ mod base;
#[cfg(feature = "CFBundle")]
mod bundle;
mod cf_type;
#[cfg(feature = "CFData")]
mod data;
#[cfg(feature = "CFDate")]
mod date;
mod generated;
Expand Down
4 changes: 2 additions & 2 deletions framework-crates/objc2-core-foundation/src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ impl CFString {
// NOTE: This is NOT public, since it's completely broken for differently
// encoded strings, see the `as_str_broken` test below.
#[allow(dead_code)]
unsafe fn as_str(&self) -> Option<&str> {
unsafe fn as_str_unchecked(&self) -> Option<&str> {
let bytes = unsafe { CFStringGetCStringPtr(self, CFStringEncoding::UTF8) };
NonNull::new(bytes as *mut c_char).map(|bytes| {
// SAFETY: The pointer is valid for as long as the CFString is not
Expand Down Expand Up @@ -314,7 +314,7 @@ mod tests {

// But `CFStringGetCStringPtr` completely ignores the UTF-8 conversion
// we asked it to do, i.e. a huge correctness footgun!
assert_eq!(unsafe { s.as_str() }, Some("e&"));
assert_eq!(unsafe { s.as_str_unchecked() }, Some("e&"));
}

#[test]
Expand Down
16 changes: 12 additions & 4 deletions framework-crates/objc2-foundation/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ extern_methods!(
);

impl NSData {
// TODO: Rename to `from_bytes` to match `CFData::from_bytes`.
pub fn with_bytes(bytes: &[u8]) -> Retained<Self> {
let bytes_ptr = bytes.as_ptr() as *mut c_void;
unsafe { Self::initWithBytes_length(Self::alloc(), bytes_ptr, bytes.len()) }
Expand Down Expand Up @@ -77,7 +78,7 @@ impl NSData {
/// difficult to uphold.
///
/// [`to_vec`]: Self::to_vec
pub unsafe fn as_slice_unchecked(&self) -> &[u8] {
pub unsafe fn as_bytes_unchecked(&self) -> &[u8] {
let ptr = self.bytes_raw();
if !ptr.is_null() {
let ptr: *const u8 = ptr.cast();
Expand All @@ -103,8 +104,15 @@ impl NSData {

/// Copy the contents of the data into a new [`Vec`].
pub fn to_vec(&self) -> Vec<u8> {
// SAFETY: The bytes are immediately copied to a new `Vec`.
unsafe { self.as_slice_unchecked() }.to_vec()
// NOTE: We don't do `Vec::from`, as that will call the allocator
// while the buffer is active, and we don't know if that allocator
// uses a CFMutableData under the hood (though very theoretical).

let mut vec = Vec::with_capacity(self.len());
// SAFETY: We've pre-allocated the Vec, so it won't call the allocator
// while the byte slice is alive (and hence it won't ).
vec.extend_from_slice(unsafe { self.as_bytes_unchecked() });
vec
}

/// Iterate over the bytes of the data.
Expand All @@ -122,7 +130,7 @@ impl NSMutableData {
/// slice is alive.
#[doc(alias = "mutableBytes")]
#[allow(clippy::mut_from_ref)]
pub unsafe fn as_mut_slice_unchecked(&self) -> &mut [u8] {
pub unsafe fn as_mut_bytes_unchecked(&self) -> &mut [u8] {
let ptr = self.mutable_bytes_raw();
// &Cell<[u8]> is safe because the slice length is not actually in the
// cell
Expand Down
2 changes: 1 addition & 1 deletion framework-crates/objc2-foundation/src/tests/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn test_from_vec() {
let bytes_ptr = bytes.as_ptr();

let data = NSData::from_vec(bytes);
assert_eq!(unsafe { data.as_slice_unchecked() }.as_ptr(), bytes_ptr);
assert_eq!(unsafe { data.as_bytes_unchecked() }.as_ptr(), bytes_ptr);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{NSData, NSMutableData};
#[test]
fn test_bytes_mut() {
let data = NSMutableData::with_bytes(&[7, 16]);
unsafe { data.as_mut_slice_unchecked()[0] = 3 };
unsafe { data.as_mut_bytes_unchecked()[0] = 3 };
assert_eq!(data.to_vec(), [3, 16]);
}

Expand Down

0 comments on commit 5efb6eb

Please sign in to comment.