#![forbid(unsafe_code)]
use super::*;
pub struct DbObjectPackedReader<'a> {
bytes: &'a [u8],
pos: usize,
limits: crate::wire::ProtocolLimits,
}
impl<'a> DbObjectPackedReader<'a> {
pub fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
pos: 0,
limits: crate::wire::ProtocolLimits::DEFAULT,
}
}
pub fn with_limits(bytes: &'a [u8], limits: crate::wire::ProtocolLimits) -> Result<Self> {
Ok(Self {
bytes,
pos: 0,
limits: limits.validate()?,
})
}
pub fn limits(&self) -> crate::wire::ProtocolLimits {
self.limits
}
pub fn read_u8(&mut self) -> Result<u8> {
let value = self
.bytes
.get(self.pos)
.copied()
.ok_or(ProtocolError::TtcDecode("truncated DbObject packed data"))?;
self.pos += 1;
Ok(value)
}
fn read_raw(&mut self, len: usize) -> Result<&'a [u8]> {
self.limits.check_response_bytes(len)?;
let end = self.pos.checked_add(len).ok_or(ProtocolError::TtcDecode(
"DbObject packed data offset overflow",
))?;
let bytes = self
.bytes
.get(self.pos..end)
.ok_or(ProtocolError::TtcDecode("truncated DbObject packed data"))?;
self.pos = end;
Ok(bytes)
}
fn skip(&mut self, len: usize) -> Result<()> {
self.read_raw(len).map(|_| ())
}
fn read_u32be(&mut self) -> Result<u32> {
let bytes = self.read_raw(4)?;
Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
ProtocolError::TtcDecode("invalid DbObject u32")
})?))
}
pub fn read_i32be(&mut self) -> Result<i32> {
let bytes = self.read_raw(4)?;
Ok(i32::from_be_bytes(bytes.try_into().map_err(|_| {
ProtocolError::TtcDecode("invalid DbObject i32")
})?))
}
pub fn read_length(&mut self) -> Result<usize> {
match self.read_u8()? {
TNS_LONG_LENGTH_INDICATOR => usize::try_from(self.read_u32be()?)
.map_err(|_| ProtocolError::TtcDecode("DbObject length overflow")),
length => Ok(usize::from(length)),
}
}
fn skip_length(&mut self) -> Result<()> {
if self.read_u8()? == TNS_LONG_LENGTH_INDICATOR {
self.skip(4)?;
}
Ok(())
}
pub fn read_value_bytes(&mut self) -> Result<Option<Vec<u8>>> {
let length = match self.read_u8()? {
TNS_NULL_LENGTH_INDICATOR => return Ok(None),
TNS_LONG_LENGTH_INDICATOR => usize::try_from(self.read_u32be()?)
.map_err(|_| ProtocolError::TtcDecode("DbObject value length overflow"))?,
length => usize::from(length),
};
Ok(Some(self.read_raw(length)?.to_vec()))
}
pub fn read_header(&mut self) -> Result<()> {
let flags = self.read_u8()?;
let _version = self.read_u8()?;
self.skip_length()?;
if flags & TNS_OBJ_IS_DEGENERATE != 0 {
return Err(ProtocolError::UnsupportedFeature(
"DbObject stored in a LOB",
));
}
if flags & TNS_OBJ_NO_PREFIX_SEG == 0 {
let prefix_len = self.read_length()?;
self.skip(prefix_len)?;
}
Ok(())
}
fn bytes_left(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
pub fn remaining(&self) -> usize {
self.bytes_left()
}
pub fn read_atomic_null(&mut self, is_collection_context: bool) -> Result<bool> {
let value = self.read_u8()?;
match (value, is_collection_context) {
(TNS_OBJ_ATOMIC_NULL, _) | (TNS_NULL_LENGTH_INDICATOR, true) => Ok(true),
_ => {
self.pos = self.pos.saturating_sub(1);
Ok(false)
}
}
}
}
impl crate::wire::BoundedReader for DbObjectPackedReader<'_> {
fn remaining(&self) -> usize {
self.bytes_left()
}
fn protocol_limits(&self) -> crate::wire::ProtocolLimits {
self.limits
}
}
pub fn image_write_value_bytes(buf: &mut Vec<u8>, value: &[u8]) -> Result<()> {
if value.len() <= TNS_OBJ_MAX_SHORT_LENGTH {
buf.push(value.len() as u8);
buf.extend_from_slice(value);
return Ok(());
}
let length = u32::try_from(value.len()).map_err(|_| ProtocolError::InvalidPacketLength {
length: value.len(),
minimum: 0,
})?;
buf.push(TNS_LONG_LENGTH_INDICATOR);
buf.extend_from_slice(&length.to_be_bytes());
buf.extend_from_slice(value);
Ok(())
}
pub fn image_write_length(buf: &mut Vec<u8>, length: usize) -> Result<()> {
if length <= TNS_OBJ_MAX_SHORT_LENGTH {
buf.push(length as u8);
} else {
buf.push(TNS_LONG_LENGTH_INDICATOR);
buf.extend_from_slice(
&u32::try_from(length)
.map_err(|_| ProtocolError::InvalidPacketLength { length, minimum: 0 })?
.to_be_bytes(),
);
}
Ok(())
}
pub fn image_begin(is_collection: bool) -> Vec<u8> {
let mut image_flags = TNS_OBJ_IS_VERSION_81;
if is_collection {
image_flags |= TNS_OBJ_IS_COLLECTION;
} else {
image_flags |= TNS_OBJ_NO_PREFIX_SEG;
}
let mut buf = Vec::new();
buf.push(image_flags);
buf.push(TNS_OBJ_IMAGE_VERSION);
buf.push(TNS_LONG_LENGTH_INDICATOR);
buf.extend_from_slice(&0u32.to_be_bytes()); if is_collection {
buf.push(1); buf.push(1); }
buf
}
pub fn image_finalize(buf: &mut [u8]) -> Result<()> {
let size = u32::try_from(buf.len()).map_err(|_| ProtocolError::InvalidPacketLength {
length: buf.len(),
minimum: 0,
})?;
let slot = buf.get_mut(3..7).ok_or(ProtocolError::TtcDecode(
"DbObject image too short to finalize",
))?;
slot.copy_from_slice(&size.to_be_bytes());
Ok(())
}
pub fn collection_flags_for(is_assoc_array: bool) -> u8 {
if is_assoc_array {
TNS_OBJ_HAS_INDEXES
} else {
0
}
}
pub fn image_write_null(buf: &mut Vec<u8>, atomic_null: bool) {
if atomic_null {
buf.push(TNS_OBJ_ATOMIC_NULL);
} else {
buf.push(TNS_NULL_LENGTH_INDICATOR);
}
}
pub fn pack_bindvalue_into_image(buf: &mut Vec<u8>, value: &BindValue, csfrm: u8) -> Result<()> {
match value {
BindValue::Text(text) => {
let bytes = encode_text_value(text, csfrm);
image_write_value_bytes(buf, &bytes)
}
BindValue::Raw(bytes) => image_write_value_bytes(buf, bytes),
BindValue::Number(text) => {
let bytes = encode_number_text(text)?;
image_write_value_bytes(buf, &bytes)
}
BindValue::BinaryInteger(text) => {
let value = parse_binary_integer_u32(text)?;
buf.push(4);
buf.extend_from_slice(&value.to_be_bytes());
Ok(())
}
BindValue::Boolean(value) => {
buf.push(4);
buf.extend_from_slice(&u32::from(*value).to_be_bytes());
Ok(())
}
BindValue::BinaryDouble(value) => {
let bytes = encode_binary_double(*value);
image_write_value_bytes(buf, &bytes)
}
BindValue::BinaryFloat(value) => {
let bytes = encode_binary_float(*value as f32);
image_write_value_bytes(buf, &bytes)
}
BindValue::DateTime {
year,
month,
day,
hour,
minute,
second,
} => {
let bytes = encode_oracle_date(*year, *month, *day, *hour, *minute, *second)?;
image_write_value_bytes(buf, &bytes)
}
BindValue::Timestamp {
year,
month,
day,
hour,
minute,
second,
nanosecond,
ora_type_num,
} => {
let bytes = if matches!(*ora_type_num, ORA_TYPE_NUM_TIMESTAMP_TZ) {
encode_oracle_timestamp_tz(
*year,
*month,
*day,
*hour,
*minute,
*second,
*nanosecond,
)?
} else {
encode_oracle_timestamp(*year, *month, *day, *hour, *minute, *second, *nanosecond)?
};
image_write_value_bytes(buf, &bytes)
}
BindValue::TimestampTz {
year,
month,
day,
hour,
minute,
second,
nanosecond,
offset_minutes,
} => {
let bytes = encode_oracle_timestamp_tz_with_offset(
*year,
*month,
*day,
*hour,
*minute,
*second,
*nanosecond,
*offset_minutes,
)?;
image_write_value_bytes(buf, &bytes)
}
BindValue::Lob { locator, .. } => image_write_value_bytes(buf, locator),
BindValue::IntervalDS {
days,
seconds,
microseconds,
} => {
let nanoseconds = microseconds
.checked_mul(1000)
.ok_or(ProtocolError::TtcDecode(
"INTERVAL DS fractional seconds out of range",
))?;
let bytes = encode_interval_ds(*days, *seconds, nanoseconds)?;
image_write_value_bytes(buf, &bytes)
}
BindValue::IntervalYM { years, months } => {
let bytes = encode_interval_ym(*years, *months)?;
image_write_value_bytes(buf, &bytes)
}
BindValue::Null => {
image_write_null(buf, false);
Ok(())
}
_ => Err(ProtocolError::UnsupportedFeature(
"DbObject attribute type not supported for input binding",
)),
}
}
pub(crate) fn parse_binary_integer_u32(text: &str) -> Result<u32> {
let trimmed = text.trim();
let parsed: i64 = trimmed
.parse()
.map_err(|_| ProtocolError::TtcDecode("invalid BINARY_INTEGER value"))?;
Ok(parsed as u32)
}
pub fn write_dbobject_bind(writer: &mut TtcWriter, oid: &[u8], image: &[u8]) -> Result<()> {
let mut toid = Vec::with_capacity(4 + oid.len() + TNS_EXTENT_OID.len());
toid.extend_from_slice(&[0x00, 0x22, TNS_OBJ_NON_NULL_OID, TNS_OBJ_HAS_EXTENT_OID]);
toid.extend_from_slice(oid);
toid.extend_from_slice(&TNS_EXTENT_OID);
writer.write_bytes_with_two_lengths(Some(&toid))?;
writer.write_bytes_with_two_lengths(Some(oid))?;
writer.write_ub4(0); writer.write_ub4(0); writer.write_ub4(u32::try_from(image.len()).map_err(|_| {
ProtocolError::InvalidPacketLength {
length: image.len(),
minimum: 0,
}
})?);
writer.write_ub4(TNS_OBJ_TOP_LEVEL);
writer.write_bytes_with_length(image)
}
pub fn decode_dbobject_text(bytes: &[u8], dbtype_name: &str) -> Result<String> {
if matches!(dbtype_name, "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR") {
let mut chunks = bytes.chunks_exact(2);
let units = chunks
.by_ref()
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
if !chunks.remainder().is_empty() {
return Err(ProtocolError::TtcDecode("invalid DbObject UTF-16 text"));
}
return String::from_utf16(&units)
.map_err(|_| ProtocolError::TtcDecode("invalid DbObject UTF-16 text"));
}
String::from_utf8(bytes.to_vec())
.map_err(|_| ProtocolError::TtcDecode("invalid DbObject UTF-8 text"))
}
pub fn decode_dbobject_xmltype_text(bytes: &[u8]) -> Result<Option<String>> {
let mut reader = DbObjectPackedReader::new(bytes);
reader.read_header()?;
reader.skip(1)?;
let xml_flag = reader.read_u32be()?;
if xml_flag & TNS_XML_TYPE_FLAG_SKIP_NEXT_4 != 0 {
reader.skip(4)?;
}
let bytes = reader.read_raw(reader.bytes_left())?;
if xml_flag & TNS_XML_TYPE_STRING != 0 {
return decode_dbobject_text(bytes, "DB_TYPE_VARCHAR").map(Some);
}
if xml_flag & TNS_XML_TYPE_LOB != 0 {
return Ok(None);
}
Err(ProtocolError::TtcDecode("unexpected XMLTYPE flag"))
}
pub fn decode_lob_text(bytes: &[u8], csfrm: u8, locator: Option<&[u8]>) -> Result<String> {
let (use_utf16, little_endian) = lob_text_uses_utf16(csfrm, locator);
if !use_utf16 {
return core::str::from_utf8(bytes)
.map(str::to_owned)
.map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-8 text"));
}
if !bytes.len().is_multiple_of(2) {
return Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"));
}
let mut out = String::with_capacity(bytes.len() / 2);
let mut i = 0;
while i < bytes.len() {
let b0 = bytes[i];
let b1 = bytes[i + 1];
let is_ascii = if little_endian {
b1 == 0 && b0 < 0x80
} else {
b0 == 0 && b1 < 0x80
};
if is_ascii {
let ascii = if little_endian { b0 } else { b1 };
out.push(ascii as char);
i += 2;
} else {
let units = bytes[i..].chunks_exact(2).map(|chunk| {
if little_endian {
u16::from_le_bytes([chunk[0], chunk[1]])
} else {
u16::from_be_bytes([chunk[0], chunk[1]])
}
});
for unit in char::decode_utf16(units) {
let ch = unit.map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-16 text"))?;
out.push(ch);
}
return Ok(out);
}
}
Ok(out)
}
pub fn encode_lob_text(value: &str, csfrm: u8, locator: Option<&[u8]>) -> Vec<u8> {
let (use_utf16, little_endian) = lob_text_uses_utf16(csfrm, locator);
if !use_utf16 {
return value.as_bytes().to_vec();
}
let mut bytes = Vec::with_capacity(value.len() * 2);
for unit in value.encode_utf16() {
let encoded = if little_endian {
unit.to_le_bytes()
} else {
unit.to_be_bytes()
};
bytes.extend_from_slice(&encoded);
}
bytes
}
pub fn decode_bfile_locator_name(locator: &[u8]) -> Option<(String, String)> {
for dir_len_pos in 0..locator.len().saturating_sub(4) {
let dir_len = u16::from_be_bytes([locator[dir_len_pos], locator[dir_len_pos + 1]]) as usize;
if dir_len == 0 {
continue;
}
let dir_start = dir_len_pos + 2;
let dir_end = dir_start.checked_add(dir_len)?;
let file_len_end = dir_end.checked_add(2)?;
if file_len_end > locator.len() {
continue;
}
let file_len = u16::from_be_bytes([locator[dir_end], locator[dir_end + 1]]) as usize;
if file_len == 0 {
continue;
}
let file_start = file_len_end;
let file_end = file_start.checked_add(file_len)?;
if file_end != locator.len() {
continue;
}
let dir = std::str::from_utf8(&locator[dir_start..dir_end]).ok()?;
let file = std::str::from_utf8(&locator[file_start..file_end]).ok()?;
return Some((dir.to_string(), file.to_string()));
}
None
}
pub(crate) fn lob_text_uses_utf16(csfrm: u8, locator: Option<&[u8]>) -> (bool, bool) {
let use_utf16 = csfrm == CS_FORM_NCHAR
|| locator
.and_then(|locator| locator.get(TNS_LOB_LOC_OFFSET_FLAG_3))
.is_some_and(|flags| flags & TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET != 0);
let little_endian = locator
.and_then(|locator| locator.get(TNS_LOB_LOC_OFFSET_FLAG_4))
.is_some_and(|flags| flags & TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN != 0);
(use_utf16, little_endian)
}
pub fn decode_dbobject_binary_float(bytes: &[u8]) -> Result<f32> {
let mut bytes: [u8; 4] = bytes
.try_into()
.map_err(|_| ProtocolError::TtcDecode("invalid DbObject BINARY_FLOAT"))?;
if bytes[0] & 0x80 != 0 {
bytes[0] &= 0x7f;
} else {
for byte in &mut bytes {
*byte = !*byte;
}
}
Ok(f32::from_bits(u32::from_be_bytes(bytes)))
}
pub fn decode_dbobject_binary_double(bytes: &[u8]) -> Result<f64> {
let mut bytes: [u8; 8] = bytes
.try_into()
.map_err(|_| ProtocolError::TtcDecode("invalid DbObject BINARY_DOUBLE"))?;
if bytes[0] & 0x80 != 0 {
bytes[0] &= 0x7f;
} else {
for byte in &mut bytes {
*byte = !*byte;
}
}
Ok(f64::from_bits(u64::from_be_bytes(bytes)))
}
#[cfg(test)]
mod value_bytes_tests {
use super::*;
#[test]
fn value_bytes_roundtrip_uses_dbobject_single_u32be_length_format() {
for len in [244usize, 245, 246, 250, 65_535, 70_000] {
let value = (0..len).map(|i| (i % 251) as u8).collect::<Vec<_>>();
let mut image = Vec::new();
image_write_value_bytes(&mut image, &value).expect("encode value bytes");
let mut expected = Vec::new();
if len <= TNS_OBJ_MAX_SHORT_LENGTH {
expected.push(len as u8);
} else {
expected.push(TNS_LONG_LENGTH_INDICATOR);
expected.extend_from_slice(&(len as u32).to_be_bytes());
}
expected.extend_from_slice(&value);
assert_eq!(image, expected, "wire bytes for length {len}");
if len == 250 {
assert_eq!(
&image[..5],
&[TNS_LONG_LENGTH_INDICATOR, 0x00, 0x00, 0x00, 0xfa],
"250-byte values use one big-endian u32 length"
);
}
let mut reader = DbObjectPackedReader::new(&image);
let decoded = reader
.read_value_bytes()
.expect("decode value bytes")
.expect("non-null value bytes");
assert_eq!(decoded, value, "length {len}");
assert_eq!(reader.remaining(), 0, "length {len}");
}
}
}
#[cfg(test)]
mod bounded_reader_tests {
use super::*;
use crate::wire::BoundedReader;
#[test]
fn dbobject_oversized_collection_count_is_bounded_by_remaining() {
let bytes = [TNS_LONG_LENGTH_INDICATOR, 0x25, 0x00, 0x00, 0x00];
let mut reader = DbObjectPackedReader::new(&bytes);
let num_elements = reader.read_length().expect("length decodes");
assert_eq!(num_elements, 0x2500_0000);
assert!(
reader.alloc_count_checked(num_elements, 1).is_err(),
"declared count must not exceed the empty remaining buffer"
);
let v: Vec<u32> = reader.with_capacity_bounded(num_elements, 1);
assert_eq!(
v.capacity(),
0,
"pre-allocation must be capped by remaining"
);
}
#[test]
fn dbobject_legitimate_collection_count_passes() {
let bytes = [1u8, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11];
let reader = DbObjectPackedReader::new(&bytes);
assert_eq!(reader.alloc_count_checked(2, 1).expect("fits"), 2);
let v: Vec<u32> = reader.with_capacity_bounded(2, 1);
assert_eq!(v.capacity(), 2);
}
}
#[cfg(test)]
mod decode_lob_text_tests {
use super::*;
fn utf16_locator(little_endian: bool) -> Vec<u8> {
let mut loc = vec![0u8; 40];
loc[TNS_LOB_LOC_OFFSET_FLAG_3] = TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET;
if little_endian {
loc[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN;
}
loc
}
fn encode_utf16(s: &str, little_endian: bool) -> Vec<u8> {
let mut bytes = Vec::with_capacity(s.len() * 2);
for unit in s.encode_utf16() {
let pair = if little_endian {
unit.to_le_bytes()
} else {
unit.to_be_bytes()
};
bytes.extend_from_slice(&pair);
}
bytes
}
fn reference_from_utf16(bytes: &[u8], little_endian: bool) -> Result<String> {
let mut chunks = bytes.chunks_exact(2);
let units = chunks
.by_ref()
.map(|chunk| {
if little_endian {
u16::from_le_bytes([chunk[0], chunk[1]])
} else {
u16::from_be_bytes([chunk[0], chunk[1]])
}
})
.collect::<Vec<_>>();
if !chunks.remainder().is_empty() {
return Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"));
}
String::from_utf16(&units).map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-16 text"))
}
#[test]
fn utf16_matches_reference_for_varied_text_both_endians() {
let samples = [
"",
"a",
"the quick brown fox 0123456789",
"café résumé naïve", "ASCII then 漢字 then more", "emoji: 😀🎉 mixed with text", "\u{0000}\u{007f}\u{0080}\u{07ff}\u{0800}\u{ffff}", ];
for sample in samples {
for little_endian in [false, true] {
let bytes = encode_utf16(sample, little_endian);
let loc = utf16_locator(little_endian);
let got =
decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).expect("optimized decode");
let expected =
reference_from_utf16(&bytes, little_endian).expect("reference decode");
assert_eq!(got, expected, "sample {sample:?} le={little_endian}");
assert_eq!(got, sample);
}
}
}
#[test]
fn utf16_odd_length_is_rejected_like_reference() {
let loc = utf16_locator(false);
let bytes = [0x00, 0x41, 0x00];
assert!(decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).is_err());
assert!(reference_from_utf16(&bytes, false).is_err());
}
#[test]
fn utf16_lone_surrogate_is_rejected_like_reference() {
let loc = utf16_locator(false);
let mut bytes = encode_utf16("ok ", false);
bytes.extend_from_slice(&0xD83Du16.to_be_bytes());
bytes.extend_from_slice(&encode_utf16("tail", false));
assert!(decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).is_err());
assert!(reference_from_utf16(&bytes, false).is_err());
}
#[test]
fn utf8_path_matches_from_utf8() {
let loc = vec![0u8; 40];
let sample = "café — utf8 path ✓";
let bytes = sample.as_bytes();
let got = decode_lob_text(bytes, 1, Some(&loc)).expect("utf8 decode");
assert_eq!(
got,
String::from_utf8(bytes.to_vec()).expect("sample is valid UTF-8")
);
assert_eq!(got, sample);
let bad = [0x66, 0x6f, 0xff, 0x6f];
assert!(decode_lob_text(&bad, 1, Some(&loc)).is_err());
}
}