Skip to main content

oracledb_protocol/net/
mod.rs

1#![forbid(unsafe_code)]
2
3pub mod connectstring;
4
5/// `.tns-cassette` record/replay wire format (sans-I/O framing). Gated behind
6/// the `cassette` feature so the default build is byte-identical.
7#[cfg(feature = "cassette")]
8pub mod cassette;
9
10use crate::{ProtocolError, Result};
11
12/// Transport protocol for the connection (the EZConnect `protocol://` prefix).
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
14pub enum Protocol {
15    /// Plain TCP (default).
16    #[default]
17    Tcp,
18    /// TLS-encrypted TCP (TCPS); default port 2484.
19    Tcps,
20}
21
22impl Protocol {
23    /// Default listener port for this protocol.
24    #[must_use]
25    pub fn default_port(self) -> u16 {
26        match self {
27            Self::Tcp => 1521,
28            Self::Tcps => 2484,
29        }
30    }
31
32    /// Returns whether this protocol requires a TLS handshake.
33    #[must_use]
34    pub fn is_tls(self) -> bool {
35        matches!(self, Self::Tcps)
36    }
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct EasyConnect {
41    pub host: String,
42    pub port: u16,
43    pub service_name: String,
44    /// Transport protocol parsed from a `tcp://` / `tcps://` prefix (default
45    /// [`Protocol::Tcp`]).
46    pub protocol: Protocol,
47}
48
49impl From<connectstring::Protocol> for Protocol {
50    fn from(value: connectstring::Protocol) -> Self {
51        match value {
52            connectstring::Protocol::Tcp => Self::Tcp,
53            connectstring::Protocol::Tcps => Self::Tcps,
54        }
55    }
56}
57
58impl EasyConnect {
59    /// Resolves a connect string into the single primary endpoint used by the
60    /// thin connection path: host, port, service name, and transport protocol.
61    ///
62    /// This now delegates to the full [`connectstring`] parser, so it accepts
63    /// not only EZConnect / EZConnect-Plus strings but also complete TNS
64    /// connect descriptors (`(DESCRIPTION=...)`), `DESCRIPTION_LIST`s, and
65    /// multi-address `ADDRESS_LIST`s — selecting the first address that has a
66    /// host and the first description's `SERVICE_NAME`.
67    pub fn parse(input: &str) -> Result<Self> {
68        let descriptor = connectstring::parse(input)?.ok_or_else(|| {
69            ProtocolError::InvalidConnectDescriptor(format!(
70                "\"{input}\" is not a connect descriptor or EZConnect string \
71                 (it may be a tnsnames.ora alias requiring a config directory)"
72            ))
73        })?;
74
75        let address = descriptor.first_address().ok_or_else(|| {
76            ProtocolError::InvalidConnectDescriptor(
77                "connect descriptor defines no usable address (host is required)".to_string(),
78            )
79        })?;
80        let host = address.host.clone().ok_or_else(|| {
81            ProtocolError::InvalidConnectDescriptor("host is required".to_string())
82        })?;
83        let service_name = descriptor
84            .first_description()
85            .connect_data
86            .service_name
87            .clone()
88            .ok_or_else(|| {
89                ProtocolError::InvalidConnectDescriptor("service name is required".to_string())
90            })?;
91
92        Ok(Self {
93            host,
94            port: address.port,
95            service_name,
96            protocol: address.protocol.into(),
97        })
98    }
99
100    /// Parses a connect string into the full resolved [`connectstring::Descriptor`],
101    /// exposing the entire address topology and connect data (for diagnostics or
102    /// callers that need more than the single primary endpoint).
103    pub fn parse_descriptor(input: &str) -> Result<connectstring::Descriptor> {
104        connectstring::parse(input)?.ok_or_else(|| {
105            ProtocolError::InvalidConnectDescriptor(format!(
106                "\"{input}\" is not a connect descriptor or EZConnect string"
107            ))
108        })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn parses_easy_connect_with_default_port() {
118        let parsed = EasyConnect::parse("localhost/FREEPDB1")
119            .expect("default-port EZConnect descriptor should parse");
120        assert_eq!(parsed.host, "localhost");
121        assert_eq!(parsed.port, 1521);
122        assert_eq!(parsed.service_name, "FREEPDB1");
123    }
124
125    #[test]
126    fn parses_easy_connect_with_explicit_port() {
127        let parsed = EasyConnect::parse("db.example.test:1522/service")
128            .expect("explicit-port EZConnect descriptor should parse");
129        assert_eq!(parsed.host, "db.example.test");
130        assert_eq!(parsed.port, 1522);
131        assert_eq!(parsed.service_name, "service");
132        assert_eq!(parsed.protocol, Protocol::Tcp);
133    }
134
135    #[test]
136    fn parses_tcps_prefix_defaults_to_2484() {
137        let parsed = EasyConnect::parse("tcps://db.example.test/FREEPDB1")
138            .expect("tcps EZConnect descriptor should parse");
139        assert_eq!(parsed.host, "db.example.test");
140        assert_eq!(parsed.port, 2484);
141        assert_eq!(parsed.service_name, "FREEPDB1");
142        assert_eq!(parsed.protocol, Protocol::Tcps);
143        assert!(parsed.protocol.is_tls());
144    }
145
146    #[test]
147    fn parses_tcps_prefix_with_explicit_port() {
148        let parsed = EasyConnect::parse("tcps://host:2484/svc").expect("should parse");
149        assert_eq!(parsed.port, 2484);
150        assert_eq!(parsed.protocol, Protocol::Tcps);
151    }
152
153    #[test]
154    fn parses_tcp_prefix_explicitly() {
155        let parsed = EasyConnect::parse("tcp://host/svc").expect("should parse");
156        assert_eq!(parsed.port, 1521);
157        assert_eq!(parsed.protocol, Protocol::Tcp);
158    }
159
160    #[test]
161    fn parses_full_tns_descriptor_via_easy_connect() {
162        // EasyConnect::parse now delegates to the real connect-string parser,
163        // so it must resolve the first address of a full TNS descriptor.
164        let parsed = EasyConnect::parse(
165            "(DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=db.example.test)(PORT=2484))\
166             (CONNECT_DATA=(SERVICE_NAME=FREEPDB1)))",
167        )
168        .expect("full TNS descriptor should parse via EasyConnect");
169        assert_eq!(parsed.host, "db.example.test");
170        assert_eq!(parsed.port, 2484);
171        assert_eq!(parsed.service_name, "FREEPDB1");
172        assert_eq!(parsed.protocol, Protocol::Tcps);
173    }
174
175    #[test]
176    fn picks_first_address_of_address_list() {
177        let parsed = EasyConnect::parse(
178            "(DESCRIPTION=(ADDRESS_LIST=\
179             (ADDRESS=(PROTOCOL=tcp)(HOST=primary)(PORT=1521))\
180             (ADDRESS=(PROTOCOL=tcp)(HOST=standby)(PORT=1522)))\
181             (CONNECT_DATA=(SERVICE_NAME=svc)))",
182        )
183        .expect("address-list descriptor should parse");
184        assert_eq!(parsed.host, "primary");
185        assert_eq!(parsed.port, 1521);
186    }
187}