Skip to main content

dicom_ul/
address.rs

1//! Data types for addresses to nodes in DICOM networks.
2//!
3//! This module provides the definitions for [`FullAeAddr`] and [`AeAddr`],
4//! which enable consumers to couple a socket address with an expected
5//! application entity (AE) title.
6//!
7//! The syntax is `«ae_title»@«network_address»:«port»`,
8//! which works not only with IPv4 and IPv6 addresses,
9//! but also with domain names.
10use snafu::{ensure, AsErrorSource, ResultExt, Snafu};
11use std::{
12    convert::TryFrom,
13    net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs},
14    str::FromStr,
15};
16
17/// A specification for a full address to the target SCP:
18/// an application entity title, plus a generic  address,
19/// typically a socket address.
20///
21/// These addresses can be serialized and parsed
22/// with the syntax `{ae_title}@{address}`,
23/// where the socket address is parsed according to
24/// the expectations of the parameter type `T`.
25///
26/// For the version of the struct without a mandatory AE title,
27/// see [`AeAddr`].
28///
29/// # Example
30///
31/// ```
32/// # use dicom_ul::FullAeAddr;
33/// # use std::net::SocketAddr;
34/// #
35/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
36/// # // socket address can be a string
37/// let addr: FullAeAddr<String> = "SCP-STORAGE@127.0.0.1:104".parse()?;
38/// assert_eq!(addr.ae_title(), "SCP-STORAGE");
39/// assert_eq!(addr.socket_addr(), "127.0.0.1:104");
40/// # // or anything else which can be parsed into a socket address
41/// let addr: FullAeAddr<SocketAddr> = "SCP-STORAGE@127.0.0.1:104".parse()?;
42/// assert_eq!(addr.ae_title(), "SCP-STORAGE");
43/// assert_eq!(addr.socket_addr(), &SocketAddr::from(([127, 0, 0, 1], 104)));
44/// assert_eq!(&addr.to_string(), "SCP-STORAGE@127.0.0.1:104");
45/// # Ok(())
46/// # }
47/// ```
48#[derive(Debug, Clone, Eq, Hash, PartialEq)]
49pub struct FullAeAddr<T> {
50    ae_title: String,
51    socket_addr: T,
52}
53
54impl<T> FullAeAddr<T> {
55    /// Create an AE address from its bare constituent parts.
56    pub fn new(ae_title: impl Into<String>, socket_addr: T) -> Self {
57        FullAeAddr {
58            ae_title: ae_title.into(),
59            socket_addr,
60        }
61    }
62
63    /// Retrieve the application entity title portion.
64    pub fn ae_title(&self) -> &str {
65        &self.ae_title
66    }
67
68    /// Retrieve the network address portion.
69    pub fn socket_addr(&self) -> &T {
70        &self.socket_addr
71    }
72
73    /// Convert the full address into its constituent parts.
74    pub fn into_parts(self) -> (String, T) {
75        (self.ae_title, self.socket_addr)
76    }
77}
78
79impl<T> From<(String, T)> for FullAeAddr<T> {
80    fn from((ae_title, socket_addr): (String, T)) -> Self {
81        Self::new(ae_title, socket_addr)
82    }
83}
84
85/// A error which occurred when parsing an AE address.
86#[derive(Debug, Clone, Eq, PartialEq, Snafu)]
87pub enum ParseAeAddressError<E>
88where
89    E: std::fmt::Debug + AsErrorSource,
90{
91    /// Missing `@` in full AE address
92    MissingPart,
93
94    /// Could not parse network socket address
95    ParseSocketAddress { source: E },
96}
97
98impl<T> FromStr for FullAeAddr<T>
99where
100    T: FromStr,
101    T::Err: std::fmt::Debug + AsErrorSource,
102{
103    type Err = ParseAeAddressError<<T as FromStr>::Err>;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        // !!! there should be a way to escape the `@`
107        if let Some((ae_title, addr)) = s.split_once('@') {
108            ensure!(!ae_title.is_empty(), MissingPartSnafu);
109            Ok(FullAeAddr {
110                ae_title: ae_title.to_string(),
111                socket_addr: addr.parse().context(ParseSocketAddressSnafu)?,
112            })
113        } else {
114            Err(ParseAeAddressError::MissingPart)
115        }
116    }
117}
118
119impl<T> ToSocketAddrs for FullAeAddr<T>
120where
121    T: ToSocketAddrs,
122{
123    type Iter = T::Iter;
124
125    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
126        self.socket_addr.to_socket_addrs()
127    }
128}
129
130impl<T> std::fmt::Display for FullAeAddr<T>
131where
132    T: std::fmt::Display,
133{
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.write_str(&self.ae_title.replace('@', "\\@"))?;
136        f.write_str("@")?;
137        std::fmt::Display::fmt(&self.socket_addr, f)
138    }
139}
140
141/// A specification for an address to the target SCP:
142/// a generic network socket address
143/// which may also include an application entity title.
144///
145/// These addresses can be serialized and parsed
146/// with the syntax `{ae_title}@{address}`,
147/// where the socket address is parsed according to
148/// the expectations of the parameter type `T`.
149///
150/// For the version of the struct in which the AE title part is mandatory,
151/// see [`FullAeAddr`].
152///
153/// # Example
154///
155/// ```
156/// # use dicom_ul::{AeAddr, FullAeAddr};
157/// # use std::net::{SocketAddr, SocketAddrV4};
158/// #
159/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
160/// let addr: AeAddr<SocketAddrV4> = "SCP-STORAGE@127.0.0.1:104".parse()?;
161/// assert_eq!(addr.ae_title(), Some("SCP-STORAGE"));
162/// assert_eq!(addr.socket_addr(), &SocketAddrV4::new([127, 0, 0, 1].into(), 104));
163/// assert_eq!(&addr.to_string(), "SCP-STORAGE@127.0.0.1:104");
164///
165/// // AE title can be missing
166/// let addr: AeAddr<String> = "192.168.1.99:1045".parse()?;
167/// assert_eq!(addr.ae_title(), None);
168/// // but can be provided later
169/// let full_addr: FullAeAddr<_> = addr.with_ae_title("SCP-QUERY");
170/// assert_eq!(full_addr.ae_title(), "SCP-QUERY");
171/// assert_eq!(&full_addr.to_string(), "SCP-QUERY@192.168.1.99:1045");
172/// # Ok(())
173/// # }
174/// ```
175#[derive(Debug, Clone, Eq, Hash, PartialEq)]
176pub struct AeAddr<T> {
177    ae_title: Option<String>,
178    socket_addr: T,
179}
180
181impl<T> AeAddr<T> {
182    /// Create an AE address from its bare constituent parts.
183    pub fn new(ae_title: impl Into<String>, socket_addr: T) -> Self {
184        AeAddr {
185            ae_title: Some(ae_title.into()),
186            socket_addr,
187        }
188    }
189
190    /// Create an address with a missing AE title.
191    pub fn new_socket_addr(socket_addr: T) -> Self {
192        AeAddr {
193            ae_title: None,
194            socket_addr,
195        }
196    }
197
198    /// Retrieve the application entity title portion, if present.
199    pub fn ae_title(&self) -> Option<&str> {
200        self.ae_title.as_deref()
201    }
202
203    /// Retrieve the socket address portion.
204    pub fn socket_addr(&self) -> &T {
205        &self.socket_addr
206    }
207
208    /// Create a new address with the full application entity target,
209    /// discarding any potentially existing AE title.
210    pub fn with_ae_title(self, ae_title: impl Into<String>) -> FullAeAddr<T> {
211        FullAeAddr {
212            ae_title: ae_title.into(),
213            socket_addr: self.socket_addr,
214        }
215    }
216
217    /// Create a new address with the full application entity target,
218    /// using the given AE title if it is missing.
219    pub fn with_default_ae_title(self, ae_title: impl Into<String>) -> FullAeAddr<T> {
220        FullAeAddr {
221            ae_title: self.ae_title.unwrap_or_else(|| ae_title.into()),
222            socket_addr: self.socket_addr,
223        }
224    }
225
226    /// Convert the address into its constituent parts.
227    pub fn into_parts(self) -> (Option<String>, T) {
228        (self.ae_title, self.socket_addr)
229    }
230}
231
232/// This conversion provides a socket address without an AE title.
233impl From<SocketAddr> for AeAddr<SocketAddr> {
234    fn from(socket_addr: SocketAddr) -> Self {
235        AeAddr {
236            ae_title: None,
237            socket_addr,
238        }
239    }
240}
241
242/// This conversion provides an IPv4 socket address without an AE title.
243impl From<SocketAddrV4> for AeAddr<SocketAddrV4> {
244    fn from(socket_addr: SocketAddrV4) -> Self {
245        AeAddr {
246            ae_title: None,
247            socket_addr,
248        }
249    }
250}
251
252/// This conversion provides an IPv6 socket address without an AE title.
253impl From<SocketAddrV6> for AeAddr<SocketAddrV6> {
254    fn from(socket_addr: SocketAddrV6) -> Self {
255        AeAddr {
256            ae_title: None,
257            socket_addr,
258        }
259    }
260}
261
262impl<T> From<FullAeAddr<T>> for AeAddr<T> {
263    fn from(full: FullAeAddr<T>) -> Self {
264        AeAddr {
265            ae_title: Some(full.ae_title),
266            socket_addr: full.socket_addr,
267        }
268    }
269}
270
271impl<T> FromStr for AeAddr<T>
272where
273    T: FromStr,
274{
275    type Err = <T as FromStr>::Err;
276
277    fn from_str(s: &str) -> Result<Self, Self::Err> {
278        // !!! there should be a way to escape the `@`
279        if let Some((ae_title, address)) = s.split_once('@') {
280            Ok(AeAddr {
281                ae_title: Some(ae_title)
282                    .filter(|s| !s.is_empty())
283                    .map(|s| s.to_string()),
284                socket_addr: address.parse()?,
285            })
286        } else {
287            Ok(AeAddr {
288                ae_title: None,
289                socket_addr: s.parse()?,
290            })
291        }
292    }
293}
294
295#[allow(unknown_lints)]
296#[allow(clippy::infallible_try_from)]
297impl<'a> TryFrom<&'a str> for AeAddr<String> {
298    type Error = <AeAddr<String> as FromStr>::Err;
299
300    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
301        s.parse()
302    }
303}
304
305impl<T> ToSocketAddrs for AeAddr<T>
306where
307    T: ToSocketAddrs,
308{
309    type Iter = T::Iter;
310
311    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
312        self.socket_addr.to_socket_addrs()
313    }
314}
315
316impl<T> std::fmt::Display for AeAddr<T>
317where
318    T: std::fmt::Display,
319{
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        let socket_addr = self.socket_addr.to_string();
322        if let Some(ae_title) = &self.ae_title {
323            f.write_str(&ae_title.replace('@', "\\@"))?;
324            f.write_str("@")?;
325        } else if socket_addr.contains('@') {
326            // if formatted socket address contains a `@`,
327            // we need to start the output with `@`
328            // so that the start of the socket address
329            // is not interpreted as an AE title
330            f.write_str("@")?;
331        }
332
333        std::fmt::Display::fmt(&socket_addr, f)
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn ae_addr_parse() {
343        // socket address can be a string
344        let addr: FullAeAddr<String> = "SCP-STORAGE@127.0.0.1:104".parse().unwrap();
345        assert_eq!(addr.ae_title(), "SCP-STORAGE");
346        assert_eq!(addr.socket_addr(), "127.0.0.1:104");
347
348        // or anything else which can be parsed into a socket address
349        let addr: FullAeAddr<SocketAddr> = "SCP_STORAGE@127.0.0.1:104".parse().unwrap();
350        assert_eq!(addr.ae_title(), "SCP_STORAGE");
351        assert_eq!(addr.socket_addr(), &SocketAddr::from(([127, 0, 0, 1], 104)));
352        assert_eq!(&addr.to_string(), "SCP_STORAGE@127.0.0.1:104");
353
354        // IPv4 socket address
355        let addr: FullAeAddr<SocketAddrV4> = "MAMMOSTORE@10.0.0.11:104".parse().unwrap();
356        assert_eq!(addr.ae_title(), "MAMMOSTORE");
357        assert_eq!(
358            addr.socket_addr(),
359            &SocketAddrV4::new([10, 0, 0, 11].into(), 104)
360        );
361        assert_eq!(&addr.to_string(), "MAMMOSTORE@10.0.0.11:104");
362    }
363
364    /// test addresses without an AE title
365    #[test]
366    fn ae_addr_parse_no_ae() {
367        // should fail
368        let res = FullAeAddr::<String>::from_str("pacs.hospital.example.com:104");
369        assert!(matches!(res, Err(ParseAeAddressError::MissingPart)));
370        // should also fail (AE title can't be empty)
371        let res = FullAeAddr::<String>::from_str("@pacs.hospital.example.com:104");
372        assert!(matches!(res, Err(ParseAeAddressError::MissingPart)));
373
374        // should return an ae addr with no AE title
375        let addr: AeAddr<String> = "pacs.hospital.example.com:104".parse().unwrap();
376        assert_eq!(addr.ae_title(), None);
377        assert_eq!(addr.socket_addr(), "pacs.hospital.example.com:104");
378        // should also return an ae addr with no AE title
379        let addr: AeAddr<String> = "@pacs.hospital.example.com:104".parse().unwrap();
380        assert_eq!(addr.ae_title(), None);
381        assert_eq!(addr.socket_addr(), "pacs.hospital.example.com:104");
382    }
383
384    #[test]
385    fn ae_addr_parse_weird_scenarios() {
386        // can parse addresses with multiple @'s
387        let addr: FullAeAddr<String> = "ABC@DICOM@pacs.archive.example.com:104".parse().unwrap();
388        assert_eq!(addr.ae_title(), "ABC");
389        assert_eq!(addr.socket_addr(), "DICOM@pacs.archive.example.com:104");
390        assert_eq!(&addr.to_string(), "ABC@DICOM@pacs.archive.example.com:104");
391    }
392}