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