Skip to main content

oracledb_protocol/
lib.rs

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/// Structured details for a protocol resource-limit violation.
23///
24/// The limit names are stable policy keys from [`wire::ProtocolLimits`], so
25/// callers can classify or log the exact bound that rejected a payload without
26/// parsing the display string.
27#[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    // The next three mirror python-oracledb error numbers DPY-8000, DPY-8001
81    // and DPY-4041 so a Python-facing layer can map them one-to-one.
82    // "exeeds" reproduces the reference's spelling (errors.py ERR_VALUE_TOO_LARGE).
83    #[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    // OSON / DB_TYPE_JSON. These mirror python-oracledb error numbers so the
100    // Python-facing layer can map them one-to-one:
101    //   DPY-5004 ERR_OSON_NODE_TYPE_NOT_SUPPORTED is *not* this; 5004 is the
102    //   "not previously encoded" case (bad magic/version) and 5006 is a
103    //   structurally invalid OSON image (truncation / bad offset).
104    #[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    /// A JSON scalar node decoded to an Oracle type with no Python mapping
109    /// (e.g. INTERVAL YEAR TO MONTH). Mirrors DPY-3007 / ERR_DB_TYPE_NOT_SUPPORTED.
110    #[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/// Structured server error information parsed from the TTC error trailer
134/// (reference impl/thin/messages/base.pyx `_process_error_info`).
135#[derive(Clone, Debug, Default, Eq, PartialEq)]
136pub struct ServerErrorDetails {
137    pub message: String,
138    /// ORA error number (extended field).
139    pub code: u32,
140    /// Error position / parse offset (sb2; 0 when not reported).
141    pub pos: i32,
142    /// Server-reported row count at the time of the error.
143    pub row_count: u64,
144    /// Encoded rowid of the last affected row, if any.
145    pub rowid: Option<String>,
146    /// Row counts received before the error when
147    /// `executemany(arraydmlrowcounts=True)` was requested.
148    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/// Fuzz-only thin wrappers over `pub(crate)` decoder entry points.
204///
205/// This module is compiled **only** under `--cfg fuzzing` (set automatically
206/// by `cargo-fuzz`). It exposes the crate-internal decode functions that take
207/// adversarial server bytes — the server-error trailer parser and the
208/// `pub(crate)` scalar codecs — so the `fuzz/` targets can call them directly
209/// without widening the normal public API. Each wrapper is a zero-logic
210/// forward to the real function; the goal is to prove these never panic on
211/// malformed input (they must fail closed with a [`ProtocolError`]).
212#[cfg(fuzzing)]
213pub mod fuzz_api {
214    use crate::wire::TtcReader;
215    use crate::Result;
216
217    /// Fuzz the server-error trailer parser (`parse_server_error_info`).
218    /// `ttc_field_version` is taken from the first input byte so the fuzzer
219    /// can explore both the legacy and 20.1+ trailer layouts.
220    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    /// Fuzz the server-side piggyback skipper (`skip_server_side_piggyback`).
227    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    /// Fuzz every `pub(crate)` scalar codec that decodes raw column bytes.
233    /// Drives them all from one input so a single target covers the full
234    /// scalar surface (NUMBER, datetime, intervals, binary float/double).
235    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    /// Fuzz the Advanced Queuing response decoders (enqueue / dequeue / array).
245    /// The first input byte selects the negotiated TTC field version and the
246    /// payload kind so the fuzzer can reach the RAW / JSON / Object branches;
247    /// the rest is the adversarial server payload. All three AQ parsers must
248    /// fail closed on any malformed input (they only `read_*` from a bounded
249    /// `TtcReader`, never index raw bytes).
250    pub fn fuzz_aq_responses(data: &[u8]) {
251        use crate::thin::aq::{
252            parse_aq_array_response, parse_aq_deq_response, parse_aq_enq_response, AqPayloadKind,
253        };
254        let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
255        let caps = crate::thin::ClientCapabilities {
256            ttc_field_version: 24 - (selector & 0x07),
257            ..crate::thin::ClientCapabilities::default()
258        };
259        let kind = match (selector >> 3) % 3 {
260            0 => AqPayloadKind::Raw,
261            1 => AqPayloadKind::Json,
262            _ => AqPayloadKind::Object,
263        };
264        let _ = parse_aq_enq_response(payload, caps);
265        let _ = parse_aq_deq_response(payload, caps, &kind);
266        // `operation` and `props_count` are derived from the selector so the
267        // array decoder explores both the dequeue-array and enqueue-array shapes.
268        let operation = i32::from(selector >> 6);
269        let props_count = u32::from(selector & 0x0f);
270        let _ = parse_aq_array_response(payload, caps, operation, props_count, &kind);
271    }
272
273    /// Fuzz the subscription (CQN/AQ-notification) response + notification
274    /// stream decoders. The first input byte drives the TTC field version, the
275    /// namespace, and the QoS flags so the fuzzer reaches the OAC-record and
276    /// grouping-notification branches. Both parsers must fail closed.
277    pub fn fuzz_subscr_responses(data: &[u8]) {
278        use crate::thin::{
279            parse_notification_stream, parse_subscribe_response, ClientCapabilities,
280        };
281        let (selector, payload) = data.split_first().map_or((0u8, data), |(v, r)| (*v, r));
282        let caps = ClientCapabilities {
283            ttc_field_version: 24 - (selector & 0x07),
284            ..ClientCapabilities::default()
285        };
286        let _ = parse_subscribe_response(payload, caps);
287        let namespace = u32::from(selector >> 4);
288        let public_qos = u32::from((selector >> 2) & 0x03);
289        let _ = parse_notification_stream(payload, namespace, public_qos, None);
290        let _ = parse_notification_stream(payload, namespace, public_qos, Some("FUZZDB"));
291    }
292
293    /// Fuzz the connect-string parsers on one untrusted string: the TNS
294    /// connect-descriptor / EZConnect-Plus parser
295    /// ([`crate::net::connectstring::parse`]) and the in-memory tnsnames.ora
296    /// lexer (`tnsnames::fuzz_parse_file`).
297    ///
298    /// Both consume untrusted env / config / user input and must *never*
299    /// panic / OOM / overflow the stack — only return `Err` (or, for the
300    /// descriptor case, `Ok(None)` meaning "this is a tnsnames alias"). The
301    /// descriptor recursion-depth DoS was fixed in bead `uf8`
302    /// (`MAX_DESCRIPTOR_DEPTH`); this entry point guards that fix and hunts
303    /// siblings in the EZConnect quote/host/port lexer and the tnsnames
304    /// comment / multi-line / paren-balancing tokenizer.
305    pub fn fuzz_connect_string(input: &str) {
306        let _ = crate::net::connectstring::parse(input);
307        let _ = crate::net::connectstring::tnsnames::fuzz_parse_file(input);
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn identity_fields_are_trimmed_and_bounded() {
317        let identity = ClientIdentity::new(
318            "  program-name-longer-than-thirty-bytes  ",
319            "machine",
320            "user",
321            "terminal",
322            "driver",
323        )
324        .expect("valid identity fields should sanitize");
325
326        assert_eq!(identity.program, "program-name-longer-than-thirt");
327        assert_eq!(identity.machine, "machine");
328    }
329
330    #[test]
331    fn identity_rejects_empty_fields() {
332        let err = ClientIdentity::new("", "machine", "user", "terminal", "driver")
333            .expect_err("empty program should be rejected");
334        assert!(matches!(
335            err,
336            ProtocolError::InvalidClientIdentity {
337                field: "program",
338                ..
339            }
340        ));
341    }
342
343    #[test]
344    fn resource_limit_accessor_returns_typed_details() {
345        let err = ProtocolError::ResourceLimit {
346            limit: "response_bytes",
347            observed: 33,
348            maximum: 32,
349        };
350        assert_eq!(
351            err.resource_limit(),
352            Some(ResourceLimit {
353                limit: "response_bytes",
354                observed: 33,
355                maximum: 32,
356            })
357        );
358        assert_eq!(ProtocolError::TtcDecode("bad").resource_limit(), None);
359    }
360}