dicom_ul/association/
server.rs

1//! Association acceptor module
2//!
3//! The module provides an abstraction for a DICOM association
4//! in which this application entity listens to incoming association requests.
5//! See [`ServerAssociationOptions`]
6//! for details and examples on how to create an association.
7use std::{borrow::Cow, io::Write, net::TcpStream};
8
9use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
10use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
11use snafu::{ensure, Backtrace, ResultExt, Snafu};
12
13use crate::{
14    pdu::{
15        reader::{read_pdu, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE},
16        writer::write_pdu,
17        AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ,
18        AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ,
19        Pdu, PresentationContextResult, PresentationContextResultReason, UserIdentity,
20        UserVariableItem,
21    },
22    IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME,
23};
24
25use super::{
26    pdata::{PDataReader, PDataWriter},
27    uid::trim_uid,
28};
29
30#[derive(Debug, Snafu)]
31#[non_exhaustive]
32pub enum Error {
33    /// missing at least one abstract syntax to accept negotiations
34    MissingAbstractSyntax { backtrace: Backtrace },
35
36    /// failed to receive association request
37    ReceiveRequest {
38        #[snafu(backtrace)]
39        source: crate::pdu::reader::Error,
40    },
41
42    /// failed to send association response
43    SendResponse {
44        #[snafu(backtrace)]
45        source: crate::pdu::writer::Error,
46    },
47
48    /// failed to prepare PDU
49    Send {
50        #[snafu(backtrace)]
51        source: crate::pdu::writer::Error,
52    },
53
54    /// failed to send PDU over the wire
55    WireSend {
56        source: std::io::Error,
57        backtrace: Backtrace,
58    },
59
60    /// failed to receive PDU
61    Receive {
62        #[snafu(backtrace)]
63        source: crate::pdu::reader::Error,
64    },
65
66    #[snafu(display("unexpected request from SCU `{:?}`", pdu))]
67    #[non_exhaustive]
68    UnexpectedRequest {
69        /// the PDU obtained from the server
70        pdu: Box<Pdu>,
71    },
72
73    #[snafu(display("unknown request from SCU `{:?}`", pdu))]
74    #[non_exhaustive]
75    UnknownRequest {
76        /// the PDU obtained from the server, of variant Unknown
77        pdu: Box<Pdu>,
78    },
79
80    /// association rejected
81    Rejected { backtrace: Backtrace },
82
83    /// association aborted
84    Aborted { backtrace: Backtrace },
85
86    #[snafu(display(
87        "PDU is too large ({} bytes) to be sent to the remote application entity",
88        length
89    ))]
90    #[non_exhaustive]
91    SendTooLongPdu { length: usize, backtrace: Backtrace },
92}
93
94pub type Result<T, E = Error> = std::result::Result<T, E>;
95
96/// Common interface for application entity access control policies.
97///
98/// Existing implementations include [`AcceptAny`] and [`AcceptCalledAeTitle`],
99/// but users are free to implement their own.
100pub trait AccessControl {
101    /// Obtain the decision of whether to accept an incoming association request
102    /// based on the recorded application entity titles and/or user identity.
103    ///
104    /// Returns Ok(()) if the requester node should be given clearance.
105    /// Otherwise, a concrete association RJ service user reason is given.
106    fn check_access(
107        &self,
108        this_ae_title: &str,
109        calling_ae_title: &str,
110        called_ae_title: &str,
111        user_identity: Option<&UserIdentity>,
112    ) -> Result<(), AssociationRJServiceUserReason>;
113}
114
115/// An access control rule that accepts any incoming association request.
116#[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)]
117pub struct AcceptAny;
118
119impl AccessControl for AcceptAny {
120    fn check_access(
121        &self,
122        _this_ae_title: &str,
123        _calling_ae_title: &str,
124        _called_ae_title: &str,
125        _user_identity: Option<&UserIdentity>,
126    ) -> Result<(), AssociationRJServiceUserReason> {
127        Ok(())
128    }
129}
130
131/// An access control rule that accepts association requests
132/// that match the called AE title with the node's AE title.
133#[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)]
134pub struct AcceptCalledAeTitle;
135
136impl AccessControl for AcceptCalledAeTitle {
137    fn check_access(
138        &self,
139        this_ae_title: &str,
140        _calling_ae_title: &str,
141        called_ae_title: &str,
142        _user_identity: Option<&UserIdentity>,
143    ) -> Result<(), AssociationRJServiceUserReason> {
144        if this_ae_title == called_ae_title {
145            Ok(())
146        } else {
147            Err(AssociationRJServiceUserReason::CalledAETitleNotRecognized)
148        }
149    }
150}
151
152/// A DICOM association builder for an acceptor DICOM node,
153/// often taking the role of a service class provider (SCP).
154///
155/// This is the standard way of negotiating and establishing
156/// an association with a requesting node.
157/// The outcome is a [`ServerAssociation`].
158/// Unlike the [`ClientAssociationOptions`],
159/// a value of this type can be reused for multiple connections.
160///
161/// [`ClientAssociationOptions`]: crate::association::ClientAssociationOptions
162///
163/// # Example
164///
165/// ```no_run
166/// # use std::net::TcpListener;
167/// # use dicom_ul::association::server::ServerAssociationOptions;
168/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
169/// # let tcp_listener: TcpListener = unimplemented!();
170/// let scp_options = ServerAssociationOptions::new()
171///    .with_abstract_syntax("1.2.840.10008.1.1")
172///    .with_transfer_syntax("1.2.840.10008.1.2.1");
173///
174/// let (stream, _address) = tcp_listener.accept()?;
175/// scp_options.establish(stream)?;
176/// # Ok(())
177/// # }
178/// ```
179///
180/// The SCP will by default accept all transfer syntaxes
181/// supported by the main [transfer syntax registry][1],
182/// unless one or more transfer syntaxes are explicitly indicated
183/// through calls to [`with_transfer_syntax`][2].
184///
185/// Access control logic is also available,
186/// enabling application entities to decide on
187/// whether to accept or reject the association request
188/// based on the _called_ and _calling_ AE titles.
189///
190/// - By default, the application will accept requests from anyone
191///   ([`AcceptAny`])
192/// - To only accept requests with a matching _called_ AE title,
193///   add a call to [`accept_called_ae_title`]
194///   ([`AcceptCalledAeTitle`]).
195/// - Any other policy can be implemented through the [`AccessControl`] trait.
196///
197/// [`accept_called_ae_title`]: Self::accept_called_ae_title
198/// [`AcceptAny`]: AcceptAny
199/// [`AcceptCalledAeTitle`]: AcceptCalledAeTitle
200/// [`AccessControl`]: AccessControl
201///
202/// [1]: dicom_transfer_syntax_registry
203/// [2]: ServerAssociationOptions::with_transfer_syntax
204#[derive(Debug, Clone)]
205pub struct ServerAssociationOptions<'a, A> {
206    /// the application entity access control policy
207    ae_access_control: A,
208    /// the AE title of this DICOM node
209    ae_title: Cow<'a, str>,
210    /// the requested application context name
211    application_context_name: Cow<'a, str>,
212    /// the list of requested abstract syntaxes
213    abstract_syntax_uids: Vec<Cow<'a, str>>,
214    /// the list of requested transfer syntaxes
215    transfer_syntax_uids: Vec<Cow<'a, str>>,
216    /// the expected protocol version
217    protocol_version: u16,
218    /// the maximum PDU length
219    max_pdu_length: u32,
220    /// whether to receive PDUs in strict mode
221    strict: bool,
222    /// whether to accept unknown abstract syntaxes
223    promiscuous: bool,
224}
225
226impl<'a> Default for ServerAssociationOptions<'a, AcceptAny> {
227    fn default() -> Self {
228        ServerAssociationOptions {
229            ae_access_control: AcceptAny,
230            ae_title: "THIS-SCP".into(),
231            application_context_name: "1.2.840.10008.3.1.1.1".into(),
232            abstract_syntax_uids: Vec::new(),
233            transfer_syntax_uids: Vec::new(),
234            protocol_version: 1,
235            max_pdu_length: crate::pdu::reader::DEFAULT_MAX_PDU,
236            strict: true,
237            promiscuous: false,
238        }
239    }
240}
241
242impl<'a> ServerAssociationOptions<'a, AcceptAny> {
243    /// Create a new set of options for establishing an association.
244    pub fn new() -> Self {
245        Self::default()
246    }
247}
248
249impl<'a, A> ServerAssociationOptions<'a, A>
250where
251    A: AccessControl,
252{
253    /// Change the access control policy to accept any association
254    /// regardless of the specified AE titles.
255    ///
256    /// This is the default behavior when the options are first created.
257    pub fn accept_any(self) -> ServerAssociationOptions<'a, AcceptAny> {
258        self.ae_access_control(AcceptAny)
259    }
260
261    /// Change the access control policy to accept an association
262    /// if the called AE title matches this node's AE title.
263    ///
264    /// The default is to accept any requesting node
265    /// regardless of the specified AE titles.
266    pub fn accept_called_ae_title(self) -> ServerAssociationOptions<'a, AcceptCalledAeTitle> {
267        self.ae_access_control(AcceptCalledAeTitle)
268    }
269
270    /// Change the access control policy.
271    ///
272    /// The default is to accept any requesting node
273    /// regardless of the specified AE titles.
274    pub fn ae_access_control<P>(self, access_control: P) -> ServerAssociationOptions<'a, P>
275    where
276        P: AccessControl,
277    {
278        let ServerAssociationOptions {
279            ae_title,
280            application_context_name,
281            abstract_syntax_uids,
282            transfer_syntax_uids,
283            protocol_version,
284            max_pdu_length,
285            strict,
286            promiscuous,
287            ae_access_control: _,
288        } = self;
289
290        ServerAssociationOptions {
291            ae_access_control: access_control,
292            ae_title,
293            application_context_name,
294            abstract_syntax_uids,
295            transfer_syntax_uids,
296            protocol_version,
297            max_pdu_length,
298            strict,
299            promiscuous,
300        }
301    }
302
303    /// Define the application entity title referring to this DICOM node.
304    ///
305    /// The default is `THIS-SCP`.
306    pub fn ae_title<T>(mut self, ae_title: T) -> Self
307    where
308        T: Into<Cow<'a, str>>,
309    {
310        self.ae_title = ae_title.into();
311        self
312    }
313
314    /// Include this abstract syntax
315    /// in the list of proposed presentation contexts.
316    pub fn with_abstract_syntax<T>(mut self, abstract_syntax_uid: T) -> Self
317    where
318        T: Into<Cow<'a, str>>,
319    {
320        self.abstract_syntax_uids
321            .push(trim_uid(abstract_syntax_uid.into()));
322        self
323    }
324
325    /// Include this transfer syntax in each proposed presentation context.
326    pub fn with_transfer_syntax<T>(mut self, transfer_syntax_uid: T) -> Self
327    where
328        T: Into<Cow<'a, str>>,
329    {
330        self.transfer_syntax_uids
331            .push(trim_uid(transfer_syntax_uid.into()));
332        self
333    }
334
335    /// Override the maximum expected PDU length.
336    pub fn max_pdu_length(mut self, value: u32) -> Self {
337        self.max_pdu_length = value;
338        self
339    }
340
341    /// Override strict mode:
342    /// whether receiving PDUs must not
343    /// surpass the negotiated maximum PDU length.
344    pub fn strict(mut self, strict: bool) -> Self {
345        self.strict = strict;
346        self
347    }
348
349    /// Override promiscuous mode:
350    /// whether to accept unknown abstract syntaxes.
351    pub fn promiscuous(mut self, promiscuous: bool) -> Self {
352        self.promiscuous = promiscuous;
353        self
354    }
355
356    /// Negotiate an association with the given TCP stream.
357    pub fn establish(&self, mut socket: TcpStream) -> Result<ServerAssociation> {
358        ensure!(
359            !self.abstract_syntax_uids.is_empty() || self.promiscuous,
360            MissingAbstractSyntaxSnafu
361        );
362
363        let max_pdu_length = self.max_pdu_length;
364
365        let pdu =
366            read_pdu(&mut socket, max_pdu_length, self.strict).context(ReceiveRequestSnafu)?;
367        let mut buffer: Vec<u8> = Vec::with_capacity(max_pdu_length as usize);
368        match pdu {
369            Pdu::AssociationRQ(AssociationRQ {
370                protocol_version,
371                calling_ae_title,
372                called_ae_title,
373                application_context_name,
374                presentation_contexts,
375                user_variables,
376            }) => {
377                if protocol_version != self.protocol_version {
378                    write_pdu(
379                        &mut buffer,
380                        &Pdu::AssociationRJ(AssociationRJ {
381                            result: AssociationRJResult::Permanent,
382                            source: AssociationRJSource::ServiceUser(
383                                AssociationRJServiceUserReason::NoReasonGiven,
384                            ),
385                        }),
386                    )
387                    .context(SendResponseSnafu)?;
388                    socket.write_all(&buffer).context(WireSendSnafu)?;
389                    return RejectedSnafu.fail();
390                }
391
392                if application_context_name != self.application_context_name {
393                    write_pdu(
394                        &mut buffer,
395                        &Pdu::AssociationRJ(AssociationRJ {
396                            result: AssociationRJResult::Permanent,
397                            source: AssociationRJSource::ServiceUser(
398                                AssociationRJServiceUserReason::ApplicationContextNameNotSupported,
399                            ),
400                        }),
401                    )
402                    .context(SendResponseSnafu)?;
403                    socket.write_all(&buffer).context(WireSendSnafu)?;
404                    return RejectedSnafu.fail();
405                }
406
407                self.ae_access_control
408                    .check_access(
409                        &self.ae_title,
410                        &calling_ae_title,
411                        &called_ae_title,
412                        user_variables
413                            .iter()
414                            .find_map(|user_variable| match user_variable {
415                                UserVariableItem::UserIdentityItem(user_identity) => {
416                                    Some(user_identity)
417                                }
418                                _ => None,
419                            }),
420                    )
421                    .map(Ok)
422                    .unwrap_or_else(|reason| {
423                        write_pdu(
424                            &mut buffer,
425                            &Pdu::AssociationRJ(AssociationRJ {
426                                result: AssociationRJResult::Permanent,
427                                source: AssociationRJSource::ServiceUser(reason),
428                            }),
429                        )
430                        .context(SendResponseSnafu)?;
431                        socket.write_all(&buffer).context(WireSendSnafu)?;
432                        RejectedSnafu.fail()
433                    })?;
434
435                // fetch requested maximum PDU length
436                let requestor_max_pdu_length = user_variables
437                    .iter()
438                    .find_map(|item| match item {
439                        UserVariableItem::MaxLength(len) => Some(*len),
440                        _ => None,
441                    })
442                    .unwrap_or(DEFAULT_MAX_PDU);
443
444                // treat 0 as the maximum size admitted by the standard
445                let requestor_max_pdu_length = if requestor_max_pdu_length == 0 {
446                    MAXIMUM_PDU_SIZE
447                } else {
448                    requestor_max_pdu_length
449                };
450
451                let presentation_contexts: Vec<_> = presentation_contexts
452                    .into_iter()
453                    .map(|pc| {
454                        if !self
455                            .abstract_syntax_uids
456                            .contains(&trim_uid(Cow::from(pc.abstract_syntax)))
457                            && !self.promiscuous
458                        {
459                            return PresentationContextResult {
460                                id: pc.id,
461                                reason: PresentationContextResultReason::AbstractSyntaxNotSupported,
462                                transfer_syntax: "1.2.840.10008.1.2".to_string(),
463                            };
464                        }
465
466                        let (transfer_syntax, reason) = self
467                            .choose_ts(pc.transfer_syntaxes)
468                            .map(|ts| (ts, PresentationContextResultReason::Acceptance))
469                            .unwrap_or_else(|| {
470                                (
471                                    "1.2.840.10008.1.2".to_string(),
472                                    PresentationContextResultReason::TransferSyntaxesNotSupported,
473                                )
474                            });
475
476                        PresentationContextResult {
477                            id: pc.id,
478                            reason,
479                            transfer_syntax,
480                        }
481                    })
482                    .collect();
483
484                write_pdu(
485                    &mut buffer,
486                    &Pdu::AssociationAC(AssociationAC {
487                        protocol_version: self.protocol_version,
488                        application_context_name,
489                        presentation_contexts: presentation_contexts.clone(),
490                        calling_ae_title: calling_ae_title.clone(),
491                        called_ae_title,
492                        user_variables: vec![
493                            UserVariableItem::MaxLength(max_pdu_length),
494                            UserVariableItem::ImplementationClassUID(
495                                IMPLEMENTATION_CLASS_UID.to_string(),
496                            ),
497                            UserVariableItem::ImplementationVersionName(
498                                IMPLEMENTATION_VERSION_NAME.to_string(),
499                            ),
500                        ],
501                    }),
502                )
503                .context(SendResponseSnafu)?;
504                socket.write_all(&buffer).context(WireSendSnafu)?;
505
506                Ok(ServerAssociation {
507                    presentation_contexts,
508                    requestor_max_pdu_length,
509                    acceptor_max_pdu_length: max_pdu_length,
510                    socket,
511                    client_ae_title: calling_ae_title,
512                    buffer,
513                    strict: self.strict,
514                })
515            }
516            Pdu::ReleaseRQ => {
517                write_pdu(&mut buffer, &Pdu::ReleaseRP).context(SendResponseSnafu)?;
518                socket.write_all(&buffer).context(WireSendSnafu)?;
519                AbortedSnafu.fail()
520            }
521            pdu @ Pdu::AssociationAC { .. }
522            | pdu @ Pdu::AssociationRJ { .. }
523            | pdu @ Pdu::PData { .. }
524            | pdu @ Pdu::ReleaseRP
525            | pdu @ Pdu::AbortRQ { .. } => UnexpectedRequestSnafu { pdu }.fail(),
526            pdu @ Pdu::Unknown { .. } => UnknownRequestSnafu { pdu }.fail(),
527        }
528    }
529
530    /// From a sequence of transfer syntaxes,
531    /// choose the first transfer syntax to
532    /// - be on the options' list of transfer syntaxes, and
533    /// - be supported by the main transfer syntax registry.
534    ///
535    /// If the options' list is empty,
536    /// accept the first transfer syntax supported.
537    fn choose_ts<I, T>(&self, it: I) -> Option<T>
538    where
539        I: IntoIterator<Item = T>,
540        T: AsRef<str>,
541    {
542        if self.transfer_syntax_uids.is_empty() {
543            return choose_supported(it);
544        }
545
546        it.into_iter().find(|ts| {
547            let ts = ts.as_ref();
548            if self.transfer_syntax_uids.is_empty() {
549                ts.trim_end_matches(|c: char| c.is_whitespace() || c == '\0') == "1.2.840.10008.1.2"
550            } else {
551                self.transfer_syntax_uids.contains(&trim_uid(ts.into())) && is_supported(ts)
552            }
553        })
554    }
555}
556
557/// A DICOM upper level association from the perspective
558/// of an accepting application entity.
559///
560/// The most common operations of an established association are
561/// [`send`](Self::send)
562/// and [`receive`](Self::receive).
563/// Sending large P-Data fragments may be easier through the P-Data sender
564/// abstraction (see [`send_pdata`](Self::send_pdata)).
565///
566/// When the value falls out of scope,
567/// the program will shut down the underlying TCP connection.
568#[derive(Debug)]
569pub struct ServerAssociation {
570    /// The accorded presentation contexts
571    presentation_contexts: Vec<PresentationContextResult>,
572    /// The maximum PDU length that the remote application entity accepts
573    requestor_max_pdu_length: u32,
574    /// The maximum PDU length that this application entity is expecting to receive
575    acceptor_max_pdu_length: u32,
576    /// The TCP stream to the other DICOM node
577    socket: TcpStream,
578    /// The application entity title of the other DICOM node
579    client_ae_title: String,
580    /// write buffer to send fully assembled PDUs on wire
581    buffer: Vec<u8>,
582    /// whether to receive PDUs in strict mode
583    strict: bool,
584}
585
586impl ServerAssociation {
587    /// Obtain a view of the negotiated presentation contexts.
588    pub fn presentation_contexts(&self) -> &[PresentationContextResult] {
589        &self.presentation_contexts
590    }
591
592    /// Obtain the remote DICOM node's application entity title.
593    pub fn client_ae_title(&self) -> &str {
594        &self.client_ae_title
595    }
596
597    /// Send a PDU message to the other intervenient.
598    pub fn send(&mut self, msg: &Pdu) -> Result<()> {
599        self.buffer.clear();
600        write_pdu(&mut self.buffer, msg).context(SendSnafu)?;
601        if self.buffer.len() > self.requestor_max_pdu_length as usize {
602            return SendTooLongPduSnafu {
603                length: self.buffer.len(),
604            }
605            .fail();
606        }
607        self.socket.write_all(&self.buffer).context(WireSendSnafu)
608    }
609
610    /// Read a PDU message from the other intervenient.
611    pub fn receive(&mut self) -> Result<Pdu> {
612        read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict).context(ReceiveSnafu)
613    }
614
615    /// Send a provider initiated abort message
616    /// and shut down the TCP connection,
617    /// terminating the association.
618    pub fn abort(mut self) -> Result<()> {
619        let pdu = Pdu::AbortRQ {
620            source: AbortRQSource::ServiceProvider(
621                AbortRQServiceProviderReason::ReasonNotSpecified,
622            ),
623        };
624        let out = self.send(&pdu);
625        let _ = self.socket.shutdown(std::net::Shutdown::Both);
626        out
627    }
628
629    /// Prepare a P-Data writer for sending
630    /// one or more data item PDUs.
631    ///
632    /// Returns a writer which automatically
633    /// splits the inner data into separate PDUs if necessary.
634    pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> {
635        PDataWriter::new(
636            &mut self.socket,
637            presentation_context_id,
638            self.requestor_max_pdu_length,
639        )
640    }
641
642    /// Prepare a P-Data reader for receiving
643    /// one or more data item PDUs.
644    ///
645    /// Returns a reader which automatically
646    /// receives more data PDUs once the bytes collected are consumed.
647    pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> {
648        PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length)
649    }
650
651    /// Obtain access to the inner TCP stream
652    /// connected to the association acceptor.
653    ///
654    /// This can be used to send the PDU in semantic fragments of the message,
655    /// thus using less memory.
656    ///
657    /// **Note:** reading and writing should be done with care
658    /// to avoid inconsistencies in the association state.
659    /// Do not call `send` and `receive` while not in a PDU boundary.
660    pub fn inner_stream(&mut self) -> &mut TcpStream {
661        &mut self.socket
662    }
663}
664
665/// Check that a transfer syntax repository
666/// supports the given transfer syntax,
667/// meaning that it can parse and decode DICOM data sets.
668///
669/// ```
670/// # use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
671/// # use dicom_ul::association::server::is_supported_with_repo;
672/// // Implicit VR Little Endian is guaranteed to be supported
673/// assert!(is_supported_with_repo(TransferSyntaxRegistry, "1.2.840.10008.1.2"));
674/// ```
675pub fn is_supported_with_repo<R>(ts_repo: R, ts_uid: &str) -> bool
676where
677    R: TransferSyntaxIndex,
678{
679    ts_repo
680        .get(ts_uid)
681        .filter(|ts| !ts.is_unsupported())
682        .is_some()
683}
684
685/// Check that the main transfer syntax registry
686/// supports the given transfer syntax,
687/// meaning that it can parse and decode DICOM data sets.
688///
689/// ```
690/// # use dicom_ul::association::server::is_supported;
691/// // Implicit VR Little Endian is guaranteed to be supported
692/// assert!(is_supported("1.2.840.10008.1.2"));
693/// ```
694pub fn is_supported(ts_uid: &str) -> bool {
695    is_supported_with_repo(TransferSyntaxRegistry, ts_uid)
696}
697
698/// From a sequence of transfer syntaxes,
699/// choose the first transfer syntax to be supported
700/// by the given transfer syntax repository.
701pub fn choose_supported_with_repo<R, I, T>(ts_repo: R, it: I) -> Option<T>
702where
703    R: TransferSyntaxIndex,
704    I: IntoIterator<Item = T>,
705    T: AsRef<str>,
706{
707    it.into_iter()
708        .find(|ts| is_supported_with_repo(&ts_repo, ts.as_ref()))
709}
710
711/// From a sequence of transfer syntaxes,
712/// choose the first transfer syntax to be supported
713/// by the main transfer syntax registry.
714pub fn choose_supported<I, T>(it: I) -> Option<T>
715where
716    I: IntoIterator<Item = T>,
717    T: AsRef<str>,
718{
719    it.into_iter().find(|ts| is_supported(ts.as_ref()))
720}
721
722#[cfg(test)]
723mod tests {
724    use super::choose_supported;
725
726    #[test]
727    fn test_choose_supported() {
728        assert_eq!(choose_supported(vec!["1.1.1.1.1"]), None,);
729
730        // string slices, impl VR first
731        assert_eq!(
732            choose_supported(vec!["1.2.840.10008.1.2", "1.2.840.10008.1.2.1"]),
733            Some("1.2.840.10008.1.2"),
734        );
735
736        // heap allocated strings slices, expl VR first
737        assert_eq!(
738            choose_supported(vec![
739                "1.2.840.10008.1.2.1".to_string(),
740                "1.2.840.10008.1.2".to_string()
741            ]),
742            Some("1.2.840.10008.1.2.1".to_string()),
743        );
744    }
745}