Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap prop names into a PropName type offering free conversion to str #780

Merged
merged 14 commits into from
Aug 7, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

* Convert properties to `&PropName` which can be converted at no cost to `&CStr`
and `&str` (mina86)

## 0.21.0 (2023-05-09)

* Add doc-check to CI with fix warnings in docs (YuraKotov)
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mod env;
mod iter_range;
pub mod merge_operator;
pub mod perf;
mod prop_name;
pub mod properties;
mod slice_transform;
mod snapshot;
Expand Down
318 changes: 318 additions & 0 deletions src/prop_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
use crate::ffi_util::CStrLike;

use std::ffi::{CStr, CString};

/// A borrowed name of a RocksDB property.
///
/// The value is guaranteed to be a NUL-terminated UTF-8 string. This means it
mina86 marked this conversation as resolved.
Show resolved Hide resolved
/// can be converted to [`CStr`] and [`str`] with at zero cost.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct PropName(CStr);

impl PropName {
/// Creates a new object from a NUL-terminated ASCII string.
///
/// Panics if the `value` contains non-ASCII bytes, isn’t terminated by
/// a NUL byte or contains interior NUL bytes.
pub(crate) const fn new_unwrap(value: &str) -> &Self {
let bytes = value.as_bytes();

// Check terminating NUL byte.
let mut idx = bytes.len().saturating_sub(1);
assert!(
!bytes.is_empty() && bytes[idx] == 0,
"input was not nul-terminated"
);
// Check all other bytes are non-NUL ASCII bytes.
while idx > 0 {
idx -= 1;
assert!(
bytes[idx].is_ascii() && bytes[idx] != 0,
"input contained interior nul or non-ASCII byte"
);
}
aleksuss marked this conversation as resolved.
Show resolved Hide resolved

// SAFETY:
// 1. We’ve just verified `bytes` is a NUL-terminated ASCII string
// with no interior NUL bytes.
// 2. Self and CStr have the same representation so casting is
// sound.
unsafe {
let cstr = CStr::from_bytes_with_nul_unchecked(bytes);
&*(cstr as *const CStr as *const Self)
}
}

/// Converts the value into a C string slice.
#[inline]
pub fn as_c_str(&self) -> &CStr {
&self.0
}

/// Converts the value into a string slice.
///
/// NUL byte terminating the underlying C string is not included in the
/// returned slice.
#[inline]
pub fn as_str(&self) -> &str {
// SAFETY: self.0 is guaranteed to be valid ASCII string.
unsafe { std::str::from_utf8_unchecked(self.0.to_bytes()) }
}
}

impl core::ops::Deref for PropName {
type Target = CStr;

#[inline]
fn deref(&self) -> &Self::Target {
self.as_c_str()
}
}

impl core::convert::AsRef<CStr> for PropName {
#[inline]
fn as_ref(&self) -> &CStr {
self.as_c_str()
}
}

impl core::convert::AsRef<str> for PropName {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl std::borrow::ToOwned for PropName {
type Owned = PropertyName;

#[inline]
fn to_owned(&self) -> Self::Owned {
PropertyName(self.0.to_owned())
}

// TODO: Uncomment once compiler version is updated to 1.63.
mina86 marked this conversation as resolved.
Show resolved Hide resolved
// fn cone_into(&self, target: &mut Self::Owned) {
// self.0.clone_into(&mut target.0)
// }
}

impl core::fmt::Display for PropName {
#[inline]
fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_str().fmt(fmtr)
}
}

impl core::fmt::Debug for PropName {
#[inline]
fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_str().fmt(fmtr)
}
}

impl core::cmp::PartialEq<CStr> for PropName {
#[inline]
fn eq(&self, other: &CStr) -> bool {
self.as_c_str().eq(other)
}
}

impl core::cmp::PartialEq<str> for PropName {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str().eq(other)
}
}

impl core::cmp::PartialEq<PropName> for CStr {
#[inline]
fn eq(&self, other: &PropName) -> bool {
self.eq(other.as_c_str())
}
}

impl core::cmp::PartialEq<PropName> for str {
#[inline]
fn eq(&self, other: &PropName) -> bool {
self.eq(other.as_str())
}
}

impl<'a> CStrLike for &'a PropName {
type Baked = &'a CStr;
type Error = std::convert::Infallible;

#[inline]
fn bake(self) -> Result<Self::Baked, Self::Error> {
Ok(&self.0)
}

#[inline]
fn into_c_string(self) -> Result<CString, Self::Error> {
Ok(self.0.to_owned())
}
}

/// An owned name of a RocksDB property.
///
/// The value is guaranteed to be a NUL-terminated UTF-8 string. This means it
/// can be converted to [`CString`] and [`String`] at zero cost.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct PropertyName(CString);

impl PropertyName {
/// Creates a new object from valid NUL-terminated UTF-8 string. The string
/// must not contain interior NUL bytes.
#[inline]
unsafe fn from_vec_with_nul_unchecked(inner: Vec<u8>) -> Self {
// SAFETY: Caller promises inner is NUL-terminated and valid UTF-8.
Self(CString::from_vec_with_nul_unchecked(inner))
}

/// Converts the property name into a string.
///
/// NUL byte terminating the underlying C string is not included in the
/// returned value.
#[inline]
pub fn into_string(self) -> String {
// SAFETY: self.0 is guaranteed to be valid UTF-8.
unsafe { String::from_utf8_unchecked(self.0.into_bytes()) }
}

/// Converts the value into a C string.
#[inline]
pub fn into_c_string(self) -> CString {
self.0
}
}

impl std::ops::Deref for PropertyName {
type Target = PropName;

#[inline]
fn deref(&self) -> &Self::Target {
// SAFETY: 1. PropName and CStr have the same representation so casting
// is safe. 2. self.0 is guaranteed to be valid NUL-terminated UTF-8
// string.
unsafe { &*(self.0.as_c_str() as *const CStr as *const PropName) }
}
}

impl core::convert::AsRef<CStr> for PropertyName {
#[inline]
fn as_ref(&self) -> &CStr {
self.as_c_str()
}
}

impl core::convert::AsRef<str> for PropertyName {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl std::borrow::Borrow<PropName> for PropertyName {
#[inline]
fn borrow(&self) -> &PropName {
self
}
}

impl core::fmt::Display for PropertyName {
#[inline]
fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_str().fmt(fmtr)
}
}

impl core::fmt::Debug for PropertyName {
#[inline]
fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_str().fmt(fmtr)
}
}

impl core::cmp::PartialEq<CString> for PropertyName {
#[inline]
fn eq(&self, other: &CString) -> bool {
self.as_c_str().eq(other.as_c_str())
}
}

impl core::cmp::PartialEq<String> for PropertyName {
#[inline]
fn eq(&self, other: &String) -> bool {
self.as_str().eq(other.as_str())
}
}

impl core::cmp::PartialEq<PropertyName> for CString {
#[inline]
fn eq(&self, other: &PropertyName) -> bool {
self.as_c_str().eq(other.as_c_str())
}
}

impl core::cmp::PartialEq<PropertyName> for String {
#[inline]
fn eq(&self, other: &PropertyName) -> bool {
self.as_str().eq(other.as_str())
}
}

impl CStrLike for PropertyName {
type Baked = CString;
type Error = std::convert::Infallible;

#[inline]
fn bake(self) -> Result<Self::Baked, Self::Error> {
Ok(self.0)
}

#[inline]
fn into_c_string(self) -> Result<CString, Self::Error> {
Ok(self.0)
}
}

impl<'a> CStrLike for &'a PropertyName {
type Baked = &'a CStr;
type Error = std::convert::Infallible;

#[inline]
fn bake(self) -> Result<Self::Baked, Self::Error> {
Ok(self.as_c_str())
}

#[inline]
fn into_c_string(self) -> Result<CString, Self::Error> {
Ok(self.0.clone())
}
}

/// Constructs a property name for an ‘at level’ property.
///
/// `name` is the infix of the property name (e.g. `"num-files-at-level"`) and
/// `level` is level to get statistics of. The property name is constructed as
mina86 marked this conversation as resolved.
Show resolved Hide resolved
/// `"rocksdb.<name><level>"`.
///
/// Expects `name` not to contain any interior NUL bytes.
pub(crate) unsafe fn level_property(name: &str, level: usize) -> PropertyName {
let bytes = format!("rocksdb.{name}{level}\0").into_bytes();
// SAFETY: We’re appending terminating NUL and caller promises `name` has no
// interior NUL bytes.
PropertyName::from_vec_with_nul_unchecked(bytes)
}

#[test]
fn sanity_checks() {
let want = "rocksdb.cfstats-no-file-histogram";
assert_eq!(want, crate::properties::CFSTATS_NO_FILE_HISTOGRAM);

let want = "rocksdb.num-files-at-level5";
assert_eq!(want, &*crate::properties::num_files_at_level(5));
}