1#![forbid(unsafe_code)]
2#![cfg_attr(test, allow(clippy::unwrap_used))]
4
5pub mod capabilities;
6pub mod crypto;
7pub mod dpl;
8pub mod net;
9pub mod oson;
10pub mod packet;
11pub mod sql;
12pub mod thin;
13pub mod tls;
14pub mod vector;
15pub mod wire;
16
17use std::borrow::Cow;
18
19pub const PYTHON_ORACLEDB_REFERENCE_TAG: &str = "v4.0.1";
20pub const PYTHON_ORACLEDB_REFERENCE_COMMIT: &str = "3daef052904e41668bb862e6fa40f43c22a81beb";
21pub const TNS_VERSION_MIN: u16 = 300;
22pub const TNS_VERSION_MIN_ACCEPTED: u16 = 315;
31pub const TNS_VERSION_DESIRED: u16 = 319;
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct ResourceLimit {
40 pub limit: &'static str,
41 pub observed: usize,
42 pub maximum: usize,
43}
44
45#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum ProtocolError {
48 #[error("truncated packet header: got {got} bytes")]
49 TruncatedHeader { got: usize },
50 #[error("invalid packet length {length}; expected at least {minimum}")]
51 InvalidPacketLength { length: usize, minimum: usize },
52 #[error("packet length {declared} exceeds available bytes {available}")]
53 IncompletePacket { declared: usize, available: usize },
54 #[error("packet length {length} exceeds TNS two-byte length field")]
55 PacketTooLarge { length: usize },
56 #[error(
57 "server TNS protocol version {version} is below the minimum {minimum} supported by \
58 this driver (Oracle Database 12.1 wire format); connections to this database server \
59 version are not supported (mirrors python-oracledb DPY-3010)"
60 )]
61 UnsupportedVersion { version: u16, minimum: u16 },
62 #[error("invalid client identity field {field}: {reason}")]
63 InvalidClientIdentity {
64 field: &'static str,
65 reason: Cow<'static, str>,
66 },
67 #[error("invalid connect descriptor: {0}")]
68 InvalidConnectDescriptor(String),
69 #[error("TTC decode failed: {0}")]
70 TtcDecode(&'static str),
71 #[error("unknown TTC message type {message_type} at position {position}")]
72 UnknownMessageType { message_type: u8, position: usize },
73 #[error("protocol resource limit exceeded: {limit} observed {observed}, maximum {maximum}")]
74 ResourceLimit {
75 limit: &'static str,
76 observed: usize,
77 maximum: usize,
78 },
79 #[error("server returned Oracle error: {0}")]
80 ServerError(String),
81 #[error("server returned Oracle error: {message}")]
82 ServerErrorWithRowCount { message: String, row_count: u64 },
83 #[error("server returned Oracle error: {}", .0.message)]
84 ServerErrorInfo(Box<ServerErrorDetails>),
85 #[error("unsupported feature: {0}")]
86 UnsupportedFeature(&'static str),
87 #[error("missing authentication parameter {key}")]
88 MissingAuthParameter { key: &'static str },
89 #[error("unsupported password verifier type {verifier_type:#x}")]
90 UnsupportedVerifier { verifier_type: u32 },
91 #[error("invalid AES key length")]
92 InvalidAesKey,
93 #[error("invalid server authentication response")]
94 InvalidServerResponse,
95 #[error(
99 "DPY-8000: value of size {actual_size} exeeds maximum allowed size of \
100 {max_size} for column \"{column_name}\" of row {row_num}"
101 )]
102 ValueTooLarge {
103 actual_size: usize,
104 max_size: u32,
105 column_name: String,
106 row_num: u64,
107 },
108 #[error("DPY-8001: value for column \"{column_name}\" may not be null on row {row_num}")]
109 NullsNotAllowed { column_name: String, row_num: u64 },
110 #[error("DPY-4041: the maximum size of a Direct Path load has been exceeded")]
111 DirectPathLoadTooMuchData,
112 #[error("not implemented: {0}")]
113 NotImplemented(&'static str),
114 #[error("DPY-5004: input data is not in the OSON format: {0}")]
120 OsonNotEncoded(&'static str),
121 #[error("DPY-5006: invalid OSON data: {0}")]
122 OsonInvalid(&'static str),
123 #[error("DPY-3007: the data type {0} is not supported")]
126 OsonTypeNotSupported(&'static str),
127}
128
129impl ProtocolError {
130 pub fn resource_limit(&self) -> Option<ResourceLimit> {
131 match self {
132 Self::ResourceLimit {
133 limit,
134 observed,
135 maximum,
136 } => Some(ResourceLimit {
137 limit,
138 observed: *observed,
139 maximum: *maximum,
140 }),
141 _ => None,
142 }
143 }
144}
145
146pub type Result<T> = std::result::Result<T, ProtocolError>;
147
148#[derive(Clone, Debug, Default, Eq, PartialEq)]
151pub struct ServerErrorDetails {
152 pub message: String,
153 pub code: u32,
155 pub pos: i32,
157 pub row_count: u64,
159 pub rowid: Option<String>,
161 pub array_dml_row_counts: Option<Vec<u64>>,
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct ClientIdentity {
168 pub program: String,
169 pub machine: String,
170 pub osuser: String,
171 pub terminal: String,
172 pub driver_name: String,
173}
174
175impl ClientIdentity {
176 pub fn new(
177 program: impl Into<String>,
178 machine: impl Into<String>,
179 osuser: impl Into<String>,
180 terminal: impl Into<String>,
181 driver_name: impl Into<String>,
182 ) -> Result<Self> {
183 Ok(Self {
184 program: sanitize_identity_field("program", program.into())?,
185 machine: sanitize_identity_field("machine", machine.into())?,
186 osuser: sanitize_identity_field("osuser", osuser.into())?,
187 terminal: sanitize_identity_field("terminal", terminal.into())?,
188 driver_name: sanitize_identity_field("driver_name", driver_name.into())?,
189 })
190 }
191}
192
193fn sanitize_identity_field(field: &'static str, value: String) -> Result<String> {
194 let trimmed = value.trim();
195 if trimmed.is_empty() {
196 return Err(ProtocolError::InvalidClientIdentity {
197 field,
198 reason: Cow::Borrowed("value must not be empty"),
199 });
200 }
201
202 let mut out = String::with_capacity(trimmed.len().min(30));
203 for ch in trimmed.chars() {
204 if ch.is_control() {
205 return Err(ProtocolError::InvalidClientIdentity {
206 field,
207 reason: Cow::Borrowed("control characters are not allowed"),
208 });
209 }
210 if out.len() + ch.len_utf8() > 30 {
211 break;
212 }
213 out.push(ch);
214 }
215 Ok(out)
216}
217
218#[cfg(fuzzing)]
228pub mod fuzz_api {
229 use crate::wire::{BoundedReader, TtcReader};
230 use crate::Result;
231
232 pub fn fuzz_parse_server_error_info(data: &[u8]) -> Result<()> {
236 let (ttc_field_version, rest) = data.split_first().map_or((24u8, data), |(v, r)| (*v, r));
237 let mut reader = TtcReader::new(rest);
238 crate::thin::parse_server_error_info(&mut reader, ttc_field_version).map(|_| ())
239 }
240
241 pub fn fuzz_skip_server_side_piggyback(data: &[u8]) -> Result<()> {
243 let mut reader = TtcReader::new(data);
244 crate::thin::skip_server_side_piggyback(&mut reader).map(|_| ())
245 }
246
247 pub fn fuzz_scalar_codecs(data: &[u8]) {
251 let _ = crate::thin::decode_number_value(data);
252 let _ = crate::thin::decode_datetime_value(data);
253 let _ = crate::thin::decode_interval_ds(data);
254 let _ = crate::thin::decode_interval_ym(data);
255 let _ = crate::thin::decode_binary_float(data);
256 let _ = crate::thin::decode_binary_double(data);
257 }
258
259 pub fn fuzz_dbobject_image_walk(data: &[u8]) {
264 let (ops, payload) = data.split_at(data.len().min(64));
265 let mut reader = crate::thin::DbObjectPackedReader::new(payload);
266 for op in ops {
267 match op % 7 {
268 0 => {
269 let _ = reader.read_u8();
270 }
271 1 => {
272 let _ = reader.read_i32be();
273 }
274 2 => {
275 let _ = reader.read_length();
276 }
277 3 => {
278 let _ = reader.read_value_bytes();
279 }
280 4 => {
281 let _ = reader.read_header();
282 }
283 5 => {
284 let _ = reader.read_atomic_null(op & 0x80 != 0);
285 }
286 _ => {
287 let count = usize::from(*op);
288 let _ = reader.alloc_count_checked(count, 1);
289 let _: Vec<u8> = reader.with_capacity_bounded(count, 1);
290 }
291 }
292 if reader.remaining() == 0 {
293 break;
294 }
295 }
296 }
297
298 pub fn fuzz_dbobject_scalars(data: &[u8]) {
303 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
304 let dbtype_name = match selector & 0x03 {
305 0 => "DB_TYPE_VARCHAR",
306 1 => "DB_TYPE_NVARCHAR",
307 2 => "DB_TYPE_CHAR",
308 _ => "DB_TYPE_NCHAR",
309 };
310 let csfrm = if selector & 0x04 == 0 {
311 crate::thin::CS_FORM_IMPLICIT
312 } else {
313 crate::thin::CS_FORM_NCHAR
314 };
315 let locator = (selector & 0x08 != 0).then_some(payload);
316
317 let _ = crate::thin::decode_dbobject_text(payload, dbtype_name);
318 let _ = crate::thin::decode_dbobject_xmltype_text(payload);
319 let _ = crate::thin::decode_lob_text(payload, csfrm, locator);
320 let _ = crate::thin::decode_bfile_locator_name(payload);
321 let _ = crate::thin::decode_dbobject_binary_float(payload);
322 let _ = crate::thin::decode_dbobject_binary_double(payload);
323 if let Ok(text) = core::str::from_utf8(payload) {
324 let _ = crate::thin::parse_binary_integer_u32(text);
325 }
326
327 if let Ok(type_name) = core::str::from_utf8(data) {
333 let precision = (selector & 0x10 != 0).then_some((selector & 0x0f) as i8);
334 let scale = (selector & 0x20 != 0).then_some(((selector >> 2) & 0x0f) as i8);
335 let _ = crate::thin::public_dbtype_name_from_oracle_type_name(type_name);
336 let _ = crate::thin::dbobject_attr_precision_scale(type_name, precision, scale);
337 let _ = crate::thin::dbobject_attr_public_precision_scale(type_name, precision, scale);
338 }
339 }
340
341 pub fn fuzz_aq_responses(data: &[u8]) {
348 use crate::thin::aq::{
349 parse_aq_array_response, parse_aq_deq_response, parse_aq_enq_response, AqPayloadKind,
350 };
351 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
352 let caps = crate::thin::ClientCapabilities {
353 ttc_field_version: 24 - (selector & 0x07),
354 ..crate::thin::ClientCapabilities::default()
355 };
356 let kind = match (selector >> 3) % 3 {
357 0 => AqPayloadKind::Raw,
358 1 => AqPayloadKind::Json,
359 _ => AqPayloadKind::Object,
360 };
361 let _ = parse_aq_enq_response(payload, caps);
362 let _ = parse_aq_deq_response(payload, caps, &kind);
363 let operation = i32::from(selector >> 6);
366 let props_count = u32::from(selector & 0x0f);
367 let _ = parse_aq_array_response(payload, caps, operation, props_count, &kind);
368 }
369
370 pub fn fuzz_subscr_responses(data: &[u8]) {
375 use crate::thin::{
376 parse_notification_stream, parse_subscribe_response, ClientCapabilities,
377 };
378 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
379 let caps = ClientCapabilities {
380 ttc_field_version: 24 - (selector & 0x07),
381 ..ClientCapabilities::default()
382 };
383 let _ = parse_subscribe_response(payload, caps);
384 let namespace = u32::from(selector >> 4);
385 let public_qos = u32::from((selector >> 2) & 0x03);
386 let _ = parse_notification_stream(payload, namespace, public_qos, None);
387 let _ = parse_notification_stream(payload, namespace, public_qos, Some("FUZZDB"));
388 }
389
390 pub fn fuzz_connect_string(input: &str) {
403 let _ = crate::net::connectstring::parse(input);
404 let _ = crate::net::connectstring::tnsnames::fuzz_parse_file(input);
405 }
406
407 pub fn fuzz_alter_session_value(input: &str) {
414 let keys = ["current_schema", "edition", "time_zone", ""];
415 let key = keys[input.as_bytes().first().copied().unwrap_or(0) as usize % keys.len()];
416 let _ = crate::sql::parse_alter_session_value(input, key);
417 }
418
419 pub fn fuzz_sql_statement_surface(data: &[u8]) {
437 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
438 let Ok(statement) = core::str::from_utf8(payload) else {
439 return;
440 };
441
442 let _ = crate::sql::scan_bind_names(statement);
443 let _ = crate::sql::unique_bind_names(statement);
444 let _ = crate::sql::bind_names_per_occurrence(statement);
445 let _ = crate::sql::plsql_output_bind_names(statement);
446 let _ = crate::sql::plsql_assignment_bind_names(statement);
447 let _ = crate::sql::returning_bind_names(statement);
448 let _ = crate::sql::dml_returning_single_bind_name(statement);
449 let _ = crate::sql::rewrite_dml_returning_projection(statement, "ATTR");
450 let _ = crate::sql::plsql_function_return_bind_name(statement);
451 let _ = crate::sql::statement_is_plsql(statement);
452 let _ = crate::sql::statement_is_ddl(statement);
453 let _ = crate::sql::statement_is_dml(statement);
454 let _ = crate::sql::simple_sql_identifier(statement);
455 let _ = crate::sql::replace_bind_placeholder(statement, "value", statement);
459 let _ = crate::sql::replace_input_bind_placeholder(statement, "value", statement);
460
461 let name_len = payload.len().min(usize::from(selector).max(1));
466 if let Some(name) = payload
467 .get(..name_len)
468 .and_then(|slice| core::str::from_utf8(slice).ok())
469 {
470 let _ = crate::sql::is_quoted_bind_name(name);
471 let _ = crate::sql::public_bind_name(name);
472 let _ = crate::sql::bind_names_equal(name, statement);
473 let _ = crate::sql::bind_name_matches_key(name, statement);
474 let _ = crate::sql::generated_object_attr_bind_name(name, statement);
475 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn identity_fields_are_trimmed_and_bounded() {
485 let identity = ClientIdentity::new(
486 " program-name-longer-than-thirty-bytes ",
487 "machine",
488 "user",
489 "terminal",
490 "driver",
491 )
492 .expect("valid identity fields should sanitize");
493
494 assert_eq!(identity.program, "program-name-longer-than-thirt");
495 assert_eq!(identity.machine, "machine");
496 }
497
498 #[test]
499 fn identity_rejects_empty_fields() {
500 let err = ClientIdentity::new("", "machine", "user", "terminal", "driver")
501 .expect_err("empty program should be rejected");
502 assert!(matches!(
503 err,
504 ProtocolError::InvalidClientIdentity {
505 field: "program",
506 ..
507 }
508 ));
509 }
510
511 #[test]
512 fn resource_limit_accessor_returns_typed_details() {
513 let err = ProtocolError::ResourceLimit {
514 limit: "response_bytes",
515 observed: 33,
516 maximum: 32,
517 };
518 assert_eq!(
519 err.resource_limit(),
520 Some(ResourceLimit {
521 limit: "response_bytes",
522 observed: 33,
523 maximum: 32,
524 })
525 );
526 assert_eq!(ProtocolError::TtcDecode("bad").resource_limit(), None);
527 }
528}