Skip to content

Commit

Permalink
introduce variadic kind
Browse files Browse the repository at this point in the history
  • Loading branch information
pacman82 committed Jan 21, 2023
1 parent 27cd460 commit 1f8addd
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 49 deletions.
11 changes: 11 additions & 0 deletions odbc-api/src/buffers/indicator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,15 @@ impl Indicator {
Indicator::Length(len) => len.try_into().unwrap(),
}
}

/// Does this indicator imply truncation for a value of the given length?
///
/// `length_in_buffer` is specified in bytes without terminating zeroes.
pub fn is_truncated(self, length_in_buffer: usize) -> bool {
match self {
Indicator::Null => false,
Indicator::NoTotal => true,
Indicator::Length(complete_length) => complete_length > length_in_buffer,
}
}
}
2 changes: 1 addition & 1 deletion odbc-api/src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ mod varchar;
pub use self::{
blob::{Blob, BlobParam, BlobRead, BlobSlice},
varbin::{VarBinary, VarBinaryArray, VarBinaryBox, VarBinarySlice, VarBinarySliceMut},
varchar::{VarChar, VarCharArray, VarCharBox, VarCharSlice, VarCharSliceMut},
varchar::{VariadicCell, VarCharArray, VarCharBox, VarCharSlice, VarCharSliceMut},
};

use std::ffi::c_void;
Expand Down
183 changes: 135 additions & 48 deletions odbc-api/src/parameter/varchar.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
borrow::{Borrow, BorrowMut},
ffi::c_void,
marker::PhantomData,
};

use odbc_sys::{CDataType, NULL_DATA};
Expand All @@ -13,6 +14,29 @@ use crate::{

use super::CElement;

/// Intended to be used as a generic argument for [`VariadicCell`] to declare that this buffer is
/// used to hold narrow (as opposed to wide UTF-16) text.
pub struct Text;

pub unsafe trait VariadicKind {
/// Number of terminating zeroes required for this kind of variadic buffer.
const TERMINATING_ZEROES: usize;
const C_DATA_TYPE: CDataType;
fn relational_type(length: usize) -> DataType;
}

unsafe impl VariadicKind for Text {
const TERMINATING_ZEROES: usize = 1;
const C_DATA_TYPE: CDataType = CDataType::Char;

fn relational_type(length: usize) -> DataType {
// Since we might use as an input buffer, we report the full buffer length in the type and
// do not deduct 1 for the terminating zero.
DataType::Varchar { length }
}

}

/// Binds a byte array as Variadic sized character data. It can not be used for columnar bulk
/// fetches, but if the buffer type is stack allocated it can be utilized in row wise bulk fetches.
///
Expand All @@ -23,7 +47,7 @@ use super::CElement;
/// * [`self::VarCharArray`] - stack allocated owned input / output parameter
/// * [`self::VarCharBox`] - heap allocated owned input /output parameter
#[derive(Debug, Clone, Copy)]
pub struct VarChar<B> {
pub struct VariadicCell<B, K> {
/// Contains the value. Characters must be valid up to the index indicated by `indicator`. If
/// `indicator` is longer than buffer, the last element in buffer must be a terminating zero,
/// which is not regarded as being part of the payload itself.
Expand All @@ -35,15 +59,20 @@ pub struct VarChar<B> {
/// unless the value is `\0`. In that case we assume `\0` to be a terminating zero left over
/// from truncation, rather than the last character of the string.
indicator: isize,
/// Variadic Kind, declaring wether the buffer holds text or binary data.
kind: PhantomData<K>,
}

/// Parameter type for owned, variable sized character data.
///
/// We use `Box<[u8]>` rather than `Vec<u8>` as a buffer type since the indicator pointer already
/// has the role of telling us how many bytes in the buffer are part of the payload.
pub type VarCharBox = VarChar<Box<[u8]>>;
pub type VarCharBox = VariadicCell<Box<[u8]>, Text>;

impl VarCharBox {
impl<K> VariadicCell<Box<[u8]>, K>
where
K: VariadicKind,
{
/// Constructs a 'missing' value.
pub fn null() -> Self {
// We do not want to use the empty buffer (`&[]`) here. It would be bound as `VARCHAR(0)`
Expand All @@ -65,45 +94,42 @@ impl VarCharBox {
}
}

impl<B> VarChar<B>
impl<B, K> VariadicCell<B, K>
where
B: Borrow<[u8]>,
K: VariadicKind,
{
/// Creates a new instance from an existing buffer. Should the indicator be `NoTotal` or indicate
/// a length longer than buffer, the last element in the buffer must be nul (`\0`).
/// Creates a new instance from an existing buffer. For text should the indicator be `NoTotal`
/// or indicate a length longer than buffer, the last element in the buffer must be nul (`\0`).
pub fn from_buffer(buffer: B, indicator: Indicator) -> Self {
let buf = buffer.borrow();
match indicator {
Indicator::Null => (),
Indicator::NoTotal => {
if buf.is_empty() || *buf.last().unwrap() != 0 {
panic!("Truncated value must be terminated with zero.")
}
}
Indicator::Length(len) => {
if len > buf.len() && (buf.is_empty() || *buf.last().unwrap() != 0) {
panic!("Truncated value must be terminated with zero.")
}
if indicator.is_truncated(buf.len()) {
// Value is truncated. Let's check that all required terminating zeroes are at the end
// of the buffer.
if !ends_in_zeroes(buf, K::TERMINATING_ZEROES) {
panic!("Truncated value must be terminated with zero.")
}
};
}

Self {
buffer,
indicator: indicator.to_isize(),
kind: PhantomData,
}
}

/// Returns the binary representation of the string, excluding the terminating zero or `None` in
/// Valid payload of the buffer (excluding terminating zeroes) returned as slice or `None` in
/// case the indicator is `NULL_DATA`.
pub fn as_bytes(&self) -> Option<&[u8]> {
let slice = self.buffer.borrow();
match self.indicator() {
Indicator::Null => None,
Indicator::NoTotal => Some(&slice[..(slice.len() - 1)]),
Indicator::NoTotal => Some(&slice[..(slice.len() - K::TERMINATING_ZEROES)]),
Indicator::Length(len) => {
if self.is_complete() {
Some(&slice[..len])
} else {
Some(&slice[..(slice.len() - 1)])
Some(&slice[..(slice.len() - K::TERMINATING_ZEROES)])
}
}
}
Expand Down Expand Up @@ -133,30 +159,52 @@ where
/// fn process_text_slice(text: &[u8]) { /*...*/}
///
/// ```
///
/// ```
/// use odbc_api::{CursorRow, parameter::VarBinaryArray, Error, handles::Statement};
///
/// fn process_large_binary(
/// col_index: u16,
/// row: &mut CursorRow<'_>
/// ) -> Result<(), Error>{
/// let mut buf = VarBinaryArray::<512>::NULL;
/// row.get_data(col_index, &mut buf)?;
/// while !buf.is_complete() {
/// // Process bytes in stream without allocation. We can assume repeated calls to
/// // get_data do not return `None` since it would have done so on the first call.
/// process_slice(buf.as_bytes().unwrap());
/// }
/// Ok(())
/// }
///
/// fn process_slice(text: &[u8]) { /*...*/}
///
/// ```
pub fn is_complete(&self) -> bool {
let slice = self.buffer.borrow();
match self.indicator() {
Indicator::Null => true,
Indicator::NoTotal => false,
Indicator::Length(len) => {
len < slice.len() || slice.is_empty() || *slice.last().unwrap() != 0
}
}
let max_value_length = if ends_in_zeroes(&slice, K::TERMINATING_ZEROES) {
slice.len() - K::TERMINATING_ZEROES
} else {
slice.len()
};
!self.indicator().is_truncated(max_value_length)
}

/// Read access to the underlying ODBC indicator. After data has been fetched the indicator
/// value is set to the length the buffer should have had, excluding the terminating zero. It
/// may also be `NULL_DATA` to indicate `NULL` or `NO_TOTAL` which tells us the data source
/// does not know how big the buffer must be to hold the complete value. `NO_TOTAL` implies that
/// the content of the current buffer is valid up to its maximum capacity.
/// value is set to the length the buffer should have had to hold the entire value. It may also
/// be [`Indicator::Null`] to indicate `NULL` or [`Indicator::NoTotal`] which tells us the data
/// source does not know how big the buffer must be to hold the complete value.
/// [`Indicator::NoTotal`] implies that the content of the current buffer is valid up to its
/// maximum capacity.
pub fn indicator(&self) -> Indicator {
Indicator::from_isize(self.indicator)
}
}

impl<B> VarChar<B>
impl<B, K> VariadicCell<B, K>
where
B: Borrow<[u8]>,
K: VariadicKind,
{
/// Call this method to reset the indicator to a value which matches the length returned by the
/// [`Self::as_bytes`] method. This is useful if you want to insert values into the database
Expand All @@ -165,17 +213,20 @@ where
/// detect the truncation and throw an error.
pub fn hide_truncation(&mut self) {
if !self.is_complete() {
self.indicator = (self.buffer.borrow().len() - 1).try_into().unwrap();
self.indicator = (self.buffer.borrow().len() - K::TERMINATING_ZEROES)
.try_into()
.unwrap();
}
}
}

unsafe impl<B> CData for VarChar<B>
unsafe impl<B, K> CData for VariadicCell<B, K>
where
B: Borrow<[u8]>,
K: VariadicKind
{
fn cdata_type(&self) -> CDataType {
CDataType::Char
K::C_DATA_TYPE
}

fn indicator_ptr(&self) -> *const isize {
Expand All @@ -194,22 +245,20 @@ where
}
}

impl<B> HasDataType for VarChar<B>
impl<B, K> HasDataType for VariadicCell<B, K>
where
B: Borrow<[u8]>,
K: VariadicKind
{
fn data_type(&self) -> DataType {
// Since we might use as an input buffer, we report the full buffer length in the type and
// do not deduct 1 for the terminating zero.
DataType::Varchar {
length: self.buffer.borrow().len(),
}
K::relational_type(self.buffer.borrow().len())
}
}

unsafe impl<B> CDataMut for VarChar<B>
unsafe impl<B,K> CDataMut for VariadicCell<B, K>
where
B: BorrowMut<[u8]>,
K: VariadicKind
{
fn mut_indicator_ptr(&mut self) -> *mut isize {
&mut self.indicator as *mut isize
Expand Down Expand Up @@ -244,16 +293,17 @@ where
/// };
/// # Ok::<(), odbc_api::Error>(())
/// ```
pub type VarCharSlice<'a> = VarChar<&'a [u8]>;
pub type VarCharSlice<'a> = VariadicCell<&'a [u8], Text>;

impl<'a> VarCharSlice<'a> {
impl<'a, K> VariadicCell<&'a [u8], K> where K: VariadicKind {
/// Indicates missing data
pub const NULL: Self = Self {
// We do not want to use the empty buffer (`&[]`) here. It would be bound as `VARCHAR(0)`
// which caused errors with Microsoft Access and older versions of the Microsoft SQL Server
// ODBC driver.
buffer: &[0],
indicator: NULL_DATA,
kind: PhantomData,
};

/// Constructs a new VarChar containing the text in the specified buffer.
Expand All @@ -268,19 +318,20 @@ impl<'a> VarCharSlice<'a> {
}

/// Wraps a slice so it can be used as an output parameter for character data.
pub type VarCharSliceMut<'a> = VarChar<&'a mut [u8]>;
pub type VarCharSliceMut<'a> = VariadicCell<&'a mut [u8], Text>;

/// A stack allocated VARCHAR type.
///
/// Due to its memory layout this type can be bound either as a single parameter, or as a column of
/// a row-by-row output, but not be used in columnar parameter arrays or output buffers.
pub type VarCharArray<const LENGTH: usize> = VarChar<[u8; LENGTH]>;
pub type VarCharArray<const LENGTH: usize> = VariadicCell<[u8; LENGTH], Text>;

impl<const LENGTH: usize> VarCharArray<LENGTH> {
/// Indicates a missing value.
pub const NULL: Self = VarCharArray {
buffer: [0; LENGTH],
indicator: NULL_DATA,
kind: PhantomData,
};

/// Construct from a slice. If value is longer than `LENGTH` it will be truncated. In that case
Expand All @@ -294,10 +345,25 @@ impl<const LENGTH: usize> VarCharArray<LENGTH> {
} else {
buffer[..text.len()].copy_from_slice(text);
};
Self { buffer, indicator }
Self {
buffer,
indicator,
kind: PhantomData,
}
}
}

/// Figures out, wether or not the buffer ends with a fixed number of zeroes.
fn ends_in_zeroes(buffer: &[u8], number_of_zeroes: usize) -> bool {
buffer.len() >= number_of_zeroes
&& buffer
.iter()
.rev()
.copied()
.take(number_of_zeroes)
.all(|byte| byte == 0)
}

// We can't go all out and implement these traits for anything implementing Borrow and BorrowMut,
// because erroneous but still safe implementation of these traits could cause invalid memory access
// down the road. E.g. think about returning a different slice with a different length for borrow
Expand All @@ -312,3 +378,24 @@ unsafe impl OutputParameter for VarCharSliceMut<'_> {}

unsafe impl CElement for VarCharBox {}
unsafe impl OutputParameter for VarCharBox {}

#[cfg(test)]
mod tests {

use super::{Indicator, VarCharSlice};

#[test]
fn must_accept_fitting_values_and_correctly_truncated_ones() {
// Fine: not truncated
VarCharSlice::from_buffer(b"12345", Indicator::Length(5));
// Fine: truncated, but ends in zero
VarCharSlice::from_buffer(b"1234\0", Indicator::Length(10));
}

#[test]
#[should_panic]
fn must_ensure_truncated_values_are_terminated() {
// Not fine, value is too long, but not terminated by zero
VarCharSlice::from_buffer(b"12345", Indicator::Length(10));
}
}

0 comments on commit 1f8addd

Please sign in to comment.