Skip to content

Commit

Permalink
Wrap props names into a PropName type offering free conversion to str
Browse files Browse the repository at this point in the history
Introduce properties::PropName and properties::PropertyName which wrap
CStr and CString types respectively with additional requirement that
the value is valid UTF-8.  This means that the types offer zero-cost
conversion into string.

Use it with property names in properties module such that the
properties can be used with property_value (which was the case before)
and also Displayed (which previously required UTF-8 verification or an
unsafe block and now can be done safely with zero additional cost).

This is API breaking change (since type of objects in properties
module change) but the breakage should be easy to deal with
(especially since the new types implement CStrLike and thus most
common usage of the property names will continue to work).
  • Loading branch information
mina86 committed May 15, 2023
1 parent b539412 commit d85661e
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 77 deletions.
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
304 changes: 304 additions & 0 deletions src/prop_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
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
/// 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"
);
while idx > 0 {
idx -= 1;
assert!(
bytes[idx].is_ascii() && bytes[idx] != 0,
"input contained interior nul or non-ASCII byte"
);
}

// 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 {
fn as_ref(&self) -> &str {
self.as_str()
}
}

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

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

// TODO: Uncomment once compiler version is updated to 1.63.
// 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 {
fn eq(&self, other: &CStr) -> bool {
self.as_c_str().eq(other)
}
}

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

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

impl core::cmp::PartialEq<PropName> for str {
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 no 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.
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 {
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl std::borrow::Borrow<PropName> for PropertyName {
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 {
fn eq(&self, other: &CString) -> bool {
self.as_c_str().eq(other.as_c_str())
}
}

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

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

impl core::cmp::PartialEq<PropertyName> for String {
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
/// `"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));
}

0 comments on commit d85661e

Please sign in to comment.