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_DESIRED: u16 = 319;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct ResourceLimit {
29 pub limit: &'static str,
30 pub observed: usize,
31 pub maximum: usize,
32}
33
34#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum ProtocolError {
37 #[error("truncated packet header: got {got} bytes")]
38 TruncatedHeader { got: usize },
39 #[error("invalid packet length {length}; expected at least {minimum}")]
40 InvalidPacketLength { length: usize, minimum: usize },
41 #[error("packet length {declared} exceeds available bytes {available}")]
42 IncompletePacket { declared: usize, available: usize },
43 #[error("packet length {length} exceeds TNS two-byte length field")]
44 PacketTooLarge { length: usize },
45 #[error("unsupported TNS version {version}")]
46 UnsupportedVersion { version: u16 },
47 #[error("invalid client identity field {field}: {reason}")]
48 InvalidClientIdentity {
49 field: &'static str,
50 reason: Cow<'static, str>,
51 },
52 #[error("invalid connect descriptor: {0}")]
53 InvalidConnectDescriptor(String),
54 #[error("TTC decode failed: {0}")]
55 TtcDecode(&'static str),
56 #[error("unknown TTC message type {message_type} at position {position}")]
57 UnknownMessageType { message_type: u8, position: usize },
58 #[error("protocol resource limit exceeded: {limit} observed {observed}, maximum {maximum}")]
59 ResourceLimit {
60 limit: &'static str,
61 observed: usize,
62 maximum: usize,
63 },
64 #[error("server returned Oracle error: {0}")]
65 ServerError(String),
66 #[error("server returned Oracle error: {message}")]
67 ServerErrorWithRowCount { message: String, row_count: u64 },
68 #[error("server returned Oracle error: {}", .0.message)]
69 ServerErrorInfo(Box<ServerErrorDetails>),
70 #[error("unsupported feature: {0}")]
71 UnsupportedFeature(&'static str),
72 #[error("missing authentication parameter {key}")]
73 MissingAuthParameter { key: &'static str },
74 #[error("unsupported password verifier type {verifier_type:#x}")]
75 UnsupportedVerifier { verifier_type: u32 },
76 #[error("invalid AES key length")]
77 InvalidAesKey,
78 #[error("invalid server authentication response")]
79 InvalidServerResponse,
80 #[error(
84 "DPY-8000: value of size {actual_size} exeeds maximum allowed size of \
85 {max_size} for column \"{column_name}\" of row {row_num}"
86 )]
87 ValueTooLarge {
88 actual_size: usize,
89 max_size: u32,
90 column_name: String,
91 row_num: u64,
92 },
93 #[error("DPY-8001: value for column \"{column_name}\" may not be null on row {row_num}")]
94 NullsNotAllowed { column_name: String, row_num: u64 },
95 #[error("DPY-4041: the maximum size of a Direct Path load has been exceeded")]
96 DirectPathLoadTooMuchData,
97 #[error("not implemented: {0}")]
98 NotImplemented(&'static str),
99 #[error("DPY-5004: input data is not in the OSON format: {0}")]
105 OsonNotEncoded(&'static str),
106 #[error("DPY-5006: invalid OSON data: {0}")]
107 OsonInvalid(&'static str),
108 #[error("DPY-3007: the data type {0} is not supported")]
111 OsonTypeNotSupported(&'static str),
112}
113
114impl ProtocolError {
115 pub fn resource_limit(&self) -> Option<ResourceLimit> {
116 match self {
117 Self::ResourceLimit {
118 limit,
119 observed,
120 maximum,
121 } => Some(ResourceLimit {
122 limit,
123 observed: *observed,
124 maximum: *maximum,
125 }),
126 _ => None,
127 }
128 }
129}
130
131pub type Result<T> = std::result::Result<T, ProtocolError>;
132
133#[derive(Clone, Debug, Default, Eq, PartialEq)]
136pub struct ServerErrorDetails {
137 pub message: String,
138 pub code: u32,
140 pub pos: i32,
142 pub row_count: u64,
144 pub rowid: Option<String>,
146 pub array_dml_row_counts: Option<Vec<u64>>,
149}
150
151#[derive(Clone, Debug, Eq, PartialEq)]
152pub struct ClientIdentity {
153 pub program: String,
154 pub machine: String,
155 pub osuser: String,
156 pub terminal: String,
157 pub driver_name: String,
158}
159
160impl ClientIdentity {
161 pub fn new(
162 program: impl Into<String>,
163 machine: impl Into<String>,
164 osuser: impl Into<String>,
165 terminal: impl Into<String>,
166 driver_name: impl Into<String>,
167 ) -> Result<Self> {
168 Ok(Self {
169 program: sanitize_identity_field("program", program.into())?,
170 machine: sanitize_identity_field("machine", machine.into())?,
171 osuser: sanitize_identity_field("osuser", osuser.into())?,
172 terminal: sanitize_identity_field("terminal", terminal.into())?,
173 driver_name: sanitize_identity_field("driver_name", driver_name.into())?,
174 })
175 }
176}
177
178fn sanitize_identity_field(field: &'static str, value: String) -> Result<String> {
179 let trimmed = value.trim();
180 if trimmed.is_empty() {
181 return Err(ProtocolError::InvalidClientIdentity {
182 field,
183 reason: Cow::Borrowed("value must not be empty"),
184 });
185 }
186
187 let mut out = String::with_capacity(trimmed.len().min(30));
188 for ch in trimmed.chars() {
189 if ch.is_control() {
190 return Err(ProtocolError::InvalidClientIdentity {
191 field,
192 reason: Cow::Borrowed("control characters are not allowed"),
193 });
194 }
195 if out.len() + ch.len_utf8() > 30 {
196 break;
197 }
198 out.push(ch);
199 }
200 Ok(out)
201}
202
203#[cfg(fuzzing)]
213pub mod fuzz_api {
214 use crate::wire::{BoundedReader, TtcReader};
215 use crate::Result;
216
217 pub fn fuzz_parse_server_error_info(data: &[u8]) -> Result<()> {
221 let (ttc_field_version, rest) = data.split_first().map_or((24u8, data), |(v, r)| (*v, r));
222 let mut reader = TtcReader::new(rest);
223 crate::thin::parse_server_error_info(&mut reader, ttc_field_version).map(|_| ())
224 }
225
226 pub fn fuzz_skip_server_side_piggyback(data: &[u8]) -> Result<()> {
228 let mut reader = TtcReader::new(data);
229 crate::thin::skip_server_side_piggyback(&mut reader).map(|_| ())
230 }
231
232 pub fn fuzz_scalar_codecs(data: &[u8]) {
236 let _ = crate::thin::decode_number_value(data);
237 let _ = crate::thin::decode_datetime_value(data);
238 let _ = crate::thin::decode_interval_ds(data);
239 let _ = crate::thin::decode_interval_ym(data);
240 let _ = crate::thin::decode_binary_float(data);
241 let _ = crate::thin::decode_binary_double(data);
242 }
243
244 pub fn fuzz_dbobject_image_walk(data: &[u8]) {
249 let (ops, payload) = data.split_at(data.len().min(64));
250 let mut reader = crate::thin::DbObjectPackedReader::new(payload);
251 for op in ops {
252 match op % 7 {
253 0 => {
254 let _ = reader.read_u8();
255 }
256 1 => {
257 let _ = reader.read_i32be();
258 }
259 2 => {
260 let _ = reader.read_length();
261 }
262 3 => {
263 let _ = reader.read_value_bytes();
264 }
265 4 => {
266 let _ = reader.read_header();
267 }
268 5 => {
269 let _ = reader.read_atomic_null(op & 0x80 != 0);
270 }
271 _ => {
272 let count = usize::from(*op);
273 let _ = reader.alloc_count_checked(count, 1);
274 let _: Vec<u8> = reader.with_capacity_bounded(count, 1);
275 }
276 }
277 if reader.remaining() == 0 {
278 break;
279 }
280 }
281 }
282
283 pub fn fuzz_dbobject_scalars(data: &[u8]) {
288 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
289 let dbtype_name = match selector & 0x03 {
290 0 => "DB_TYPE_VARCHAR",
291 1 => "DB_TYPE_NVARCHAR",
292 2 => "DB_TYPE_CHAR",
293 _ => "DB_TYPE_NCHAR",
294 };
295 let csfrm = if selector & 0x04 == 0 {
296 crate::thin::CS_FORM_IMPLICIT
297 } else {
298 crate::thin::CS_FORM_NCHAR
299 };
300 let locator = (selector & 0x08 != 0).then_some(payload);
301
302 let _ = crate::thin::decode_dbobject_text(payload, dbtype_name);
303 let _ = crate::thin::decode_dbobject_xmltype_text(payload);
304 let _ = crate::thin::decode_lob_text(payload, csfrm, locator);
305 let _ = crate::thin::decode_bfile_locator_name(payload);
306 let _ = crate::thin::decode_dbobject_binary_float(payload);
307 let _ = crate::thin::decode_dbobject_binary_double(payload);
308 if let Ok(text) = core::str::from_utf8(payload) {
309 let _ = crate::thin::parse_binary_integer_u32(text);
310 }
311 }
312
313 pub fn fuzz_aq_responses(data: &[u8]) {
320 use crate::thin::aq::{
321 parse_aq_array_response, parse_aq_deq_response, parse_aq_enq_response, AqPayloadKind,
322 };
323 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
324 let caps = crate::thin::ClientCapabilities {
325 ttc_field_version: 24 - (selector & 0x07),
326 ..crate::thin::ClientCapabilities::default()
327 };
328 let kind = match (selector >> 3) % 3 {
329 0 => AqPayloadKind::Raw,
330 1 => AqPayloadKind::Json,
331 _ => AqPayloadKind::Object,
332 };
333 let _ = parse_aq_enq_response(payload, caps);
334 let _ = parse_aq_deq_response(payload, caps, &kind);
335 let operation = i32::from(selector >> 6);
338 let props_count = u32::from(selector & 0x0f);
339 let _ = parse_aq_array_response(payload, caps, operation, props_count, &kind);
340 }
341
342 pub fn fuzz_subscr_responses(data: &[u8]) {
347 use crate::thin::{
348 parse_notification_stream, parse_subscribe_response, ClientCapabilities,
349 };
350 let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
351 let caps = ClientCapabilities {
352 ttc_field_version: 24 - (selector & 0x07),
353 ..ClientCapabilities::default()
354 };
355 let _ = parse_subscribe_response(payload, caps);
356 let namespace = u32::from(selector >> 4);
357 let public_qos = u32::from((selector >> 2) & 0x03);
358 let _ = parse_notification_stream(payload, namespace, public_qos, None);
359 let _ = parse_notification_stream(payload, namespace, public_qos, Some("FUZZDB"));
360 }
361
362 pub fn fuzz_connect_string(input: &str) {
375 let _ = crate::net::connectstring::parse(input);
376 let _ = crate::net::connectstring::tnsnames::fuzz_parse_file(input);
377 }
378
379 pub fn fuzz_alter_session_value(input: &str) {
386 let keys = ["current_schema", "edition", "time_zone", ""];
387 let key = keys[input.as_bytes().first().copied().unwrap_or(0) as usize % keys.len()];
388 let _ = crate::sql::parse_alter_session_value(input, key);
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn identity_fields_are_trimmed_and_bounded() {
398 let identity = ClientIdentity::new(
399 " program-name-longer-than-thirty-bytes ",
400 "machine",
401 "user",
402 "terminal",
403 "driver",
404 )
405 .expect("valid identity fields should sanitize");
406
407 assert_eq!(identity.program, "program-name-longer-than-thirt");
408 assert_eq!(identity.machine, "machine");
409 }
410
411 #[test]
412 fn identity_rejects_empty_fields() {
413 let err = ClientIdentity::new("", "machine", "user", "terminal", "driver")
414 .expect_err("empty program should be rejected");
415 assert!(matches!(
416 err,
417 ProtocolError::InvalidClientIdentity {
418 field: "program",
419 ..
420 }
421 ));
422 }
423
424 #[test]
425 fn resource_limit_accessor_returns_typed_details() {
426 let err = ProtocolError::ResourceLimit {
427 limit: "response_bytes",
428 observed: 33,
429 maximum: 32,
430 };
431 assert_eq!(
432 err.resource_limit(),
433 Some(ResourceLimit {
434 limit: "response_bytes",
435 observed: 33,
436 maximum: 32,
437 })
438 );
439 assert_eq!(ProtocolError::TtcDecode("bad").resource_limit(), None);
440 }
441}