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}