diff --git a/echoscu/src/main.rs b/echoscu/src/main.rs index 09303f29..38bb240e 100644 --- a/echoscu/src/main.rs +++ b/echoscu/src/main.rs @@ -3,7 +3,7 @@ use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::{tags, uids}; use dicom_object::{mem::InMemDicomObject, StandardDataDictionary}; use dicom_ul::{ - association::{Association, SyncAssociation, client::ClientAssociationOptions}, + association::{client::ClientAssociationOptions}, pdu::{self, PDataValueType, Pdu}, }; use pdu::PDataValue; diff --git a/findscu/src/main.rs b/findscu/src/main.rs index 452e8d82..d43b7537 100644 --- a/findscu/src/main.rs +++ b/findscu/src/main.rs @@ -6,7 +6,6 @@ use dicom_dump::DumpOptions; use dicom_encoding::transfer_syntax; use dicom_object::{mem::InMemDicomObject, open_file, StandardDataDictionary}; use dicom_transfer_syntax_registry::{entries, TransferSyntaxRegistry}; -use dicom_ul::association::{Association, SyncAssociation}; use dicom_ul::pdu::Pdu; use dicom_ul::{ association::ClientAssociationOptions, diff --git a/storescp/src/store_async.rs b/storescp/src/store_async.rs index d792c7f0..8f038ab0 100644 --- a/storescp/src/store_async.rs +++ b/storescp/src/store_async.rs @@ -4,7 +4,8 @@ use dicom_dictionary_std::tags; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_object::{FileMetaTableBuilder, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{Pdu, association::{Association, AsyncAssociation, AsyncServerAssociation}, pdu::{PDataValueType, PresentationContextResultReason}}; +use dicom_ul::prelude::*; +use dicom_ul::{Pdu, association::AsyncServerAssociation, pdu::{PDataValueType, PresentationContextResultReason}}; use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; diff --git a/storescp/src/store_sync.rs b/storescp/src/store_sync.rs index debe343d..49467df2 100644 --- a/storescp/src/store_sync.rs +++ b/storescp/src/store_sync.rs @@ -5,7 +5,7 @@ use dicom_dictionary_std::tags; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_object::{FileMetaTableBuilder, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{Pdu, ServerAssociation, association::{Association, CloseSocket, SyncAssociation}, pdu::{PDataValueType, PresentationContextResultReason}}; +use dicom_ul::{Pdu, ServerAssociation, association::{Association, CloseSocket}, pdu::{PDataValueType, PresentationContextResultReason}}; use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs index 7bbd1efc..3dc4f8af 100644 --- a/storescu/src/store_async.rs +++ b/storescu/src/store_async.rs @@ -5,7 +5,7 @@ use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use dicom_ul::{ - Pdu, association::{Association, AsyncAssociation, client::AsyncClientAssociation}, pdu::{PDataValue, PDataValueType} + Pdu, association::client::AsyncClientAssociation, pdu::{PDataValue, PDataValueType} }; use indicatif::ProgressBar; use snafu::{OptionExt, Report, ResultExt}; diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs index cb7e4095..a6d1050c 100644 --- a/storescu/src/store_sync.rs +++ b/storescu/src/store_sync.rs @@ -7,7 +7,7 @@ use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use dicom_ul::{ - ClientAssociation, Pdu, association::{Association, CloseSocket, SyncAssociation}, pdu::{PDataValue, PDataValueType} + ClientAssociation, Pdu, association::CloseSocket, pdu::{PDataValue, PDataValueType} }; use indicatif::ProgressBar; use snafu::{OptionExt, Report, ResultExt}; diff --git a/ul/README.md b/ul/README.md index 9bffd380..b3b3b11f 100644 --- a/ul/README.md +++ b/ul/README.md @@ -3,7 +3,23 @@ [![CratesIO](https://img.shields.io/crates/v/dicom-ul.svg)](https://crates.io/crates/dicom-ul) [![Documentation](https://docs.rs/dicom-ul/badge.svg)](https://docs.rs/dicom-ul) -This is an implementation of the DICOM upper layer protocol. +An implementation of the DICOM upper layer protocol. +This crate contains the types and methods needed +to interact with DICOM nodes through the upper layer protocol. +It can be used as a base for finite-state machines and higher-level helpers, +enabling the creation of concrete +service class users (SCUs) and service class providers (SCPs). +TLS support for secure transport connections +is also available via [Rustls](https://crates.io/crates/rustls). + +Examples of DICOM network tools constructed using `dicom-ul` include +[dicom-storescp](https://crates.io/crates/dicom-storescp), +[dicom-storescu](https://crates.io/crates/dicom-storescu), +and [dicom-findscu](https://crates.io/crates/dicom-findscu). + +This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project +and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom) +for convenience. ## Testing diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 7961aba7..c545142c 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -14,7 +14,7 @@ use std::{ use crate::{ AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, association::{ - Association, CloseSocket, NegotiatedOptions, SocketOptions, SyncAssociation, encode_pdu, + Association, NegotiatedOptions, SocketOptions, SyncAssociation, encode_pdu, private::SyncAssociationSealed, read_pdu_from_wire, }, pdu::{ AbortRQSource, AssociationAC, AssociationRQ, DEFAULT_MAX_PDU, LARGE_PDU_SIZE, @@ -23,6 +23,8 @@ use crate::{ write_pdu, } }; +#[cfg(feature = "async")] +use crate::association::AsyncAssociation; use snafu::{ensure, ResultExt}; use super::{ @@ -30,11 +32,17 @@ use super::{ Result, }; +// stray module from 0.9.0, remove in 0.10.0 +#[deprecated(since = "0.9.1")] +pub mod non_blocking {} + #[cfg(feature = "sync-tls")] pub type TlsStream = rustls::StreamOwned; #[cfg(feature = "async-tls")] pub type AsyncTlsStream = tokio_rustls::client::TlsStream; +pub use crate::association::CloseSocket; + /// Helper function to establish a TCP client connection fn tcp_connection( ae_address: &AeAddr, @@ -171,7 +179,7 @@ fn tls_connection( /// // Loading certificates and keys for demonstration purposes /// let ca_cert = CertificateDer::from_pem_slice(std::fs::read("ssl/ca.crt")?.as_ref()) /// .expect("Failed to load client cert"); -/// +/// /// // Server certificate -- signed by CA /// let server_cert = CertificateDer::from_pem_slice(std::fs::read("ssl/server.crt")?.as_ref()) /// .expect("Failed to load server cert"); @@ -181,7 +189,7 @@ fn tls_connection( /// .expect("Failed to load client cert"); /// let client_private_key = PrivateKeyDer::from_pem_slice(std::fs::read("ssl/client.key")?.as_ref()) /// .expect("Failed to load client private key"); -/// +/// /// // Create a root cert store for the client which includes the server certificate /// let mut certs = RootCertStore::empty(); /// certs.add_parsable_certificates(vec![ca_cert.clone()]); @@ -945,22 +953,18 @@ impl<'a> ClientAssociationOptions<'a> { /// of a requesting application entity. /// /// The most common operations of an established association are -/// [`send`](Self::send) -/// and [`receive`](Self::receive). +/// [`send`](SyncAssociation::send) +/// and [`receive`](SyncAssociation::receive). /// Sending large P-Data fragments may be easier through the P-Data sender -/// abstraction (see [`send_pdata`](Self::send_pdata)). -/// -/// When the value falls out of scope, -/// the program will automatically try to gracefully release the association -/// through a standard C-RELEASE message exchange, -/// then shut down the underlying TCP connection. +/// abstraction (see [`send_pdata`](SyncAssociation::send_pdata)). /// -/// This may either be sync or async depending on which method was called to -/// establish the association. +/// Call `release` at the end +/// to perform a standard C-RELEASE message exchange +/// and shut down the underlying TCP connection. +/// Not calling this method will only close the socket +/// without gracefully releasing the association. #[derive(Debug)] -pub struct ClientAssociation -where S: CloseSocket + std::io::Read + std::io::Write, -{ +pub struct ClientAssociation { /// The presentation contexts accorded with the acceptor application entity, /// without the rejected ones. presentation_contexts: Vec, @@ -1038,6 +1042,100 @@ where S: CloseSocket + std::io::Read + std::io::Write, pub fn write_timeout(&self) -> Option { self.write_timeout } + + /// Retrieve the maximum PDU length + /// that the association acceptor is expecting to receive. + pub fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } + + /// Retrieve the maximum PDU length + /// that the association requestor is expecting to receive. + pub fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } + + /// Retrieve the user variables that were taken from the server. + /// + /// It usually contains the maximum PDU length, + /// the implementation class UID, and the implementation version name. + pub fn user_variables(&self) -> &[UserVariableItem] { + &self.user_variables + } + + /// Retrieve the list of negotiated presentation contexts. + pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } +} + +// compatibility filler, remove in 0.10.0 +impl ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write, +{ + /// Send a PDU message to the other intervenient. + pub fn send(&mut self, pdu: &Pdu) -> Result<()> { + SyncAssociation::send(self, pdu) + } + + /// Read a PDU message from the other intervenient. + pub fn receive(&mut self) -> Result { + SyncAssociation::receive(self) + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + pub fn send_pdata( + &mut self, + presentation_context_id: u8, + ) -> crate::association::pdata::PDataWriter<&mut S> { + SyncAssociation::send_pdata(self, presentation_context_id) + } + + /// Iniate a graceful release of the association. + /// + /// A DIMSE A-RELEASE transaction is initiated by this application entity, + /// and the underlying socket is closed once settled. + /// + /// Note that as of version 0.9.1, + /// `ClientAssociation` no longer calls this method on [`Drop`], + /// so remember to call `release` explicitly + /// at the end of all DIMSE transactions. + pub fn release(self) -> Result<()> { + SyncAssociation::release(self) + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + pub fn abort(self) -> Result<()> { + SyncAssociation::abort(self) + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + pub fn receive_pdata(&mut self) -> crate::association::pdata::PDataReader<'_, &mut S> { + SyncAssociation::receive_pdata(self) + } + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + pub fn inner_stream(&mut self) -> &mut S { + SyncAssociation::inner_stream(self) + } } impl SyncAssociationSealed for ClientAssociation @@ -1071,9 +1169,23 @@ where S: CloseSocket + std::io::Read + std::io::Write{ } } +/// Trait with the behavior to synchronously release an association +#[deprecated(since = "0.9.1", note = "Call `SyncAssociation::release` instead")] +pub trait Release { + #[deprecated(since = "0.9.1", note = "Call `SyncAssociation::release` instead")] + fn release(&mut self) -> Result<()>; +} + +#[allow(deprecated)] +impl Release for ClientAssociation { + fn release(&mut self) -> Result<()> { + SyncAssociationSealed::release(self) + } +} + #[cfg(feature = "async")] /// Initiate simple TCP connection to the given address -pub async fn async_connection( +pub(crate) async fn async_connection( ae_address: &AeAddr, opts: &SocketOptions, ) -> Result where T: tokio::net::ToSocketAddrs{ @@ -1111,11 +1223,22 @@ where Ok(tls_stream) } +/// A DICOM upper level association from the perspective +/// of a requesting application entity. +/// +/// The most common operations of an established association are +/// [`send`](AsyncAssociation::release) and [`receive`](AsyncAssociation::release). +/// Sending large P-Data fragments may be easier through the P-Data sender +/// abstraction (see [`send_pdata`](AsyncAssociation::send_pdata)). +/// +/// Call [`release`](AsyncAssociation::release) at the end +/// to perform a standard C-RELEASE message exchange +/// and shut down the underlying TCP connection. +/// Not calling this method will only close the socket +/// without gracefully releasing the association. #[cfg(feature = "async")] #[derive(Debug)] -pub struct AsyncClientAssociation -where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, -{ +pub struct AsyncClientAssociation { /// The presentation contexts accorded with the acceptor application entity, /// without the rejected ones. presentation_contexts: Vec, @@ -1259,7 +1382,7 @@ impl<'a> ClientAssociationOptions<'a> { /// /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; - /// #[tokio::main] + /// # #[tokio::main] /// # async fn run() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_abstract_syntax("1.2.840.10008.1.1") @@ -1303,7 +1426,7 @@ impl<'a> ClientAssociationOptions<'a> { /// /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; - /// #[tokio::main] + /// # #[tokio::main] /// # async fn run() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_abstract_syntax("1.2.840.10008.1.1") @@ -1344,9 +1467,7 @@ impl<'a> ClientAssociationOptions<'a> { } #[cfg(feature = "async")] -impl Association for AsyncClientAssociation -where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, -{ +impl Association for AsyncClientAssociation { fn peer_ae_title(&self) -> &str { &self.peer_ae_title } @@ -1385,9 +1506,7 @@ where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, } #[cfg(feature = "async")] -impl AsyncClientAssociation -where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, -{ +impl AsyncClientAssociation { /// Retrieve read timeout for the association pub fn read_timeout(&self) -> Option { self.read_timeout @@ -1397,6 +1516,102 @@ where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, pub fn write_timeout(&self) -> Option { self.write_timeout } + + /// Retrieve the maximum PDU length + /// that the association acceptor is expecting to receive. + pub fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } + + /// Retrieve the maximum PDU length + /// that the association requestor is expecting to receive. + pub fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } + + /// Retrieve the user variables that were taken from the server. + /// + /// It usually contains the maximum PDU length, + /// the implementation class UID, and the implementation version name. + pub fn user_variables(&self) -> &[UserVariableItem] { + &self.user_variables + } + + /// Retrieve the list of negotiated presentation contexts. + pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } +} + +// compatibility filler, remove in 0.10.0 +#[cfg(feature = "async")] +impl AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + pub fn inner_stream(&mut self) -> &mut S { + AsyncAssociation::inner_stream(self) + } + + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + AsyncAssociation::send(self, msg).await + } + + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + AsyncAssociation::receive(self).await + } + + /// Iniate a graceful release of the association. + /// + /// A DIMSE A-RELEASE transaction is initiated by this application entity, + /// and the underlying socket is closed once settled. + /// + /// Note that implementers of this trait + /// do not try to release the association on [`Drop`], + /// so remember to call `release` explicitly + /// at the end of all DIMSE transactions. + pub async fn release(self) -> Result<()> { + AsyncAssociation::release(self).await + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + pub async fn abort(self) -> Result<()> { + AsyncAssociation::abort(self).await + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + pub fn send_pdata( + &mut self, + presentation_context_id: u8, + ) -> crate::association::pdata::non_blocking::AsyncPDataWriter<&mut S> { + AsyncAssociation::send_pdata(self, presentation_context_id) + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + pub fn receive_pdata(&mut self) -> crate::association::pdata::PDataReader<'_, &mut S> { + AsyncAssociation::receive_pdata(self) + } } #[cfg(feature = "async")] @@ -1437,7 +1652,7 @@ where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, } #[cfg(feature = "async")] -impl super::AsyncAssociation for AsyncClientAssociation +impl AsyncAssociation for AsyncClientAssociation where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ fn inner_stream(&mut self) -> &mut S { diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index d45c6142..32e8fc3b 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -196,7 +196,7 @@ pub(crate) struct NegotiatedOptions{ /// Socket configuration for associations #[derive(Debug, Clone, Copy, Default)] -pub struct SocketOptions { +pub(crate) struct SocketOptions { /// Timeout for individual read operations read_timeout: Option, /// Timeout for individual send operations @@ -204,6 +204,7 @@ pub struct SocketOptions { /// Timeout for connection establishment connection_timeout: Option, } + /// Trait to close underlying socket pub trait CloseSocket { fn close(&mut self) -> std::io::Result<()>; @@ -409,8 +410,16 @@ pub trait SyncAssociation: priv private::SyncAssociationSealed::abort(&mut self) } - /// Iniate a graceful release of the association - fn release(mut self) -> Result<()> where Self: Sized{ + /// Iniate a graceful release of the association. + /// + /// A DIMSE A-RELEASE transaction is initiated by this application entity, + /// and the underlying socket is closed once settled. + /// + /// Note that as of version 0.9.1, + /// implementers of this trait no longer call this method on [`Drop`], + /// so remember to call `release` explicitly + /// at the end of all DIMSE transactions. + fn release(mut self) -> Result<()> where Self: Sized { private::SyncAssociationSealed::release(&mut self) } @@ -488,7 +497,15 @@ pub trait AsyncAssociation impl std::future::Future> + Send where Self: Sized + Send { async move { @@ -543,7 +560,7 @@ async fn timeout( } /// Encode a PDU into the provided buffer -pub fn encode_pdu(buffer: &mut Vec, pdu: &Pdu, peer_max_pdu_length: u32) -> Result<()> { +pub(crate) fn encode_pdu(buffer: &mut Vec, pdu: &Pdu, peer_max_pdu_length: u32) -> Result<()> { write_pdu( buffer, pdu).context(SendPduSnafu)?; if buffer.len() > peer_max_pdu_length as usize { return SendTooLongPduSnafu { diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 32eeb90e..291c924a 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -37,6 +37,13 @@ use super::{ Error, Result }; +#[cfg(feature = "async")] +use crate::association::AsyncAssociation; + +// stray module from 0.9.0, remove in 0.10.0 +#[deprecated(since = "0.9.1")] +pub mod non_blocking {} + #[cfg(feature = "sync-tls")] pub type TlsStream = rustls::StreamOwned; #[cfg(feature = "async-tls")] @@ -788,8 +795,7 @@ where /// When the value falls out of scope, /// the program will shut down the underlying TCP connection. #[derive(Debug)] -pub struct ServerAssociation -where S: std::io::Read + std::io::Write + CloseSocket{ +pub struct ServerAssociation { /// The accorded presentation contexts presentation_contexts: Vec, /// The maximum PDU length that the remote application entity accepts @@ -811,6 +817,86 @@ where S: std::io::Read + std::io::Write + CloseSocket{ user_variables: Vec, } +// compatibility filler, remove in 0.10.0 +impl ServerAssociation { + /// Obtain a view of the negotiated presentation contexts. + pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } + + /// Retrieve the maximum PDU length + /// that the association acceptor is expecting to receive. + pub fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } + + /// Retrieve the maximum PDU length + /// that the association requestor is expecting to receive. + pub fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } + + /// Obtain the remote DICOM node's application entity title. + #[deprecated(since = "0.9.1", note = "Call `peer_ae_title` from trait `Association`")] + pub fn client_ae_title(&self) -> &str { + &self.client_ae_title + } +} + +impl ServerAssociation +where S: std::io::Read + std::io::Write + CloseSocket { + /// Send a PDU message to the other intervenient. + pub fn send(&mut self, msg: &Pdu) -> Result<()> { + SyncAssociation::send(self, msg) + } + + /// Read a PDU message from the other intervenient. + pub fn receive(&mut self) -> Result { + SyncAssociation::receive(self) + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + pub fn abort(self) -> Result<()> { + SyncAssociation::abort(self) + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + pub fn send_pdata( + &mut self, + presentation_context_id: u8, + ) -> crate::association::pdata::PDataWriter<&mut S> { + SyncAssociation::send_pdata(self, presentation_context_id) + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + pub fn receive_pdata(&mut self) -> crate::association::pdata::PDataReader<'_, &mut S> { + SyncAssociation::receive_pdata(self) + } + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + pub fn inner_stream(&mut self) -> &mut S { + SyncAssociation::inner_stream(self) + } +} + impl Association for ServerAssociation where S: std::io::Read + std::io::Write + CloseSocket{ @@ -848,6 +934,10 @@ where S: std::io::Read + std::io::Write + CloseSocket{ &self.client_ae_title } + /// Retrieve the user variables that were taken from the server. + /// + /// It usually contains the maximum PDU length, + /// the implementation class UID, and the implementation version name. fn user_variables(&self) -> &[UserVariableItem] { &self.user_variables } @@ -1059,8 +1149,7 @@ where /// the program will shut down the underlying TCP connection. #[cfg(feature = "async")] #[derive(Debug)] -pub struct AsyncServerAssociation -where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ +pub struct AsyncServerAssociation { /// The accorded presentation contexts presentation_contexts: Vec, /// The maximum PDU length that the remote application entity accepts @@ -1175,6 +1264,102 @@ where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ } } +// compatibility filler, remove in 0.10.0 +#[cfg(feature = "async")] +impl AsyncServerAssociation { + /// Obtain a view of the negotiated presentation contexts. + pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } + + /// Retrieve the maximum PDU length + /// that the association acceptor is expecting to receive. + pub fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } + + /// Retrieve the maximum PDU length + /// that the association requestor is expecting to receive. + pub fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } + + /// Obtain the remote DICOM node's application entity title. + #[deprecated(since = "0.9.1", note = "Call `peer_ae_title` from trait `Association`")] + pub fn client_ae_title(&self) -> &str { + &self.client_ae_title + } +} + +// compatibility filler, remove in 0.10.0 +#[cfg(feature = "async")] +impl AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send { + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + AsyncAssociation::send(self, msg).await + } + + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + AsyncAssociation::receive(self).await + } + + /// Iniate a graceful release of the association. + /// + /// A DIMSE A-RELEASE transaction is initiated by this application entity, + /// and the underlying socket is closed once settled. + /// + /// Note that implementers of this trait + /// do not try to release the association on [`Drop`], + /// so remember to call `release` explicitly + /// at the end of all DIMSE transactions. + pub async fn release(self) -> Result<()> { + AsyncAssociation::release(self).await + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + pub async fn abort(self) -> Result<()> { + AsyncAssociation::abort(self).await + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + pub fn send_pdata( + &mut self, + presentation_context_id: u8, + ) -> crate::association::pdata::non_blocking::AsyncPDataWriter<&mut S> { + AsyncAssociation::send_pdata(self, presentation_context_id) + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + pub fn receive_pdata(&mut self) -> crate::association::pdata::PDataReader<'_, &mut S> { + AsyncAssociation::receive_pdata(self) + } + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + pub fn inner_stream(&mut self) -> &mut S { + AsyncAssociation::inner_stream(self) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ul/src/association/tests.rs b/ul/src/association/tests.rs index 4e088aa4..a6d6fee4 100644 --- a/ul/src/association/tests.rs +++ b/ul/src/association/tests.rs @@ -2,9 +2,7 @@ use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::{tags, uids::VERIFICATION}; use dicom_object::InMemDicomObject; use dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN; -use crate::association::SyncAssociation; -#[cfg(feature = "async")] -use crate::association::AsyncAssociation; + // Helper funtion to create a C-ECHO command fn create_c_echo_command(message_id: u16) -> Vec { let obj = InMemDicomObject::command_from_element_iter([ diff --git a/ul/src/lib.rs b/ul/src/lib.rs index fcfab486..e73aa359 100644 --- a/ul/src/lib.rs +++ b/ul/src/lib.rs @@ -35,6 +35,7 @@ pub mod address; pub mod association; pub mod pdu; +pub mod prelude; /// The current implementation class UID generically referring to DICOM-rs. /// diff --git a/ul/src/pdu/mod.rs b/ul/src/pdu/mod.rs index eaf6b288..5e441728 100644 --- a/ul/src/pdu/mod.rs +++ b/ul/src/pdu/mod.rs @@ -99,14 +99,6 @@ pub enum ReadError { backtrace: Backtrace, }, - #[snafu(display("Invalid PDU field length"))] - InvalidPduFieldLength { - backtrace: Backtrace, - }, - - #[snafu(display("Could not read user variable"))] - ReadUserVariable { backtrace: Backtrace }, - #[snafu(display("Invalid item length {} (must be >=2)", length))] InvalidItemLength { length: u32 }, @@ -156,6 +148,12 @@ pub enum ReadError { MissingAbstractSyntax { backtrace: Backtrace }, #[snafu(display("Missing transfer syntax"))] MissingTransferSyntax { backtrace: Backtrace }, + #[snafu(display("Invalid PDU field length"))] + InvalidPduFieldLength { + backtrace: Backtrace, + }, + #[snafu(display("Could not read user variable"))] + ReadUserVariable { backtrace: Backtrace }, } /// Message component for a proposed presentation context. diff --git a/ul/src/prelude.rs b/ul/src/prelude.rs new file mode 100644 index 00000000..c062702f --- /dev/null +++ b/ul/src/prelude.rs @@ -0,0 +1,12 @@ +//! Prelude module for easy inclusion of important types +//! +//! This module re-exports all association traits as underscore imports. +//! +//! # Usage +//! +//! ```ignore +//! use dicom_ul::prelude::*; +//! ``` +#[cfg(feature = "async")] +pub use crate::association::AsyncAssociation as _; +pub use crate::association::{Association as _, SyncAssociation as _}; diff --git a/ul/tests/association.rs b/ul/tests/association.rs index e5fc70c3..dd98e425 100644 --- a/ul/tests/association.rs +++ b/ul/tests/association.rs @@ -1,5 +1,5 @@ use dicom_dictionary_std::uids::{self, VERIFICATION}; -use dicom_ul::{ClientAssociationOptions, Pdu, ServerAssociationOptions, association::SyncAssociation}; +use dicom_ul::{ClientAssociationOptions, Pdu, ServerAssociationOptions}; use rstest::rstest; use std::time::Instant; #[cfg(feature = "sync-tls")] @@ -126,7 +126,7 @@ fn create_test_config() -> Result<(Arc, Arc Result<()> { - use dicom_ul::{ServerAssociationOptions, association::AsyncAssociation}; + use dicom_ul::ServerAssociationOptions; // set up crypto provider -- Just use the default provider which is aws_lc_rs let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); diff --git a/ul/tests/association_echo.rs b/ul/tests/association_echo.rs index 1dd85ac2..f62d0a73 100644 --- a/ul/tests/association_echo.rs +++ b/ul/tests/association_echo.rs @@ -1,5 +1,6 @@ use dicom_ul::{ - ServerAssociation, association::{Association, Error, SyncAssociation, client::ClientAssociationOptions, server::ServerAssociationOptions}, pdu::{ + ServerAssociation, association::{Error, client::ClientAssociationOptions, server::ServerAssociationOptions}, + pdu::{ PDataValue, PDataValueType, Pdu, PresentationContextNegotiated, PresentationContextResultReason, } @@ -166,7 +167,6 @@ async fn spawn_scp_async( .with_abstract_syntax(VERIFICATION_SOP_CLASS); let h = tokio::spawn(async move { - use dicom_ul::association::AsyncAssociation; let (stream, _addr) = listener.accept().await?; let mut association = scp.establish_async(stream).await?; @@ -375,7 +375,6 @@ async fn scu_scp_association_test_async() { #[cfg(feature = "async")] async fn run_scu_scp_association_test_async(max_is_client: bool) { - use dicom_ul::association::AsyncAssociation; let (max_client_pdu_len, max_server_pdu_len) = if max_is_client { (HI_PDU_LEN, LO_PDU_LEN) diff --git a/ul/tests/association_promiscuous.rs b/ul/tests/association_promiscuous.rs index e1ff8cb8..e33e9251 100644 --- a/ul/tests/association_promiscuous.rs +++ b/ul/tests/association_promiscuous.rs @@ -1,8 +1,8 @@ use std::net::SocketAddr; -use dicom_ul::association::{Association, SyncAssociation, Error::NoAcceptedPresentationContexts}; +use dicom_ul::association::Error::NoAcceptedPresentationContexts; #[cfg(feature = "async")] -use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; +use dicom_ul::association::AsyncServerAssociation; use dicom_ul::pdu::PresentationContextResultReason::Acceptance; use dicom_ul::pdu::{ PresentationContextNegotiated, PresentationContextResultReason, UserVariableItem, diff --git a/ul/tests/association_store.rs b/ul/tests/association_store.rs index a085f8a1..f8e7c33d 100644 --- a/ul/tests/association_store.rs +++ b/ul/tests/association_store.rs @@ -1,8 +1,9 @@ use dicom_ul::{ - ServerAssociation, association::{Association, SyncAssociation, client::ClientAssociationOptions}, pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason} + ServerAssociation, association::client::ClientAssociationOptions, + pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason}, }; #[cfg(feature = "async")] -use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; +use dicom_ul::association::AsyncServerAssociation; use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; diff --git a/ul/tests/association_store_uncompressed.rs b/ul/tests/association_store_uncompressed.rs index 65c783a0..cb3720a3 100644 --- a/ul/tests/association_store_uncompressed.rs +++ b/ul/tests/association_store_uncompressed.rs @@ -2,10 +2,11 @@ //! which only accepts uncompressed transfer syntaxes use dicom_ul::{ - ServerAssociation, association::{Association, SyncAssociation, client::ClientAssociationOptions}, pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason} + ServerAssociation, association::client::ClientAssociationOptions, + pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason} }; #[cfg(feature = "async")] -use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; +use dicom_ul::association::AsyncServerAssociation; use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions;