use bytes::Bytes;
use crate::constants::{lob_flags, OracleType};
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub enum LobData {
String(String),
Bytes(Bytes),
}
impl LobData {
pub fn as_string(&self) -> Option<&String> {
match self {
LobData::String(s) => Some(s),
LobData::Bytes(_) => None,
}
}
pub fn as_bytes(&self) -> Option<&Bytes> {
match self {
LobData::Bytes(b) => Some(b),
LobData::String(_) => None,
}
}
pub fn into_string(self) -> Option<String> {
match self {
LobData::String(s) => Some(s),
LobData::Bytes(_) => None,
}
}
pub fn into_bytes(self) -> Option<Bytes> {
match self {
LobData::Bytes(b) => Some(b),
LobData::String(_) => None,
}
}
pub fn is_string(&self) -> bool {
matches!(self, LobData::String(_))
}
pub fn is_bytes(&self) -> bool {
matches!(self, LobData::Bytes(_))
}
pub fn len(&self) -> usize {
match self {
LobData::String(s) => s.len(),
LobData::Bytes(b) => b.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone)]
pub struct LobLocator {
pub(crate) locator: Bytes,
pub(crate) size: u64,
pub(crate) chunk_size: u32,
pub(crate) oracle_type: OracleType,
pub(crate) _csfrm: u8,
}
impl LobLocator {
pub fn new(
locator: Bytes,
size: u64,
chunk_size: u32,
oracle_type: OracleType,
csfrm: u8,
) -> Self {
Self {
locator,
size,
chunk_size,
oracle_type,
_csfrm: csfrm,
}
}
pub fn size(&self) -> u64 {
self.size
}
pub fn chunk_size(&self) -> u32 {
self.chunk_size
}
pub fn oracle_type(&self) -> OracleType {
self.oracle_type
}
pub fn is_blob(&self) -> bool {
self.oracle_type == OracleType::Blob
}
pub fn is_clob(&self) -> bool {
self.oracle_type == OracleType::Clob
}
pub fn is_bfile(&self) -> bool {
self.oracle_type == OracleType::Bfile
}
pub fn is_initialized(&self) -> bool {
if self.locator.len() > 5 {
(self.locator[5] & lob_flags::LOC_FLAGS_INIT) != 0
} else {
false
}
}
pub fn uses_var_length_charset(&self) -> bool {
if self.locator.len() > lob_flags::LOC_OFFSET_FLAG_3 {
(self.locator[lob_flags::LOC_OFFSET_FLAG_3] & lob_flags::LOC_FLAGS_VAR_LENGTH_CHARSET)
!= 0
} else {
false
}
}
pub fn is_temp(&self) -> bool {
if self.locator.len() > lob_flags::LOC_OFFSET_FLAG_4 {
(self.locator[lob_flags::LOC_OFFSET_FLAG_4] & lob_flags::LOC_FLAGS_TEMP) != 0
} else {
false
}
}
pub fn locator_bytes(&self) -> &[u8] {
&self.locator
}
pub fn encoding(&self) -> &'static str {
if self.uses_var_length_charset() {
"UTF-16BE"
} else {
"UTF-8"
}
}
pub fn get_file_name(&self) -> Option<(String, String)> {
if !self.is_bfile() {
return None;
}
const LOC_FIXED_OFFSET: usize = 16;
if self.locator.len() < LOC_FIXED_OFFSET + 2 {
return None;
}
let dir_name_len = u16::from_be_bytes([
self.locator[LOC_FIXED_OFFSET],
self.locator[LOC_FIXED_OFFSET + 1],
]) as usize;
let dir_name_offset = LOC_FIXED_OFFSET + 2;
let file_name_len_offset = dir_name_offset + dir_name_len;
if self.locator.len() < file_name_len_offset + 2 {
return None;
}
let dir_name = String::from_utf8_lossy(
&self.locator[dir_name_offset..dir_name_offset + dir_name_len],
)
.to_string();
let file_name_len = u16::from_be_bytes([
self.locator[file_name_len_offset],
self.locator[file_name_len_offset + 1],
]) as usize;
let file_name_offset = file_name_len_offset + 2;
if self.locator.len() < file_name_offset + file_name_len {
return None;
}
let file_name = String::from_utf8_lossy(
&self.locator[file_name_offset..file_name_offset + file_name_len],
)
.to_string();
Some((dir_name, file_name))
}
}
#[derive(Debug, Clone)]
pub enum LobValue {
Inline(Bytes),
Locator(LobLocator),
Empty,
Null,
}
impl LobValue {
pub fn inline(data: Bytes) -> Self {
if data.is_empty() {
Self::Empty
} else {
Self::Inline(data)
}
}
pub fn locator(locator: LobLocator) -> Self {
Self::Locator(locator)
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn is_empty(&self) -> bool {
match self {
Self::Empty => true,
Self::Null => true,
Self::Inline(data) => data.is_empty(),
Self::Locator(loc) => loc.size == 0,
}
}
pub fn size(&self) -> Option<u64> {
match self {
Self::Null => None,
Self::Empty => Some(0),
Self::Inline(data) => Some(data.len() as u64),
Self::Locator(loc) => Some(loc.size),
}
}
pub fn as_inline(&self) -> Option<&Bytes> {
match self {
Self::Inline(data) => Some(data),
_ => None,
}
}
pub fn as_locator(&self) -> Option<&LobLocator> {
match self {
Self::Locator(loc) => Some(loc),
_ => None,
}
}
pub fn as_string(&self) -> Result<Option<String>> {
match self {
Self::Null => Ok(None),
Self::Empty => Ok(Some(String::new())),
Self::Inline(data) => {
Ok(Some(String::from_utf8_lossy(data).to_string()))
}
Self::Locator(_) => Err(Error::Protocol(
"LOB data requires explicit read operation".to_string(),
)),
}
}
pub fn as_bytes(&self) -> Result<Option<Bytes>> {
match self {
Self::Null => Ok(None),
Self::Empty => Ok(Some(Bytes::new())),
Self::Inline(data) => Ok(Some(data.clone())),
Self::Locator(_) => Err(Error::Protocol(
"LOB data requires explicit read operation".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lob_locator_flags() {
let mut locator_bytes = vec![0u8; 20];
locator_bytes[5] = lob_flags::LOC_FLAGS_INIT; locator_bytes[lob_flags::LOC_OFFSET_FLAG_4] = lob_flags::LOC_FLAGS_TEMP;
let locator = LobLocator::new(
Bytes::from(locator_bytes),
1000,
8060,
OracleType::Clob,
1,
);
assert!(locator.is_initialized());
assert!(locator.is_temp());
assert!(!locator.uses_var_length_charset());
assert!(locator.is_clob());
assert!(!locator.is_blob());
}
#[test]
fn test_lob_value_inline() {
let data = Bytes::from("Hello, World!");
let lob = LobValue::inline(data.clone());
assert!(!lob.is_null());
assert!(!lob.is_empty());
assert_eq!(lob.size(), Some(13));
assert_eq!(lob.as_inline(), Some(&data));
assert_eq!(lob.as_string().unwrap(), Some("Hello, World!".to_string()));
}
#[test]
fn test_lob_value_empty() {
let lob = LobValue::Empty;
assert!(!lob.is_null());
assert!(lob.is_empty());
assert_eq!(lob.size(), Some(0));
}
#[test]
fn test_bfile_locator_get_file_name() {
let dir_name = "TEST_DIR";
let file_name = "test.txt";
let mut locator_bytes = vec![0u8; 16]; locator_bytes.push((dir_name.len() >> 8) as u8);
locator_bytes.push(dir_name.len() as u8);
locator_bytes.extend_from_slice(dir_name.as_bytes());
locator_bytes.push((file_name.len() >> 8) as u8);
locator_bytes.push(file_name.len() as u8);
locator_bytes.extend_from_slice(file_name.as_bytes());
let locator = LobLocator::new(
Bytes::from(locator_bytes),
0,
0,
OracleType::Bfile,
0,
);
assert!(locator.is_bfile());
let (dir, file) = locator.get_file_name().expect("Should parse BFILE locator");
assert_eq!(dir, "TEST_DIR");
assert_eq!(file, "test.txt");
}
#[test]
fn test_bfile_locator_get_file_name_non_bfile() {
let locator_bytes = vec![0u8; 20];
let locator = LobLocator::new(
Bytes::from(locator_bytes),
0,
0,
OracleType::Blob, 0,
);
assert!(!locator.is_bfile());
assert!(locator.get_file_name().is_none());
}
#[test]
fn test_bfile_locator_get_file_name_empty_names() {
let mut locator_bytes = vec![0u8; 16]; locator_bytes.push(0);
locator_bytes.push(0);
locator_bytes.push(0);
locator_bytes.push(0);
let locator = LobLocator::new(
Bytes::from(locator_bytes),
0,
0,
OracleType::Bfile,
0,
);
let (dir, file) = locator.get_file_name().expect("Should parse empty BFILE locator");
assert_eq!(dir, "");
assert_eq!(file, "");
}
}