1#![forbid(unsafe_code)]
2
3pub mod capabilities;
4pub mod crypto;
5pub mod dpl;
6pub mod net;
7pub mod oson;
8pub mod packet;
9pub mod sql;
10pub mod thin;
11pub mod tls;
12pub mod vector;
13pub mod wire;
14
15use std::borrow::Cow;
16
17pub const PYTHON_ORACLEDB_REFERENCE_TAG: &str = "v4.0.1";
18pub const PYTHON_ORACLEDB_REFERENCE_COMMIT: &str = "3daef052904e41668bb862e6fa40f43c22a81beb";
19pub const TNS_VERSION_MIN: u16 = 300;
20pub const TNS_VERSION_MIN_ACCEPTED: u16 = 315;
29pub const TNS_VERSION_DESIRED: u16 = 319;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct ResourceLimit {
38 pub limit: &'static str,
39 pub observed: usize,
40 pub maximum: usize,
41}
42
43#[derive(Debug, thiserror::Error)]
44#[non_exhaustive]
45pub enum ProtocolError {
46 #[error("truncated packet header: got {got} bytes")]
47 TruncatedHeader { got: usize },
48 #[error("invalid packet length {length}; expected at least {minimum}")]
49 InvalidPacketLength { length: usize, minimum: usize },
50 #[error("packet length {declared} exceeds available bytes {available}")]
51 IncompletePacket { declared: usize, available: usize },
52 #[error("packet length {length} exceeds TNS two-byte length field")]
53 PacketTooLarge { length: usize },
54 #[error(
55 "server TNS protocol version {version} is below the minimum {minimum} supported by \
56 this driver (Oracle Database 12.1 wire format); connections to this database server \
57 version are not supported (mirrors python-oracledb DPY-3010)"
58 )]
59 UnsupportedVersion { version: u16, minimum: u16 },
60 #[error("invalid client identity field {field}: {reason}")]
61 InvalidClientIdentity {
62 field: &'static str,
63 reason: Cow<'static, str>,
64 },
65 #[error("invalid connect descriptor: {0}")]
66 InvalidConnectDescriptor(String),
67 #[error("TTC decode failed: {0}")]
68 TtcDecode(&'static str),
69 #[error("unknown TTC message type {message_type} at position {position}")]
70 UnknownMessageType { message_type: u8, position: usize },
71 #[error("protocol resource limit exceeded: {limit} observed {observed}, maximum {maximum}")]
72 ResourceLimit {
73 limit: &'static str,
74 observed: usize,
75 maximum: usize,
76 },
77 #[error("server returned Oracle error: {0}")]
78 ServerError(String),
79 #[error("server returned Oracle error: {message}")]
80 ServerErrorWithRowCount { message: String, row_count: u64 },
81 #[error("server returned Oracle error: {}", .0.message)]
82 ServerErrorInfo(Box<ServerErrorDetails>),
83 #[error("unsupported feature: {0}")]
84 UnsupportedFeature(&'static str),
85 #[error("missing authentication parameter {key}")]
86 MissingAuthParameter { key: &'static str },
87 #[error("unsupported password verifier type {verifier_type:#x}")]
88 UnsupportedVerifier { verifier_type: u32 },
89 #[error("invalid AES key length")]
90 InvalidAesKey,
91 #[error("invalid server authentication response")]
92 InvalidServerResponse,
93 #[error(
97 "DPY-8000: value of size {actual_size} exeeds maximum allowed size of \
98 {max_size} for column \"{column_name}\" of row {row_num}"
99 )]
100 ValueTooLarge {
101 actual_size: usize,
102 max_size: u32,
103 column_name: String,
104 row_num: u64,
105 },
106 #[error("DPY-8001: value for column \"{column_name}\" may not be null on row {row_num}")]
107 NullsNotAllowed { column_name: String, row_num: u64 },
108 #[error("DPY-4041: the maximum size of a Direct Path load has been exceeded")]
109 DirectPathLoadTooMuchData,
110 #[error("not implemented: {0}")]
111 NotImplemented(&'static str),
112 #[error("DPY-5004: input data is not in the OSON format: {0}")]
118 OsonNotEncoded(&'static str),
119 #[error("DPY-5006: invalid OSON data: {0}")]
120 OsonInvalid(&'static str),
121 #[error("DPY-3007: the data type {0} is not supported")]
124 OsonTypeNotSupported(&'static str),
125}
126
127impl ProtocolError {
128 pub fn resource_limit(&self) -> Option<ResourceLimit> {
129 match self {
130 Self::ResourceLimit {
131 limit,
132 observed,
133 maximum,
134 } => Some(ResourceLimit {
135 limit,
136 observed: *observed,
137 maximum: *maximum,
138 }),
139 _ => None,
140 }
141 }
142}
143
144pub type Result<T> = std::result::Result<T, ProtocolError>;
145
146#[derive(Clone, Debug, Default, Eq, PartialEq)]
149pub struct ServerErrorDetails {
150 pub message: String,
151 pub code: u32,
153 pub pos: i32,
155 pub row_count: u64,
157 pub rowid: Option<String>,
159 pub array_dml_row_counts: Option<Vec<u64>>,
162}
163
164#[derive(Clone, Debug, Eq, PartialEq)]
165pub struct ClientIdentity {
166 pub program: String,
167 pub machine: String,
168 pub osuser: String,
169 pub terminal: String,
170 pub driver_name: String,
171}
172
173impl ClientIdentity {
174 pub fn new(
175 program: impl Into<String>,
176 machine: impl Into<String>,
177 osuser: impl Into<String>,
178 terminal: impl Into<String>,
179 driver_name: impl Into<String>,
180 ) -> Result<Self> {
181 Ok(Self {
182 program: sanitize_identity_field("program", program.into())?,
183 machine: sanitize_identity_field("machine", machine.into())?,
184 osuser: sanitize_identity_field("osuser", osuser.into())?,
185 terminal: sanitize_identity_field("terminal", terminal.into())?,
186 driver_name: sanitize_identity_field("driver_name", driver_name.into())?,
187 })
188 }
189}
190
191fn sanitize_identity_field(field: &'static str, value: String) -> Result<String> {
192 let trimmed = value.trim();
193 if trimmed.is_empty() {
194 return Err(ProtocolError::InvalidClientIdentity {
195 field,
196 reason: Cow::Borrowed("value must not be empty"),
197 });
198 }
199
200 let mut out = String::with_capacity(trimmed.len().min(30));
201 for ch in trimmed.chars() {
202 if ch.is_control() {
203 return Err(ProtocolError::InvalidClientIdentity {
204 field,
205 reason: Cow::Borrowed("control characters are not allowed"),
206 });
207 }
208 if out.len() + ch.len_utf8() > 30 {
209 break;
210 }
211 out.push(ch);
212 }
213 Ok(out)
214}
215
216#[cfg(fuzzing)]
226pub mod fuzz_api {
227 use crate::wire::{BoundedReader, TtcReader};
228 use crate::Result;
229
230 pub fn fuzz_parse_server_error_info(data: &[u8]) -> Result<()> {
234 let (ttc_field_version, rest) = data.split_first().map_or((24u8, data), |(v, r)| (*v, r));
235 let mut reader = TtcReader::new(rest);
236 crate::thin::parse_server_error_info(&mut reader, ttc_field_version).map(|_| ())
237 }
238
239 pub fn fuzz_skip_server_side_piggyback(data: &[u8]) -> Result<()> {
241 let mut reader = TtcReader::new(data);
242 crate::thin::skip_server_side_piggyback(&mut reader).map(|_| ())
243 }
244
245 pub fn fuzz_scalar_codecs(data: &[u8]) {
249 let _ = crate::thin::decode_number_value(data);
250 let _ = crate::thin::decode_datetime_value(data);
251 let _ = crate::thin::decode_interval_ds(data);
252 let _ = crate::thin::decode_interval_ym(data);
253 let _ = crate::thin::decode_binary_float(data);
254 let _ = crate::thin::decode_binary_double(data);
255 }
256
257 pub fn fuzz_dbobject_image_walk(data: &[u8]) {
262 let (ops, payload) = data.split_at(data.len().min(64));
263 let mut reader = crate::thin::DbObjectPackedReader::new(payload);
264 for op in ops {
265 match op % 7 {
266 0 => {
267 let _ = reader.read_u8();
268 }
269 1 => {
270 let _ = reader.read_i32be();
271 }
272 2 => {
273 let _ = reader.read_length();
274 }
275 3 => {
276 let _ = reader.read_value_bytes();
277 }
278 4 => {
279 let _ = reader.read_header();
280 }
281 5 => {
282 let _ = reader.read_atomic_null(op & 0x80 != 0);
283 }
284 _ => {
285 let count = usize::from(*op);
286 let _ = reader.alloc_count_checked(count, 1);
287 let _: Vec<u8> = reader.with_capacity_bounded(count, 1);
288 }
289 }
290 if reader.remaining() == 0 {
291 break;
292 }
293 }
294 }
295
296 pub fn fuzz_dbobject_scalars(data: &[u8]) {
301 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
302 let dbtype_name = match selector & 0x03 {
303 0 => "DB_TYPE_VARCHAR",
304 1 => "DB_TYPE_NVARCHAR",
305 2 => "DB_TYPE_CHAR",
306 _ => "DB_TYPE_NCHAR",
307 };
308 let csfrm = if selector & 0x04 == 0 {
309 crate::thin::CS_FORM_IMPLICIT
310 } else {
311 crate::thin::CS_FORM_NCHAR
312 };
313 let locator = (selector & 0x08 != 0).then_some(payload);
314
315 let _ = crate::thin::decode_dbobject_text(payload, dbtype_name);
316 let _ = crate::thin::decode_dbobject_xmltype_text(payload);
317 let _ = crate::thin::decode_lob_text(payload, csfrm, locator);
318 let _ = crate::thin::decode_bfile_locator_name(payload);
319 let _ = crate::thin::decode_dbobject_binary_float(payload);
320 let _ = crate::thin::decode_dbobject_binary_double(payload);
321 if let Ok(text) = core::str::from_utf8(payload) {
322 let _ = crate::thin::parse_binary_integer_u32(text);
323 }
324 }
325
326 pub fn fuzz_aq_responses(data: &[u8]) {
333 use crate::thin::aq::{
334 parse_aq_array_response, parse_aq_deq_response, parse_aq_enq_response, AqPayloadKind,
335 };
336 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
337 let caps = crate::thin::ClientCapabilities {
338 ttc_field_version: 24 - (selector & 0x07),
339 ..crate::thin::ClientCapabilities::default()
340 };
341 let kind = match (selector >> 3) % 3 {
342 0 => AqPayloadKind::Raw,
343 1 => AqPayloadKind::Json,
344 _ => AqPayloadKind::Object,
345 };
346 let _ = parse_aq_enq_response(payload, caps);
347 let _ = parse_aq_deq_response(payload, caps, &kind);
348 let operation = i32::from(selector >> 6);
351 let props_count = u32::from(selector & 0x0f);
352 let _ = parse_aq_array_response(payload, caps, operation, props_count, &kind);
353 }
354
355 pub fn fuzz_subscr_responses(data: &[u8]) {
360 use crate::thin::{
361 parse_notification_stream, parse_subscribe_response, ClientCapabilities,
362 };
363 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
364 let caps = ClientCapabilities {
365 ttc_field_version: 24 - (selector & 0x07),
366 ..ClientCapabilities::default()
367 };
368 let _ = parse_subscribe_response(payload, caps);
369 let namespace = u32::from(selector >> 4);
370 let public_qos = u32::from((selector >> 2) & 0x03);
371 let _ = parse_notification_stream(payload, namespace, public_qos, None);
372 let _ = parse_notification_stream(payload, namespace, public_qos, Some("FUZZDB"));
373 }
374
375 pub fn fuzz_connect_string(input: &str) {
388 let _ = crate::net::connectstring::parse(input);
389 let _ = crate::net::connectstring::tnsnames::fuzz_parse_file(input);
390 }
391
392 pub fn fuzz_alter_session_value(input: &str) {
399 let keys = ["current_schema", "edition", "time_zone", ""];
400 let key = keys[input.as_bytes().first().copied().unwrap_or(0) as usize % keys.len()];
401 let _ = crate::sql::parse_alter_session_value(input, key);
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn identity_fields_are_trimmed_and_bounded() {
411 let identity = ClientIdentity::new(
412 " program-name-longer-than-thirty-bytes ",
413 "machine",
414 "user",
415 "terminal",
416 "driver",
417 )
418 .expect("valid identity fields should sanitize");
419
420 assert_eq!(identity.program, "program-name-longer-than-thirt");
421 assert_eq!(identity.machine, "machine");
422 }
423
424 #[test]
425 fn identity_rejects_empty_fields() {
426 let err = ClientIdentity::new("", "machine", "user", "terminal", "driver")
427 .expect_err("empty program should be rejected");
428 assert!(matches!(
429 err,
430 ProtocolError::InvalidClientIdentity {
431 field: "program",
432 ..
433 }
434 ));
435 }
436
437 #[test]
438 fn resource_limit_accessor_returns_typed_details() {
439 let err = ProtocolError::ResourceLimit {
440 limit: "response_bytes",
441 observed: 33,
442 maximum: 32,
443 };
444 assert_eq!(
445 err.resource_limit(),
446 Some(ResourceLimit {
447 limit: "response_bytes",
448 observed: 33,
449 maximum: 32,
450 })
451 );
452 assert_eq!(ProtocolError::TtcDecode("bad").resource_limit(), None);
453 }
454}