Skip to main content

oracledb_protocol/net/
mod.rs

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