use bytes::{Buf, BufMut, BytesMut};
use std::io::{self, Cursor};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TtcFunction {
ProtoNeg = 1,
DataTypeNeg = 2,
OpenCursor = 3,
Parse = 4,
Execute = 5,
Fetch = 6,
CloseCursor = 7,
Commit = 8,
Rollback = 9,
Describe = 10,
Define = 11,
Bind = 12,
Version = 13,
Logon = 14,
Logoff = 15,
Oall = 17,
Ping = 147,
}
impl TtcFunction {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
1 => Some(Self::ProtoNeg),
2 => Some(Self::DataTypeNeg),
3 => Some(Self::OpenCursor),
4 => Some(Self::Parse),
5 => Some(Self::Execute),
6 => Some(Self::Fetch),
7 => Some(Self::CloseCursor),
8 => Some(Self::Commit),
9 => Some(Self::Rollback),
10 => Some(Self::Describe),
11 => Some(Self::Define),
12 => Some(Self::Bind),
13 => Some(Self::Version),
14 => Some(Self::Logon),
15 => Some(Self::Logoff),
17 => Some(Self::Oall),
147 => Some(Self::Ping),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TtcHeader {
pub function: TtcFunction,
pub seq_num: u8,
pub flags: u8,
}
impl TtcHeader {
pub const SIZE: usize = 3;
pub fn parse(data: &[u8]) -> io::Result<Self> {
if data.len() < Self::SIZE {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Insufficient data for TTC header",
));
}
let mut cursor = Cursor::new(data);
let function_code = cursor.get_u8();
let seq_num = cursor.get_u8();
let flags = cursor.get_u8();
let function = TtcFunction::from_u8(function_code)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid TTC function code: {}", function_code),
)
})?;
Ok(Self {
function,
seq_num,
flags,
})
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = BytesMut::with_capacity(Self::SIZE);
buf.put_u8(self.function as u8);
buf.put_u8(self.seq_num);
buf.put_u8(self.flags);
buf.to_vec()
}
}
#[derive(Debug, Clone)]
pub struct TtcMessage {
pub header: TtcHeader,
pub payload: Vec<u8>,
}
impl TtcMessage {
pub fn new(function: TtcFunction, payload: Vec<u8>) -> Self {
let header = TtcHeader {
function,
seq_num: 0,
flags: 0,
};
Self { header, payload }
}
pub fn parse(data: &[u8]) -> io::Result<Self> {
let header = TtcHeader::parse(data)?;
let payload = data.get(TtcHeader::SIZE..).unwrap_or(&[]).to_vec();
Ok(Self { header, payload })
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = BytesMut::new();
buf.extend_from_slice(&self.header.encode());
buf.extend_from_slice(&self.payload);
buf.to_vec()
}
}
#[derive(Debug, Clone)]
pub struct TtcParse {
pub cursor_id: u16,
pub sql: String,
pub options: u32,
}
impl TtcParse {
pub fn parse(data: &[u8]) -> io::Result<Self> {
let mut cursor = Cursor::new(data);
let cursor_id = if cursor.remaining() >= 2 {
cursor.get_u16()
} else {
1
};
let options = if cursor.remaining() >= 4 {
cursor.get_u32()
} else {
0
};
let sql_bytes: Vec<u8> = data.get(cursor.position() as usize..).unwrap_or(&[]).to_vec();
let sql = String::from_utf8_lossy(&sql_bytes).to_string();
Ok(Self {
cursor_id,
sql,
options,
})
}
}
#[derive(Debug, Clone)]
pub struct TtcExecute {
pub cursor_id: u16,
pub iterations: u32,
pub options: u32,
}
impl TtcExecute {
pub fn parse(data: &[u8]) -> io::Result<Self> {
let mut cursor = Cursor::new(data);
let cursor_id = if cursor.remaining() >= 2 {
cursor.get_u16()
} else {
1
};
let iterations = if cursor.remaining() >= 4 {
cursor.get_u32()
} else {
1
};
let options = if cursor.remaining() >= 4 {
cursor.get_u32()
} else {
0
};
Ok(Self {
cursor_id,
iterations,
options,
})
}
}
#[derive(Debug, Clone)]
pub struct TtcFetch {
pub cursor_id: u16,
pub num_rows: u32,
}
impl TtcFetch {
pub fn parse(data: &[u8]) -> io::Result<Self> {
let mut cursor = Cursor::new(data);
let cursor_id = if cursor.remaining() >= 2 {
cursor.get_u16()
} else {
1
};
let num_rows = if cursor.remaining() >= 4 {
cursor.get_u32()
} else {
1
};
Ok(Self {
cursor_id,
num_rows,
})
}
}
#[derive(Debug, Clone)]
pub struct TtcLogon {
pub username: String,
pub password: String,
pub database: String,
}
impl TtcLogon {
pub fn parse(data: &[u8]) -> io::Result<Self> {
let text = String::from_utf8_lossy(data).to_string();
let username = Self::extract_field(&text, "USER=")
.or_else(|| Self::extract_field(&text, "UID="))
.unwrap_or_else(|| "helios".to_string());
let password = Self::extract_field(&text, "PASSWORD=")
.or_else(|| Self::extract_field(&text, "PWD="))
.unwrap_or_else(|| "".to_string());
let database = Self::extract_field(&text, "SERVICE_NAME=")
.or_else(|| Self::extract_field(&text, "SID="))
.unwrap_or_else(|| "heliosdb".to_string());
Ok(Self {
username,
password,
database,
})
}
fn extract_field(text: &str, field_name: &str) -> Option<String> {
let text_upper = text.to_uppercase();
if let Some(start) = text_upper.find(field_name) {
let start = start + field_name.len();
let end = text_upper[start..]
.find([')', ' ', ';'])
.map(|pos| start + pos)
.unwrap_or(text.len());
return Some(text[start..end].to_string());
}
None
}
}
pub struct TtcResponseBuilder {
buffer: BytesMut,
}
impl TtcResponseBuilder {
pub fn new() -> Self {
Self {
buffer: BytesMut::new(),
}
}
pub fn write_header(&mut self, function: TtcFunction) {
self.buffer.put_u8(function as u8);
self.buffer.put_u8(0); self.buffer.put_u8(0); }
pub fn write_row_header(&mut self, num_columns: u16) {
self.buffer.put_u8(0x15); self.buffer.put_u16(num_columns);
}
pub fn write_column(&mut self, value: &str) {
let bytes = value.as_bytes();
self.buffer.put_u16(bytes.len() as u16);
self.buffer.extend_from_slice(bytes);
}
pub fn write_null_column(&mut self) {
self.buffer.put_u16(0xFFFF); }
pub fn write_error(&mut self, code: &str, message: &str) {
self.buffer.put_u8(0x04); self.buffer.extend_from_slice(code.as_bytes());
self.buffer.put_u8(0x00);
self.buffer.extend_from_slice(message.as_bytes());
self.buffer.put_u8(0x00);
}
pub fn write_end_of_fetch(&mut self) {
self.buffer.put_u8(0x08); }
pub fn write_command_complete(&mut self, rows_affected: u64) {
self.buffer.put_u8(0x06); self.buffer.put_u64(rows_affected);
}
pub fn build(self) -> Vec<u8> {
self.buffer.to_vec()
}
}
impl Default for TtcResponseBuilder {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub mod oracle_types {
pub const VARCHAR2: u8 = 1;
pub const NUMBER: u8 = 2;
pub const LONG: u8 = 8;
pub const DATE: u8 = 12;
pub const RAW: u8 = 23;
pub const LONG_RAW: u8 = 24;
pub const CHAR: u8 = 96;
pub const BINARY_FLOAT: u8 = 100;
pub const BINARY_DOUBLE: u8 = 101;
pub const CLOB: u8 = 112;
pub const BLOB: u8 = 113;
pub const TIMESTAMP: u8 = 180;
pub const TIMESTAMP_TZ: u8 = 181;
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_ttc_header_parse() {
let data = vec![
0x04, 0x00, 0x00, ];
let header = TtcHeader::parse(&data).unwrap();
assert_eq!(header.function, TtcFunction::Parse);
}
#[test]
fn test_ttc_message_encode_decode() {
let payload = vec![1, 2, 3, 4];
let msg = TtcMessage::new(TtcFunction::Execute, payload.clone());
let encoded = msg.encode();
let decoded = TtcMessage::parse(&encoded).unwrap();
assert_eq!(decoded.header.function, TtcFunction::Execute);
assert_eq!(decoded.payload, payload);
}
#[test]
fn test_response_builder() {
let mut builder = TtcResponseBuilder::new();
builder.write_header(TtcFunction::Execute);
builder.write_command_complete(5);
let response = builder.build();
assert!(!response.is_empty());
}
}