From 3251368e622a385de3a01a5581f5bb730187d902 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Sun, 14 Jul 2024 10:26:07 -0500 Subject: [PATCH 01/28] ENH: Add POC async implementation, example using storescp --- Cargo.lock | 159 +++- encoding/src/text.rs | 2 +- storescp/Cargo.toml | 3 +- storescp/src/main.rs | 35 +- ul/Cargo.toml | 5 + ul/src/address.rs | 4 +- ul/src/association/client.rs | 403 ++++++++- ul/src/association/mod.rs | 4 +- ul/src/association/pdata.rs | 5 +- ul/src/association/server.rs | 278 +++++- ul/src/lib.rs | 4 +- ul/src/pdu/mod.rs | 144 ++++ ul/src/pdu/reader.rs | 101 +-- ul/src/pdu/reader_nonblocking.rs | 959 +++++++++++++++++++++ ul/src/pdu/writer.rs | 36 +- ul/src/pdu/writer_nonblocking.rs | 1365 ++++++++++++++++++++++++++++++ 16 files changed, 3282 insertions(+), 225 deletions(-) create mode 100644 ul/src/pdu/reader_nonblocking.rs create mode 100644 ul/src/pdu/writer_nonblocking.rs diff --git a/Cargo.lock b/Cargo.lock index 55e8e28e5..2533248d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -92,6 +101,21 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.22.0" @@ -158,11 +182,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + [[package]] name = "cc" -version = "1.0.95" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "47de7e88bbbd467951ae7f5a6f34f70d1b4d9cfce53d5fd70f74ebe118b3db56" dependencies = [ "jobserver", "libc", @@ -538,6 +568,7 @@ dependencies = [ "dicom-transfer-syntax-registry", "dicom-ul", "snafu", + "tokio", "tracing", "tracing-subscriber", ] @@ -611,6 +642,7 @@ dependencies = [ "dicom-transfer-syntax-registry", "matches", "snafu", + "tokio", "tracing", ] @@ -921,6 +953,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "glob" version = "0.3.1" @@ -1216,6 +1254,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "ndarray" version = "0.15.6" @@ -1266,12 +1315,31 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1316,6 +1384,29 @@ dependencies = [ "supports-color", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1425,6 +1516,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -1504,6 +1604,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1648,6 +1754,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1690,6 +1805,16 @@ dependencies = [ "syn", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -1856,6 +1981,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/encoding/src/text.rs b/encoding/src/text.rs index 6d19cd844..f6ae4840a 100644 --- a/encoding/src/text.rs +++ b/encoding/src/text.rs @@ -65,7 +65,7 @@ type DecodeResult = Result; /// A holder of encoding and decoding mechanisms for text in DICOM content, /// which according to the standard, depends on the specific character set. -pub trait TextCodec { +pub trait TextCodec: Send + Sync { /// Obtain the defined term (unique name) of the text encoding, /// which may be used as the value of a /// Specific Character Set (0008, 0005) element to refer to this codec. diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index b0b0eb0a8..ecbba55e0 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] clap = { version = "4.0.18", features = ["derive"] } dicom-core = { path = '../core', version = "0.7.0" } -dicom-ul = { path = '../ul', version = "0.7.0" } +dicom-ul = { path = '../ul', version = "0.7.0", features = ["tokio"] } dicom-object = { path = '../object', version = "0.7.0" } dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-dictionary-std = { path = "../dictionary-std/", version = "0.7.0" } @@ -21,3 +21,4 @@ dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", versio snafu = "0.8" tracing = "0.1.36" tracing-subscriber = "0.3.15" +tokio = "1.38.0" diff --git a/storescp/src/main.rs b/storescp/src/main.rs index e8aabb82c..e2facccbb 100644 --- a/storescp/src/main.rs +++ b/storescp/src/main.rs @@ -1,6 +1,7 @@ use std::{ - net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream}, + net::{Ipv4Addr, SocketAddrV4}, path::PathBuf, + sync::Arc, }; use clap::Parser; @@ -47,7 +48,7 @@ struct App { port: u16, } -fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { +async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { let App { verbose, calling_ae_title, @@ -90,6 +91,7 @@ fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { let mut association = options .establish(scu_stream) + .await .whatever_context("could not establish association")?; info!("New association from {}", association.client_ae_title()); @@ -99,7 +101,7 @@ fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { ); loop { - match association.receive() { + match association.receive().await { Ok(mut pdu) => { if verbose { debug!("scu ----> scp: {}", pdu.short_description()); @@ -153,7 +155,7 @@ fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { data: cecho_data, }], }; - association.send(&pdu_response).whatever_context( + association.send(&pdu_response).await.whatever_context( "failed to send C-ECHO response object to SCU", )?; } else { @@ -254,13 +256,14 @@ fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { }; association .send(&pdu_response) + .await .whatever_context("failed to send response object to SCU")?; } } } Pdu::ReleaseRQ => { buffer.clear(); - association.send(&Pdu::ReleaseRP).unwrap_or_else(|e| { + association.send(&Pdu::ReleaseRP).await.unwrap_or_else(|e| { warn!( "Failed to send association release message to SCU: {}", snafu::Report::from_error(e) @@ -355,8 +358,9 @@ fn create_cecho_response(message_id: u16) -> InMemDicomObject Result<(), Box> { - let args = App::parse(); +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Arc::new(App::parse()); tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() @@ -380,23 +384,20 @@ fn main() -> Result<(), Box> { }); let listen_addr = SocketAddrV4::new(Ipv4Addr::from(0), args.port); - let listener = TcpListener::bind(listen_addr)?; + let listener = tokio::net::TcpListener::bind(listen_addr).await?; info!( "{} listening on: tcp://{}", &args.calling_ae_title, listen_addr ); - for stream in listener.incoming() { - match stream { - Ok(scu_stream) => { - if let Err(e) = run(scu_stream, &args) { - error!("{}", snafu::Report::from_error(e)); - } - } - Err(e) => { + loop { + let (socket, _addr) = listener.accept().await?; + let args = args.clone(); + tokio::task::spawn(async move { + if let Err(e) = run(socket, &args).await { error!("{}", snafu::Report::from_error(e)); } - } + }); } Ok(()) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index fd9f40fcc..24da2cc9c 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -15,7 +15,12 @@ byteordered = "0.6" dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.0", default-features = false } snafu = "0.8" +tokio = { version = "1.38.0", features = ["full"], optional = true } tracing = "0.1.34" [dev-dependencies] matches = "0.1.8" + +[features] +tokio = ["dep:tokio"] +default = ["tokio"] diff --git a/ul/src/address.rs b/ul/src/address.rs index 875403ebb..5251e47cf 100644 --- a/ul/src/address.rs +++ b/ul/src/address.rs @@ -7,14 +7,14 @@ //! The syntax is `«ae_title»@«network_address»:«port»`, //! which works not only with IPv4 and IPv6 addresses, //! but also with domain names. +#[cfg(feature = "tokio")] +use snafu::{ensure, AsErrorSource, ResultExt, Snafu}; use std::{ convert::TryFrom, net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}, str::FromStr, }; -use snafu::{ensure, AsErrorSource, ResultExt, Snafu}; - /// A specification for a full address to the target SCP: /// an application entity title, plus a generic address, /// typically a socket address. diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 06a41b979..78ad01172 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -4,27 +4,30 @@ //! in which this application entity is the one requesting the association. //! See [`ClientAssociationOptions`] //! for details and examples on how to create an association. +use std::{borrow::Cow, convert::TryInto, net::ToSocketAddrs, time::Duration}; +#[cfg(not(feature = "tokio"))] use std::{ - borrow::Cow, - convert::TryInto, io::Write, - net::{TcpStream, ToSocketAddrs}, time::Duration, + net::{TcpStream, ToSocketAddrs}, +}; +#[cfg(feature = "tokio")] +use tokio::{ + io::{AsyncRead, AsyncWriteExt}, + net::TcpStream, }; use crate::{ pdu::{ - reader::{read_pdu, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE}, - writer::write_pdu, - AbortRQSource, AssociationAC, AssociationRJ, AssociationRQ, Pdu, + read_pdu, write_pdu, AbortRQSource, AssociationAC, AssociationRJ, AssociationRQ, Pdu, PresentationContextProposed, PresentationContextResult, PresentationContextResultReason, - UserIdentity, UserIdentityType, UserVariableItem, + UserIdentity, UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, }, AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; use snafu::{ensure, Backtrace, ResultExt, Snafu}; use super::{ - pdata::{PDataReader, PDataWriter}, + //pdata::{PDataReader, PDataWriter}, uid::trim_uid, }; @@ -39,15 +42,15 @@ pub enum Error { source: std::io::Error, backtrace: Backtrace, }, - + /// Could not set tcp read timeout - SetReadTimeout{ + SetReadTimeout { source: std::io::Error, backtrace: Backtrace, }, /// Could not set tcp write timeout - SetWriteTimeout{ + SetWriteTimeout { source: std::io::Error, backtrace: Backtrace, }, @@ -55,13 +58,13 @@ pub enum Error { /// failed to send association request SendRequest { #[snafu(backtrace)] - source: crate::pdu::writer::Error, + source: crate::pdu::WriteError, }, /// failed to receive association response ReceiveResponse { #[snafu(backtrace)] - source: crate::pdu::reader::Error, + source: crate::pdu::ReadError, }, #[snafu(display("unexpected response from server `{:?}`", pdu))] @@ -98,7 +101,7 @@ pub enum Error { #[non_exhaustive] Send { #[snafu(backtrace)] - source: crate::pdu::writer::Error, + source: crate::pdu::WriteError, }, /// failed to send PDU message on wire @@ -119,7 +122,7 @@ pub enum Error { #[non_exhaustive] Receive { #[snafu(backtrace)] - source: crate::pdu::reader::Error, + source: crate::pdu::ReadError, }, } @@ -189,10 +192,8 @@ pub struct ClientAssociationOptions<'a> { saml_assertion: Option>, /// User identity JWT jwt: Option>, - /// TCP read timeout - read_timeout: Option, - /// TCP write timeout - write_timeout: Option, + /// Timeout for individual send/receive operations + timeout: Option, } impl<'a> Default for ClientAssociationOptions<'a> { @@ -207,15 +208,14 @@ impl<'a> Default for ClientAssociationOptions<'a> { // the list of requested presentation contexts presentation_contexts: Vec::new(), protocol_version: 1, - max_pdu_length: crate::pdu::reader::DEFAULT_MAX_PDU, + max_pdu_length: DEFAULT_MAX_PDU, strict: true, username: None, password: None, kerberos_service_ticket: None, saml_assertion: None, jwt: None, - read_timeout: None, - write_timeout: None, + timeout: None, } } } @@ -412,6 +412,7 @@ impl<'a> ClientAssociationOptions<'a> { self } + #[cfg(not(feature = "tokio"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -419,6 +420,15 @@ impl<'a> ClientAssociationOptions<'a> { self.establish_impl(AeAddr::new_socket_addr(address)) } + #[cfg(feature = "tokio")] + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + pub async fn establish(self, address: A) -> Result { + self.establish_impl(AeAddr::new_socket_addr(address)).await + } + + #[cfg(not(feature = "tokio"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -449,22 +459,49 @@ impl<'a> ClientAssociationOptions<'a> { } } - /// Set the read timeout for the underlying TCP socket - pub fn read_timeout(self, timeout: Duration) -> Self { - Self { - read_timeout: Some(timeout), - ..self + #[cfg(feature = "tokio")] + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + /// + /// This method allows you to specify the called AE title + /// alongside with the socket address. + /// See [AeAddr](`crate::AeAddr`) for more details. + /// However, the AE title in this parameter + /// is overridden by any `called_ae_title` option + /// previously received. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_ul::association::client::ClientAssociationOptions; + /// # fn run() -> Result<(), Box> { + /// let association = ClientAssociationOptions::new() + /// .with_abstract_syntax("1.2.840.10008.1.1") + /// // called AE title in address + /// .establish_with("MY-STORAGE@10.0.0.100:104")?; + /// # Ok(()) + /// # } + /// ``` + pub async fn establish_with(self, ae_address: &str) -> Result { + match ae_address.try_into() { + Ok(ae_address) => self.establish_impl(ae_address).await, + Err(_) => { + self.establish_impl(AeAddr::new_socket_addr(ae_address)) + .await + } } } - /// Set the write timeout for the underlying TCP socket - pub fn write_timeout(self, timeout: Duration) -> Self { + /// Set the read timeout for the underlying TCP socket + pub fn timeout(self, timeout: Duration) -> Self { Self { - write_timeout: Some(timeout), + timeout: Some(timeout), ..self } } + #[cfg(not(feature = "tokio"))] fn establish_impl(self, ae_address: AeAddr) -> Result where T: ToSocketAddrs, @@ -483,7 +520,7 @@ impl<'a> ClientAssociationOptions<'a> { saml_assertion, jwt, read_timeout, - write_timeout + write_timeout, } = self; // fail if no presentation contexts were provided: they represent intent, @@ -546,11 +583,12 @@ impl<'a> ClientAssociationOptions<'a> { user_variables, }); - let mut socket = std::net::TcpStream::connect(ae_address) - .context(ConnectSnafu)?; - socket.set_read_timeout(read_timeout) + let mut socket = std::net::TcpStream::connect(ae_address).context(ConnectSnafu)?; + socket + .set_read_timeout(read_timeout) .context(SetReadTimeoutSnafu)?; - socket.set_write_timeout(write_timeout) + socket + .set_write_timeout(write_timeout) .context(SetWriteTimeoutSnafu)?; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); // send request @@ -649,6 +687,191 @@ impl<'a> ClientAssociationOptions<'a> { } } + #[cfg(feature = "tokio")] + async fn establish_impl(self, ae_address: AeAddr) -> Result + where + T: ToSocketAddrs, + { + let ClientAssociationOptions { + calling_ae_title, + called_ae_title, + application_context_name, + presentation_contexts, + protocol_version, + max_pdu_length, + strict, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + timeout, + } = self; + + // fail if no presentation contexts were provided: they represent intent, + // should not be omitted by the user + ensure!( + !presentation_contexts.is_empty(), + MissingAbstractSyntaxSnafu + ); + + // choose called AE title + let called_ae_title: &str = match (&called_ae_title, ae_address.ae_title()) { + (Some(aec), Some(_)) => { + tracing::warn!( + "Option `called_ae_title` overrides the AE title to `{}`", + aec + ); + aec + } + (Some(aec), None) => aec, + (None, Some(aec)) => aec, + (None, None) => "ANY-SCP", + }; + + let presentation_contexts: Vec<_> = presentation_contexts + .into_iter() + .enumerate() + .map(|(i, presentation_context)| PresentationContextProposed { + id: (i + 1) as u8, + abstract_syntax: presentation_context.0.to_string(), + transfer_syntaxes: presentation_context + .1 + .iter() + .map(|uid| uid.to_string()) + .collect(), + }) + .collect(); + + let mut user_variables = vec![ + UserVariableItem::MaxLength(max_pdu_length), + UserVariableItem::ImplementationClassUID(IMPLEMENTATION_CLASS_UID.to_string()), + UserVariableItem::ImplementationVersionName(IMPLEMENTATION_VERSION_NAME.to_string()), + ]; + + if let Some(user_identity) = Self::determine_user_identity( + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + ) { + user_variables.push(UserVariableItem::UserIdentityItem(user_identity)); + } + + let msg = Pdu::AssociationRQ(AssociationRQ { + protocol_version, + calling_ae_title: calling_ae_title.to_string(), + called_ae_title: called_ae_title.to_string(), + application_context_name: application_context_name.to_string(), + presentation_contexts, + user_variables, + }); + let socket_addrs: Vec<_> = ae_address.to_socket_addrs().unwrap().collect(); + + let mut socket = TcpStream::connect(socket_addrs.as_slice()) + .await + .context(ConnectSnafu)?; + let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); + // send request + + write_pdu(&mut buffer, &msg) + .await + .context(SendRequestSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + buffer.clear(); + // receive response + let msg = read_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict) + .await + .context(ReceiveResponseSnafu)?; + + match msg { + Pdu::AssociationAC(AssociationAC { + protocol_version: protocol_version_scp, + application_context_name: _, + presentation_contexts: presentation_contexts_scp, + calling_ae_title: _, + called_ae_title: _, + user_variables, + }) => { + ensure!( + protocol_version == protocol_version_scp, + ProtocolVersionMismatchSnafu { + expected: protocol_version, + got: protocol_version_scp, + } + ); + + let acceptor_max_pdu_length = user_variables + .iter() + .find_map(|item| match item { + UserVariableItem::MaxLength(len) => Some(*len), + _ => None, + }) + .unwrap_or(DEFAULT_MAX_PDU); + + // treat 0 as the maximum size admitted by the standard + let acceptor_max_pdu_length = if acceptor_max_pdu_length == 0 { + MAXIMUM_PDU_SIZE + } else { + acceptor_max_pdu_length + }; + + let presentation_contexts: Vec<_> = presentation_contexts_scp + .into_iter() + .filter(|c| c.reason == PresentationContextResultReason::Acceptance) + .collect(); + if presentation_contexts.is_empty() { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer); + buffer.clear(); + return NoAcceptedPresentationContextsSnafu.fail(); + } + Ok(ClientAssociation { + presentation_contexts, + requestor_max_pdu_length: max_pdu_length, + acceptor_max_pdu_length, + socket, + buffer, + strict, + timeout, + }) + } + Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::ReleaseRQ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRP { .. } => { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer); + UnexpectedResponseSnafu { pdu }.fail() + } + pdu @ Pdu::Unknown { .. } => { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer); + UnknownResponseSnafu { pdu }.fail() + } + } + } fn determine_user_identity( username: Option, password: Option, @@ -736,6 +959,8 @@ pub struct ClientAssociation { buffer: Vec, /// whether to receive PDUs in strict mode strict: bool, + /// Send/Receive operation timeout + timeout: Option, } impl ClientAssociation { @@ -760,6 +985,7 @@ impl ClientAssociation { self.requestor_max_pdu_length } + #[cfg(not(feature = "tokio"))] /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -773,11 +999,37 @@ impl ClientAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } + #[cfg(feature = "tokio")] + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + self.buffer.clear(); + write_pdu(&mut self.buffer, msg).await.context(SendSnafu)?; + if self.buffer.len() > self.acceptor_max_pdu_length as usize { + return SendTooLongPduSnafu { + length: self.buffer.len(), + } + .fail(); + } + self.socket + .write_all(&self.buffer) + .await + .context(WireSendSnafu) + } + + #[cfg(not(feature = "tokio"))] /// Read a PDU message from the other intervenient. pub fn receive(&mut self) -> Result { read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict).context(ReceiveSnafu) } + #[cfg(feature = "tokio")] + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict) + .await + .context(ReceiveSnafu) + } + #[cfg(not(feature = "tokio"))] /// Gracefully terminate the association by exchanging release messages /// and then shutting down the TCP connection. pub fn release(mut self) -> Result<()> { @@ -786,6 +1038,16 @@ impl ClientAssociation { out } + #[cfg(feature = "tokio")] + /// Gracefully terminate the association by exchanging release messages + /// and then shutting down the TCP connection. + pub async fn release(mut self) -> Result<()> { + let out = self.release_impl().await; + let _ = self.socket.shutdown().await; + out + } + + #[cfg(not(feature = "tokio"))] /// Send an abort message and shut down the TCP connection, /// terminating the association. pub fn abort(mut self) -> Result<()> { @@ -797,6 +1059,18 @@ impl ClientAssociation { out } + #[cfg(feature = "tokio")] + /// Send an abort message and shut down the TCP connection, + /// terminating the association. + pub async fn abort(mut self) -> Result<()> { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }; + let out = self.send(&pdu).await; + let _ = self.socket.shutdown().await; + out + } + /// Obtain access to the inner TCP stream /// connected to the association acceptor. /// @@ -815,23 +1089,24 @@ impl ClientAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - PDataWriter::new( - &mut self.socket, - presentation_context_id, - self.acceptor_max_pdu_length, - ) - } + // pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + // PDataWriter::new( + // &mut self.socket, + // presentation_context_id, + // self.acceptor_max_pdu_length, + // ) + // } /// 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) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) - } + // pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + // PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) + // } + #[cfg(not(feature = "tokio"))] /// Release implementation function, /// which tries to send a release request and receive a release response. /// This is in a separate private function because @@ -855,8 +1130,35 @@ impl ClientAssociation { } Ok(()) } + + #[cfg(feature = "tokio")] + /// Release implementation function, + /// which tries to send a release request and receive a release response. + /// This is in a separate private function because + /// terminating a connection should still close the connection + /// if the exchange fails. + async fn release_impl(&mut self) -> Result<()> { + let pdu = Pdu::ReleaseRQ; + self.send(&pdu).await?; + let pdu = read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict) + .await + .context(ReceiveSnafu)?; + + match pdu { + Pdu::ReleaseRP => {} + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRQ { .. } => return UnexpectedResponseSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => return UnknownResponseSnafu { pdu }.fail(), + } + Ok(()) + } } +#[cfg(not(feature = "tokio"))] /// Automatically release the association and shut down the connection. impl Drop for ClientAssociation { fn drop(&mut self) { @@ -864,3 +1166,14 @@ impl Drop for ClientAssociation { let _ = self.socket.shutdown(std::net::Shutdown::Both); } } + +#[cfg(feature = "tokio")] +/// Automatically release the association and shut down the connection. +impl Drop for ClientAssociation { + fn drop(&mut self) { + async { + let _ = self.release_impl().await; + let _ = self.socket.shutdown().await; + }; + } +} diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index ba1cead91..7c7025c9b 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -19,8 +19,8 @@ pub mod client; pub mod server; mod uid; -pub(crate) mod pdata; +//pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; -pub use pdata::{PDataReader, PDataWriter}; +//pub use pdata::{PDataReader, PDataWriter}; pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 94c753020..f3e0f2a5f 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -5,7 +5,7 @@ use std::{ use tracing::warn; -use crate::{pdu::reader::PDU_HEADER_SIZE, read_pdu, Pdu}; +use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; /// A P-Data value writer. /// @@ -322,8 +322,7 @@ mod tests { use std::collections::VecDeque; use std::io::{Read, Write}; - use crate::pdu::reader::{read_pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE}; - use crate::pdu::Pdu; + use crate::pdu::{read_pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE, Pdu}; use crate::pdu::{PDataValue, PDataValueType}; use crate::write_pdu; diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 6568de8be..3b9aa8933 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -4,7 +4,11 @@ //! in which this application entity listens to incoming association requests. //! See [`ServerAssociationOptions`] //! for details and examples on how to create an association. -use std::{borrow::Cow, io::Write, net::TcpStream}; +use std::borrow::Cow; +#[cfg(not(feature = "tokio"))] +use std::{io::Write, net::TcpStream}; +#[cfg(feature = "tokio")] +use tokio::{io::AsyncWriteExt, net::TcpStream}; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; @@ -12,18 +16,16 @@ use snafu::{ensure, Backtrace, ResultExt, Snafu}; use crate::{ pdu::{ - reader::{read_pdu, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE}, - writer::write_pdu, - AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ, - AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ, - Pdu, PresentationContextResult, PresentationContextResultReason, UserIdentity, - UserVariableItem, + read_pdu, write_pdu, AbortRQServiceProviderReason, AbortRQSource, AssociationAC, + AssociationRJ, AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, + AssociationRQ, Pdu, PresentationContextResult, PresentationContextResultReason, + UserIdentity, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, }, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; use super::{ - pdata::{PDataReader, PDataWriter}, + //pdata::{PDataReader, PDataWriter}, uid::trim_uid, }; @@ -36,19 +38,19 @@ pub enum Error { /// failed to receive association request ReceiveRequest { #[snafu(backtrace)] - source: crate::pdu::reader::Error, + source: crate::pdu::ReadError, }, /// failed to send association response SendResponse { #[snafu(backtrace)] - source: crate::pdu::writer::Error, + source: crate::pdu::WriteError, }, /// failed to prepare PDU Send { #[snafu(backtrace)] - source: crate::pdu::writer::Error, + source: crate::pdu::WriteError, }, /// failed to send PDU over the wire @@ -60,7 +62,7 @@ pub enum Error { /// failed to receive PDU Receive { #[snafu(backtrace)] - source: crate::pdu::reader::Error, + source: crate::pdu::ReadError, }, #[snafu(display("unexpected request from SCU `{:?}`", pdu))] @@ -232,7 +234,7 @@ impl<'a> Default for ServerAssociationOptions<'a, AcceptAny> { abstract_syntax_uids: Vec::new(), transfer_syntax_uids: Vec::new(), protocol_version: 1, - max_pdu_length: crate::pdu::reader::DEFAULT_MAX_PDU, + max_pdu_length: DEFAULT_MAX_PDU, strict: true, promiscuous: false, } @@ -353,6 +355,7 @@ where self } + #[cfg(not(feature = "tokio"))] /// Negotiate an association with the given TCP stream. pub fn establish(&self, mut socket: TcpStream) -> Result { ensure!( @@ -527,6 +530,191 @@ where } } + #[cfg(feature = "tokio")] + /// Negotiate an association with the given TCP stream. + pub async fn establish(&self, mut socket: TcpStream) -> Result { + ensure!( + !self.abstract_syntax_uids.is_empty() || self.promiscuous, + MissingAbstractSyntaxSnafu + ); + + let max_pdu_length = self.max_pdu_length; + + let pdu = read_pdu(&mut socket, max_pdu_length, self.strict) + .await + .context(ReceiveRequestSnafu)?; + let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); + match pdu { + Pdu::AssociationRQ(AssociationRQ { + protocol_version, + calling_ae_title, + called_ae_title, + application_context_name, + presentation_contexts, + user_variables, + }) => { + if protocol_version != self.protocol_version { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser( + AssociationRJServiceUserReason::NoReasonGiven, + ), + }), + ) + .await + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return RejectedSnafu.fail(); + } + + if application_context_name != self.application_context_name { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser( + AssociationRJServiceUserReason::ApplicationContextNameNotSupported, + ), + }), + ) + .await + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return RejectedSnafu.fail(); + } + + self.ae_access_control + .check_access( + &self.ae_title, + &calling_ae_title, + &called_ae_title, + user_variables + .iter() + .find_map(|user_variable| match user_variable { + UserVariableItem::UserIdentityItem(user_identity) => { + Some(user_identity) + } + _ => None, + }), + ) + .map(Ok) + .unwrap_or_else(|reason| { + async { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser(reason), + }), + ) + .await + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return Err::<(), Error>(RejectedSnafu.build()); + }; + Ok(()) + })?; + + // fetch requested maximum PDU length + let requestor_max_pdu_length = user_variables + .iter() + .find_map(|item| match item { + UserVariableItem::MaxLength(len) => Some(*len), + _ => None, + }) + .unwrap_or(DEFAULT_MAX_PDU); + + // treat 0 as the maximum size admitted by the standard + let requestor_max_pdu_length = if requestor_max_pdu_length == 0 { + MAXIMUM_PDU_SIZE + } else { + requestor_max_pdu_length + }; + + let presentation_contexts: Vec<_> = presentation_contexts + .into_iter() + .map(|pc| { + if !self + .abstract_syntax_uids + .contains(&trim_uid(Cow::from(pc.abstract_syntax))) + && !self.promiscuous + { + return PresentationContextResult { + id: pc.id, + reason: PresentationContextResultReason::AbstractSyntaxNotSupported, + transfer_syntax: "1.2.840.10008.1.2".to_string(), + }; + } + + let (transfer_syntax, reason) = self + .choose_ts(pc.transfer_syntaxes) + .map(|ts| (ts, PresentationContextResultReason::Acceptance)) + .unwrap_or_else(|| { + ( + "1.2.840.10008.1.2".to_string(), + PresentationContextResultReason::TransferSyntaxesNotSupported, + ) + }); + + PresentationContextResult { + id: pc.id, + reason, + transfer_syntax, + } + }) + .collect(); + + write_pdu( + &mut buffer, + &Pdu::AssociationAC(AssociationAC { + protocol_version: self.protocol_version, + application_context_name, + presentation_contexts: presentation_contexts.clone(), + calling_ae_title: calling_ae_title.clone(), + called_ae_title, + user_variables: vec![ + UserVariableItem::MaxLength(max_pdu_length), + UserVariableItem::ImplementationClassUID( + IMPLEMENTATION_CLASS_UID.to_string(), + ), + UserVariableItem::ImplementationVersionName( + IMPLEMENTATION_VERSION_NAME.to_string(), + ), + ], + }), + ) + .await + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + + Ok(ServerAssociation { + presentation_contexts, + requestor_max_pdu_length, + acceptor_max_pdu_length: max_pdu_length, + socket, + client_ae_title: calling_ae_title, + buffer, + strict: self.strict, + }) + } + Pdu::ReleaseRQ => { + write_pdu(&mut buffer, &Pdu::ReleaseRP) + .await + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + AbortedSnafu.fail() + } + pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRP + | pdu @ Pdu::AbortRQ { .. } => UnexpectedRequestSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => UnknownRequestSnafu { pdu }.fail(), + } + } + /// From a sequence of transfer syntaxes, /// choose the first transfer syntax to /// - be on the options' list of transfer syntaxes, and @@ -594,6 +782,7 @@ impl ServerAssociation { &self.client_ae_title } + #[cfg(not(feature = "tokio"))] /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -607,14 +796,41 @@ impl ServerAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } + #[cfg(feature = "tokio")] + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + self.buffer.clear(); + write_pdu(&mut self.buffer, msg).await.context(SendSnafu)?; + if self.buffer.len() > self.requestor_max_pdu_length as usize { + return SendTooLongPduSnafu { + length: self.buffer.len(), + } + .fail(); + } + self.socket + .write_all(&self.buffer) + .await + .context(WireSendSnafu) + } + + #[cfg(not(feature = "tokio"))] /// Read a PDU message from the other intervenient. pub fn receive(&mut self) -> Result { read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict).context(ReceiveSnafu) } + #[cfg(feature = "tokio")] + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict) + .await + .context(ReceiveSnafu) + } + /// Send a provider initiated abort message /// and shut down the TCP connection, /// terminating the association. + #[cfg(not(feature = "tokio"))] pub fn abort(mut self) -> Result<()> { let pdu = Pdu::AbortRQ { source: AbortRQSource::ServiceProvider( @@ -626,11 +842,27 @@ impl ServerAssociation { out } + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + #[cfg(feature = "tokio")] + pub async fn abort(mut self) -> Result<()> { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceProvider( + AbortRQServiceProviderReason::ReasonNotSpecified, + ), + }; + let out = self.send(&pdu).await; + let _ = self.socket.shutdown().await; + out + } + /// 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. + #[cfg(not(feature = "tokio"))] pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { PDataWriter::new( &mut self.socket, @@ -639,14 +871,28 @@ impl ServerAssociation { ) } + /// 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. + // #[cfg(feature = "tokio")] + // pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + // PDataWriter::new( + // &mut self.socket, + // presentation_context_id, + // self.requestor_max_pdu_length, + // ) + // } + /// 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) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) - } + // pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + // PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) + // } /// Obtain access to the inner TCP stream /// connected to the association acceptor. diff --git a/ul/src/lib.rs b/ul/src/lib.rs index 992c43a54..8eafec261 100644 --- a/ul/src/lib.rs +++ b/ul/src/lib.rs @@ -38,6 +38,6 @@ pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.6"; pub use address::{AeAddr, FullAeAddr}; pub use association::client::{ClientAssociation, ClientAssociationOptions}; pub use association::server::{ServerAssociation, ServerAssociationOptions}; -pub use pdu::reader::read_pdu; -pub use pdu::writer::write_pdu; +pub use pdu::read_pdu; +pub use pdu::write_pdu; pub use pdu::Pdu; diff --git a/ul/src/pdu/mod.rs b/ul/src/pdu/mod.rs index 6930c892b..e2e342e38 100644 --- a/ul/src/pdu/mod.rs +++ b/ul/src/pdu/mod.rs @@ -4,13 +4,157 @@ //! protocol data units (PDUs) according to //! the standard message exchange mechanisms, //! as well as readers and writers of PDUs from arbitrary data sources. +#[cfg(not(feature = "tokio"))] pub mod reader; +#[cfg(feature = "tokio")] +pub mod reader_nonblocking; +#[cfg(not(feature = "tokio"))] pub mod writer; +#[cfg(feature = "tokio")] +pub mod writer_nonblocking; use std::fmt::Display; +#[cfg(not(feature = "tokio"))] pub use reader::read_pdu; +#[cfg(feature = "tokio")] +pub use reader_nonblocking::read_pdu; +use snafu::{Backtrace, Snafu}; +#[cfg(not(feature = "tokio"))] pub use writer::write_pdu; +#[cfg(feature = "tokio")] +pub use writer_nonblocking::{write_pdu, WriteChunkError}; + +/// The default maximum PDU size +pub const DEFAULT_MAX_PDU: u32 = 16_384; + +/// The minimum PDU size, +/// as specified by the standard +pub const MINIMUM_PDU_SIZE: u32 = 4_096; + +/// The maximum PDU size, +/// as specified by the standard +pub const MAXIMUM_PDU_SIZE: u32 = 131_072; + +/// The length of the PDU header in bytes, +/// comprising the PDU type (1 byte), +/// reserved byte (1 byte), +/// and PDU length (4 bytes). +pub const PDU_HEADER_SIZE: u32 = 6; + +#[derive(Debug, Snafu)] +#[non_exhaustive] +pub enum WriteError { + #[snafu(display("Could not write chunk of {} PDU structure", name))] + WriteChunk { + /// the name of the PDU structure + name: &'static str, + source: WriteChunkError, + }, + + #[snafu(display("Could not write field `{}`", field))] + WriteField { + field: &'static str, + backtrace: Backtrace, + source: std::io::Error, + }, + + #[snafu(display("Could not write {} reserved bytes", bytes))] + WriteReserved { + bytes: u32, + backtrace: Backtrace, + source: std::io::Error, + }, + + #[snafu(display("Could not write field `{}`", field))] + EncodeField { + field: &'static str, + #[snafu(backtrace)] + source: dicom_encoding::text::EncodeTextError, + }, +} + +#[derive(Debug, Snafu)] +#[non_exhaustive] +pub enum ReadError { + #[snafu(display("Invalid max PDU length {}", max_pdu_length))] + InvalidMaxPdu { + max_pdu_length: u32, + backtrace: Backtrace, + }, + + #[snafu(display("No PDU available"))] + NoPduAvailable { backtrace: Backtrace }, + + #[snafu(display("Could not read PDU"))] + ReadPdu { + source: std::io::Error, + backtrace: Backtrace, + }, + + #[snafu(display("Could not read PDU item"))] + ReadPduItem { + source: std::io::Error, + backtrace: Backtrace, + }, + + #[snafu(display("Could not read PDU field `{}`", field))] + ReadPduField { + field: &'static str, + source: std::io::Error, + backtrace: Backtrace, + }, + + #[snafu(display("Invalid item length {} (must be >=2)", length))] + InvalidItemLength { length: u32 }, + + #[snafu(display("Could not read {} reserved bytes", bytes))] + ReadReserved { + bytes: u32, + source: std::io::Error, + backtrace: Backtrace, + }, + + #[snafu(display( + "Incoming pdu was too large: length {}, maximum is {}", + pdu_length, + max_pdu_length + ))] + PduTooLarge { + pdu_length: u32, + max_pdu_length: u32, + backtrace: Backtrace, + }, + #[snafu(display("PDU contained an invalid value {:?}", var_item))] + InvalidPduVariable { + var_item: PduVariableItem, + backtrace: Backtrace, + }, + #[snafu(display("Multiple transfer syntaxes were accepted"))] + MultipleTransferSyntaxesAccepted { backtrace: Backtrace }, + #[snafu(display("Invalid reject source or reason"))] + InvalidRejectSourceOrReason { backtrace: Backtrace }, + #[snafu(display("Invalid abort service provider"))] + InvalidAbortSourceOrReason { backtrace: Backtrace }, + #[snafu(display("Invalid presentation context result reason"))] + InvalidPresentationContextResultReason { backtrace: Backtrace }, + #[snafu(display("invalid transfer syntax sub-item"))] + InvalidTransferSyntaxSubItem { backtrace: Backtrace }, + #[snafu(display("unknown presentation context sub-item"))] + UnknownPresentationContextSubItem { backtrace: Backtrace }, + #[snafu(display("Could not decode text field `{}`", field))] + DecodeText { + field: &'static str, + #[snafu(backtrace)] + source: dicom_encoding::text::DecodeTextError, + }, + #[snafu(display("Missing application context name"))] + MissingApplicationContextName { backtrace: Backtrace }, + #[snafu(display("Missing abstract syntax"))] + MissingAbstractSyntax { backtrace: Backtrace }, + #[snafu(display("Missing transfer syntax"))] + MissingTransferSyntax { backtrace: Backtrace }, +} /// Message component for a proposed presentation context. #[derive(Clone, Eq, PartialEq, PartialOrd, Hash, Debug)] diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index 3423f2710..98c8a987a 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -6,106 +6,7 @@ use snafu::{ensure, Backtrace, OptionExt, ResultExt, Snafu}; use std::io::{Cursor, ErrorKind, Read, Seek, SeekFrom}; use tracing::warn; -/// The default maximum PDU size -pub const DEFAULT_MAX_PDU: u32 = 16_384; - -/// The minimum PDU size, -/// as specified by the standard -pub const MINIMUM_PDU_SIZE: u32 = 4_096; - -/// The maximum PDU size, -/// as specified by the standard -pub const MAXIMUM_PDU_SIZE: u32 = 131_072; - -/// The length of the PDU header in bytes, -/// comprising the PDU type (1 byte), -/// reserved byte (1 byte), -/// and PDU length (4 bytes). -pub const PDU_HEADER_SIZE: u32 = 6; - -#[derive(Debug, Snafu)] -#[non_exhaustive] -pub enum Error { - #[snafu(display("Invalid max PDU length {}", max_pdu_length))] - InvalidMaxPdu { - max_pdu_length: u32, - backtrace: Backtrace, - }, - - #[snafu(display("No PDU available"))] - NoPduAvailable { backtrace: Backtrace }, - - #[snafu(display("Could not read PDU"))] - ReadPdu { - source: std::io::Error, - backtrace: Backtrace, - }, - - #[snafu(display("Could not read PDU item"))] - ReadPduItem { - source: std::io::Error, - backtrace: Backtrace, - }, - - #[snafu(display("Could not read PDU field `{}`", field))] - ReadPduField { - field: &'static str, - source: std::io::Error, - backtrace: Backtrace, - }, - - #[snafu(display("Invalid item length {} (must be >=2)", length))] - InvalidItemLength { length: u32 }, - - #[snafu(display("Could not read {} reserved bytes", bytes))] - ReadReserved { - bytes: u32, - source: std::io::Error, - backtrace: Backtrace, - }, - - #[snafu(display( - "Incoming pdu was too large: length {}, maximum is {}", - pdu_length, - max_pdu_length - ))] - PduTooLarge { - pdu_length: u32, - max_pdu_length: u32, - backtrace: Backtrace, - }, - #[snafu(display("PDU contained an invalid value {:?}", var_item))] - InvalidPduVariable { - var_item: PduVariableItem, - backtrace: Backtrace, - }, - #[snafu(display("Multiple transfer syntaxes were accepted"))] - MultipleTransferSyntaxesAccepted { backtrace: Backtrace }, - #[snafu(display("Invalid reject source or reason"))] - InvalidRejectSourceOrReason { backtrace: Backtrace }, - #[snafu(display("Invalid abort service provider"))] - InvalidAbortSourceOrReason { backtrace: Backtrace }, - #[snafu(display("Invalid presentation context result reason"))] - InvalidPresentationContextResultReason { backtrace: Backtrace }, - #[snafu(display("invalid transfer syntax sub-item"))] - InvalidTransferSyntaxSubItem { backtrace: Backtrace }, - #[snafu(display("unknown presentation context sub-item"))] - UnknownPresentationContextSubItem { backtrace: Backtrace }, - #[snafu(display("Could not decode text field `{}`", field))] - DecodeText { - field: &'static str, - #[snafu(backtrace)] - source: dicom_encoding::text::DecodeTextError, - }, - #[snafu(display("Missing application context name"))] - MissingApplicationContextName { backtrace: Backtrace }, - #[snafu(display("Missing abstract syntax"))] - MissingAbstractSyntax { backtrace: Backtrace }, - #[snafu(display("Missing transfer syntax"))] - MissingTransferSyntax { backtrace: Backtrace }, -} - -pub type Result = std::result::Result; +pub type Result = std::result::Result; pub fn read_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result where diff --git a/ul/src/pdu/reader_nonblocking.rs b/ul/src/pdu/reader_nonblocking.rs new file mode 100644 index 000000000..fa746661c --- /dev/null +++ b/ul/src/pdu/reader_nonblocking.rs @@ -0,0 +1,959 @@ +/// PDU reader module +use crate::pdu::*; +use dicom_encoding::text::{DefaultCharacterSetCodec, TextCodec}; +use snafu::{ensure, OptionExt, ResultExt}; +use std::io::{Cursor, ErrorKind, Seek, SeekFrom}; +use tokio::io::{AsyncRead, AsyncReadExt}; +use tracing::warn; + +pub type Result = std::result::Result; + +pub async fn read_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result +where + R: AsyncRead + Unpin, +{ + ensure!( + (MINIMUM_PDU_SIZE..=MAXIMUM_PDU_SIZE).contains(&max_pdu_length), + InvalidMaxPduSnafu { max_pdu_length } + ); + + // If we can't read 2 bytes here, that means that there is no PDU + // available. Normally, we want to just return the UnexpectedEof error. However, + // this method can block and wake up when stream is closed, so in this case, we + // want to know if we had trouble even beginning to read a PDU. We still return + // UnexpectedEof if we get after we have already began reading a PDU message. + let mut bytes = [0; 2]; + if let Err(e) = reader.read_exact(&mut bytes).await { + ensure!(e.kind() != ErrorKind::UnexpectedEof, NoPduAvailableSnafu); + return Err(e).context(ReadPduFieldSnafu { field: "type" }); + } + + let pdu_type = bytes[0]; + let pdu_length = reader + .read_u32() + .await + .context(ReadPduFieldSnafu { field: "length" })?; + + // Check max_pdu_length + if strict { + ensure!( + pdu_length <= max_pdu_length, + PduTooLargeSnafu { + pdu_length, + max_pdu_length + } + ); + } else if pdu_length > max_pdu_length { + ensure!( + pdu_length <= MAXIMUM_PDU_SIZE, + PduTooLargeSnafu { + pdu_length, + max_pdu_length: MAXIMUM_PDU_SIZE + } + ); + tracing::warn!( + "Incoming pdu was too large: length {}, maximum is {}", + pdu_length, + max_pdu_length + ); + } + + let bytes = read_n(reader, pdu_length as usize) + .await + .context(ReadPduSnafu)?; + let mut cursor = Cursor::new(bytes); + let codec = DefaultCharacterSetCodec; + + match pdu_type { + 0x01 => { + // A-ASSOCIATE-RQ PDU Structure + + let mut application_context_name: Option = None; + let mut presentation_contexts = vec![]; + let mut user_variables = vec![]; + + // 7-8 - Protocol-version - This two byte field shall use one bit to identify each + // version of the DICOM UL protocol supported by the calling end-system. This is + // Version 1 and shall be identified with bit 0 set. A receiver of this PDU + // implementing only this version of the DICOM UL protocol shall only test that bit + // 0 is set. + let protocol_version = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Protocol-version", + })?; + + // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not + // tested to this value when received. + cursor + .read_u16() + .await + .context(ReadReservedSnafu { bytes: 2_u32 })?; + + // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be encoded + // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and + // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) + // meaning "no Application Name specified" shall not be used. For a complete + // description of the use of this field, see Section 7.1.1.4. + let mut ae_bytes = [0; 16]; + cursor + .read_exact(&mut ae_bytes) + .await + .context(ReadPduFieldSnafu { + field: "Called-AE-title", + })?; + let called_ae_title = codec + .decode(&ae_bytes) + .context(DecodeTextSnafu { + field: "Called-AE-title", + })? + .trim() + .to_string(); + + // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded as + // 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and + // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) + // meaning "no Application Name specified" shall not be used. For a complete + // description of the use of this field, see Section 7.1.1.3. + let mut ae_bytes = [0; 16]; + cursor + .read_exact(&mut ae_bytes) + .await + .context(ReadPduFieldSnafu { + field: "Calling-AE-title", + })?; + let calling_ae_title = codec + .decode(&ae_bytes) + .context(DecodeTextSnafu { + field: "Calling-AE-title", + })? + .trim() + .to_string(); + + // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all + // bytes but not tested to this value when received + cursor + .seek(SeekFrom::Current(32)) + .context(ReadReservedSnafu { bytes: 32_u32 })?; + + // 75-xxx - Variable items - This variable field shall contain the following items: + // one Application Context Item, one or more Presentation Context Items and one User + // Information Item. For a complete description of the use of these items see + // Section 7.1.1.2, Section 7.1.1.13, and Section 7.1.1.6. + while cursor.position() < cursor.get_ref().len() as u64 { + match read_pdu_variable(&mut cursor, &codec).await? { + PduVariableItem::ApplicationContext(val) => { + application_context_name = Some(val); + } + PduVariableItem::PresentationContextProposed(val) => { + presentation_contexts.push(val); + } + PduVariableItem::UserVariables(val) => { + user_variables = val; + } + var_item => { + return InvalidPduVariableSnafu { var_item }.fail(); + } + } + } + + Ok(Pdu::AssociationRQ(AssociationRQ { + protocol_version, + application_context_name: application_context_name + .context(MissingApplicationContextNameSnafu)?, + called_ae_title, + calling_ae_title, + presentation_contexts, + user_variables, + })) + } + 0x02 => { + // A-ASSOCIATE-AC PDU Structure + + let mut application_context_name: Option = None; + let mut presentation_contexts = vec![]; + let mut user_variables = vec![]; + + // 7-8 - Protocol-version - This two byte field shall use one bit to identify each + // version of the DICOM UL protocol supported by the calling end-system. This is + // Version 1 and shall be identified with bit 0 set. A receiver of this PDU + // implementing only this version of the DICOM UL protocol shall only test that bit + // 0 is set. + let protocol_version = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Protocol-version", + })?; + + // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not + // tested to this value when received. + cursor + .read_u16() + .await + .context(ReadReservedSnafu { bytes: 2_u32 })?; + + // 11-26 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + let mut ae_bytes = [0; 16]; + cursor + .read_exact(&mut ae_bytes) + .await + .context(ReadPduFieldSnafu { + field: "Called-AE-title", + })?; + let called_ae_title = codec + .decode(&ae_bytes) + .context(DecodeTextSnafu { + field: "Called-AE-title", + })? + .trim() + .to_string(); + + // 27-42 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + let mut ae_bytes = [0; 16]; + cursor + .read_exact(&mut ae_bytes) + .await + .context(ReadPduFieldSnafu { + field: "Calling-AE-title", + })?; + let calling_ae_title = codec + .decode(&ae_bytes) + .context(DecodeTextSnafu { + field: "Calling-AE-title", + })? + .trim() + .to_string(); + + // 43-74 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + cursor + .seek(SeekFrom::Current(32)) + .context(ReadReservedSnafu { bytes: 32_u32 })?; + + // 75-xxx - Variable items - This variable field shall contain the following items: + // one Application Context Item, one or more Presentation Context Item(s) and one + // User Information Item. For a complete description of these items see Section + // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. + while cursor.position() < cursor.get_ref().len() as u64 { + match read_pdu_variable(&mut cursor, &codec).await? { + PduVariableItem::ApplicationContext(val) => { + application_context_name = Some(val); + } + PduVariableItem::PresentationContextResult(val) => { + presentation_contexts.push(val); + } + PduVariableItem::UserVariables(val) => { + user_variables = val; + } + var_item => { + return InvalidPduVariableSnafu { var_item }.fail(); + } + } + } + + Ok(Pdu::AssociationAC(AssociationAC { + protocol_version, + application_context_name: application_context_name + .context(MissingApplicationContextNameSnafu)?, + called_ae_title, + calling_ae_title, + presentation_contexts, + user_variables, + })) + } + 0x03 => { + // A-ASSOCIATE-RJ PDU Structure + + // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 8 - Result - This Result field shall contain an integer value encoded as an unsigned + // binary number. One of the following values shall be used: + // 1 - rejected-permanent + // 2 - rejected-transient + let result = AssociationRJResult::from( + cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Result" })?, + ) + .context(InvalidRejectSourceOrReasonSnafu)?; + + // 9 - Source - This Source field shall contain an integer value encoded as an unsigned + // binary number. One of the following values shall be used: 1 - DICOM UL + // service-user 2 - DICOM UL service-provider (ACSE related function) + // 3 - DICOM UL service-provider (Presentation related function) + // 10 - Reason/Diag. - This field shall contain an integer value encoded as an unsigned + // binary number. If the Source field has the value (1) "DICOM UL + // service-user", it shall take one of the following: + // 1 - no-reason-given + // 2 - application-context-name-not-supported + // 3 - calling-AE-title-not-recognized + // 4-6 - reserved + // 7 - called-AE-title-not-recognized + // 8-10 - reserved + // If the Source field has the value (2) "DICOM UL service provided (ACSE related + // function)", it shall take one of the following: 1 - no-reason-given + // 2 - protocol-version-not-supported + // If the Source field has the value (3) "DICOM UL service provided (Presentation + // related function)", it shall take one of the following: 0 - reserved + // 1 - temporary-congestio + // 2 - local-limit-exceeded + // 3-7 - reserved + let source = AssociationRJSource::from( + cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Source" })?, + cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Reason/Diag.", + })?, + ) + .context(InvalidRejectSourceOrReasonSnafu)?; + + Ok(Pdu::AssociationRJ(AssociationRJ { result, source })) + } + 0x04 => { + // P-DATA-TF PDU Structure + + // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain one + // or more Presentation-data-value Items(s). For a complete description of the use of + // this field see Section 9.3.5.1 + let mut values = vec![]; + while cursor.position() < cursor.get_ref().len() as u64 { + // Presentation Data Value Item Structure + + // 1-4 - Item-length - This Item-length shall be the number of bytes from the first + // byte of the following field to the last byte of the Presentation-data-value + // field. It shall be encoded as an unsigned binary number. + let item_length = cursor.read_u32().await.context(ReadPduFieldSnafu { + field: "Item-Length", + })?; + + ensure!( + item_length >= 2, + InvalidItemLengthSnafu { + length: item_length + } + ); + + // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd + // integers between 1 and 255, encoded as an unsigned binary number. For a complete + // description of the use of this field see Section 7.1.1.13. + let presentation_context_id = + cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Presentation-context-ID", + })?; + + // 6-xxx - Presentation-data-value - This Presentation-data-value field shall + // contain DICOM message information (command and/or data set) with a message + // control header. For a complete description of the use of this field see Annex E. + + // The Message Control Header shall be made of one byte with the least significant + // bit (bit 0) taking one of the following values: If bit 0 is set + // to 1, the following fragment shall contain Message Command information. + // If bit 0 is set to 0, the following fragment shall contain Message Data Set + // information. The next least significant bit (bit 1) shall be + // defined by the following rules: If bit 1 is set to 1, the + // following fragment shall contain the last fragment of a Message Data Set or of a + // Message Command. If bit 1 is set to 0, the following fragment + // does not contain the last fragment of a Message Data Set or of a Message Command. + let header = cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Message Control Header", + })?; + + let value_type = if header & 0x01 > 0 { + PDataValueType::Command + } else { + PDataValueType::Data + }; + let is_last = (header & 0x02) > 0; + + let data = read_n(&mut cursor, (item_length - 2) as usize) + .await + .context(ReadPduFieldSnafu { + field: "Presentation-data-value", + })?; + + values.push(PDataValue { + presentation_context_id, + value_type, + is_last, + data, + }) + } + + Ok(Pdu::PData { data: values }) + } + 0x05 => { + // A-RELEASE-RQ PDU Structure + + // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not + // tested to this value when received. + cursor + .seek(SeekFrom::Current(4)) + .context(ReadReservedSnafu { bytes: 4_u32 })?; + + Ok(Pdu::ReleaseRQ) + } + 0x06 => { + // A-RELEASE-RP PDU Structure + + // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not + // tested to this value when received. + cursor + .seek(SeekFrom::Current(4)) + .context(ReadReservedSnafu { bytes: 4_u32 })?; + + Ok(Pdu::ReleaseRP) + } + 0x07 => { + // A-ABORT PDU Structure + + // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + let mut buf = [0u8; 2]; + cursor + .read_exact(&mut buf) + .await + .context(ReadReservedSnafu { bytes: 2_u32 })?; + + // 9 - Source - This Source field shall contain an integer value encoded as an unsigned + // binary number. One of the following values shall be used: + // - 0 - DICOM UL service-user (initiated abort) + // - 1 - reserved + // - 2 - DICOM UL service-provider (initiated abort) + // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned + // binary number. If the Source field has the value (2) "DICOM UL + // service-provider", it shall take one of the following: + // - 0 - reason-not-specified1 - unrecognized-PDU + // - 2 - unexpected-PDU + // - 3 - reserved + // - 4 - unrecognized-PDU parameter + // - 5 - unexpected-PDU parameter + // - 6 - invalid-PDU-parameter value + let source = AbortRQSource::from( + cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Source" })?, + cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Reason/Diag", + })?, + ) + .context(InvalidAbortSourceOrReasonSnafu)?; + + Ok(Pdu::AbortRQ { source }) + } + _ => { + let data = read_n(&mut cursor, pdu_length as usize) + .await + .context(ReadPduFieldSnafu { field: "Unknown" })?; + Ok(Pdu::Unknown { pdu_type, data }) + } + } +} + +async fn read_n(reader: &mut R, bytes_to_read: usize) -> std::io::Result> +where + R: AsyncRead + Unpin, +{ + let mut result = Vec::new(); + reader + .take(bytes_to_read as u64) + .read_to_end(&mut result) + .await?; + Ok(result) +} + +async fn read_pdu_variable(reader: &mut R, codec: &dyn TextCodec) -> Result +where + R: AsyncRead + Unpin, +{ + // 1 - Item-type - XXH + let item_type = reader + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved + reader + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 3-4 - Item-length + let item_length = reader.read_u16().await.context(ReadPduFieldSnafu { + field: "Item-length", + })?; + + let bytes = read_n(reader, item_length as usize) + .await + .context(ReadPduItemSnafu)?; + let mut cursor = Cursor::new(bytes); + + match item_type { + 0x10 => { + // Application Context Item Structure + + // 5-xxx - Application-context-name - A valid Application-context-name shall be encoded + // as defined in Annex F. For a description of the use of this field see Section + // 7.1.1.2. Application-context-names are structured as UIDs as defined in PS3.5 (see + // Annex A for an overview of this concept). DICOM Application-context-names are + // registered in PS3.7. + let val = codec + .decode(&cursor.into_inner()) + .context(DecodeTextSnafu { + field: "Application-context-name", + })?; + Ok(PduVariableItem::ApplicationContext(val)) + } + 0x20 => { + // Presentation Context Item Structure (proposed) + + let mut abstract_syntax: Option = None; + let mut transfer_syntaxes = vec![]; + + // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers + // between 1 and 255, encoded as an unsigned binary number. For a complete description + // of the use of this field see Section 7.1.1.13. + let presentation_context_id = cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Presentation-context-ID", + })?; + + // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field shall contain the + // following sub-items: one Abstract Syntax and one or more Transfer Syntax(es). For a + // complete description of the use and encoding of these sub-items see Section 9.3.2.2.1 + // and Section 9.3.2.2.2. + while cursor.position() < cursor.get_ref().len() as u64 { + // 1 - Item-type - XXH + let item_type = cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested + // to this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 3-4 - Item-length + let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Item-length", + })?; + + match item_type { + 0x30 => { + // Abstract Syntax Sub-Item Structure + + // 5-xxx - Abstract-syntax-name - This variable field shall contain the + // Abstract-syntax-name related to the proposed presentation context. A + // valid Abstract-syntax-name shall be encoded as defined in Annex F. For a + // description of the use of this field see Section 7.1.1.13. + // Abstract-syntax-names are structured as UIDs as defined in PS3.5 (see + // Annex B for an overview of this concept). DICOM Abstract-syntax-names are + // registered in PS3.4. + abstract_syntax = Some( + codec + .decode(&read_n(&mut cursor, item_length as usize).await.context( + ReadPduFieldSnafu { + field: "Abstract-syntax-name", + }, + )?) + .context(DecodeTextSnafu { + field: "Abstract-syntax-name", + })? + .trim() + .to_string(), + ); + } + 0x40 => { + // Transfer Syntax Sub-Item Structure + + // 5-xxx - Transfer-syntax-name(s) - This variable field shall contain the + // Transfer-syntax-name proposed for this presentation context. A valid + // Transfer-syntax-name shall be encoded as defined in Annex F. For a + // description of the use of this field see Section 7.1.1.13. + // Transfer-syntax-names are structured as UIDs as defined in PS3.5 (see + // Annex B for an overview of this concept). DICOM Transfer-syntax-names are + // registered in PS3.5. + transfer_syntaxes.push( + codec + .decode(&read_n(&mut cursor, item_length as usize).await.context( + ReadPduFieldSnafu { + field: "Transfer-syntax-name", + }, + )?) + .context(DecodeTextSnafu { + field: "Transfer-syntax-name", + })? + .trim() + .to_string(), + ); + } + _ => { + return UnknownPresentationContextSubItemSnafu.fail(); + } + } + } + + Ok(PduVariableItem::PresentationContextProposed( + PresentationContextProposed { + id: presentation_context_id, + abstract_syntax: abstract_syntax.context(MissingAbstractSyntaxSnafu)?, + transfer_syntaxes, + }, + )) + } + 0x21 => { + // Presentation Context Item Structure (result) + + let mut transfer_syntax: Option = None; + + // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers + // between 1 and 255, encoded as an unsigned binary number. For a complete description + // of the use of this field see Section 7.1.1.13. + let presentation_context_id = cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "Presentation-context-ID", + })?; + + // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded + // as an unsigned binary number. One of the following values shall be used: + // 0 - acceptance + // 1 - user-rejection + // 2 - no-reason (provider rejection) + // 3 - abstract-syntax-not-supported (provider rejection) + // 4 - transfer-syntaxes-not-supported (provider rejection) + let reason = PresentationContextResultReason::from(cursor.read_u8().await.context( + ReadPduFieldSnafu { + field: "Result/Reason", + }, + )?) + .context(InvalidPresentationContextResultReasonSnafu)?; + + // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer + // Syntax Sub-Item. When the Result/Reason field has a value other than acceptance (0), + // this field shall not be significant and its value shall not be tested when received. + // For a complete description of the use and encoding of this item see Section + // 9.3.3.2.1. + while cursor.position() < cursor.get_ref().len() as u64 { + // 1 - Item-type - XXH + let item_type = cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested + // to this value when received. + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 3-4 - Item-length + let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Item-length", + })?; + + match item_type { + 0x40 => { + // Transfer Syntax Sub-Item Structure + + // 5-xxx - Transfer-syntax-name(s) - This variable field shall contain the + // Transfer-syntax-name proposed for this presentation context. A valid + // Transfer-syntax-name shall be encoded as defined in Annex F. For a + // description of the use of this field see Section 7.1.1.13. + // Transfer-syntax-names are structured as UIDs as defined in PS3.5 (see + // Annex B for an overview of this concept). DICOM Transfer-syntax-names are + // registered in PS3.5. + match transfer_syntax { + Some(_) => { + // Multiple transfer syntax values cannot be proposed. + return MultipleTransferSyntaxesAcceptedSnafu.fail(); + } + None => { + transfer_syntax = Some( + codec + .decode( + &read_n(&mut cursor, item_length as usize) + .await + .context(ReadPduFieldSnafu { + field: "Transfer-syntax-name", + })?, + ) + .context(DecodeTextSnafu { + field: "Transfer-syntax-name", + })? + .trim() + .to_string(), + ); + } + } + } + _ => { + return InvalidTransferSyntaxSubItemSnafu.fail(); + } + } + } + + Ok(PduVariableItem::PresentationContextResult( + PresentationContextResult { + id: presentation_context_id, + reason, + transfer_syntax: transfer_syntax.context(MissingTransferSyntaxSnafu)?, + }, + )) + } + 0x50 => { + // User Information Item Structure + + let mut user_variables = vec![]; + + // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined + // by the DICOM Application Entity. The structure and content of these sub-items is + // defined in Annex D. + while cursor.position() < cursor.get_ref().len() as u64 { + // 1 - Item-type - XXH + let item_type = cursor + .read_u8() + .await + .context(ReadPduFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved + cursor + .read_u8() + .await + .context(ReadReservedSnafu { bytes: 1_u32 })?; + + // 3-4 - Item-length + let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Item-length", + })?; + + match item_type { + 0x51 => { + // Maximum Length Sub-Item Structure + + // 5-8 - Maximum-length-received - This parameter allows the + // association-requestor to restrict the maximum length of the variable + // field of the P-DATA-TF PDUs sent by the acceptor on the association once + // established. This length value is indicated as a number of bytes encoded + // as an unsigned binary number. The value of (0) indicates that no maximum + // length is specified. This maximum length value shall never be exceeded by + // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs + // received by the association-requestor. Otherwise, it shall be a protocol + // error. + user_variables.push(UserVariableItem::MaxLength( + cursor.read_u32().await.context(ReadPduFieldSnafu { + field: "Maximum-length-received", + })?, + )); + } + 0x52 => { + // Implementation Class UID Sub-Item Structure + + // 5 - xxx - Implementation-class-uid - This variable field shall contain + // the Implementation-class-uid of the Association-acceptor as defined in + // Section D.3.3.2. The Implementation-class-uid field is structured as a + // UID as defined in PS3.5. + let implementation_class_uid = codec + .decode(&read_n(&mut cursor, item_length as usize).await.context( + ReadPduFieldSnafu { + field: "Implementation-class-uid", + }, + )?) + .context(DecodeTextSnafu { + field: "Implementation-class-uid", + })? + .trim() + .to_string(); + user_variables.push(UserVariableItem::ImplementationClassUID( + implementation_class_uid, + )); + } + 0x55 => { + // Implementation Version Name Structure + + // 5 - xxx - Implementation-version-name - This variable field shall contain + // the Implementation-version-name of the Association-acceptor as defined in + // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990 + // (basic G0 set) characters. + let implementation_version_name = codec + .decode(&read_n(&mut cursor, item_length as usize).await.context( + ReadPduFieldSnafu { + field: "Implementation-version-name", + }, + )?) + .context(DecodeTextSnafu { + field: "Implementation-version-name", + })? + .trim() + .to_string(); + user_variables.push(UserVariableItem::ImplementationVersionName( + implementation_version_name, + )); + } + 0x56 => { + // SOP Class Extended Negotiation Sub-Item + + // 5-6 - SOP-class-uid-length - The SOP-class-uid-length shall be the number + // of bytes from the first byte of the following field to the last byte of the + // SOP-class-uid field. It shall be encoded as an unsigned binary number. + let sop_class_uid_length = + cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "SOP-class-uid-length", + })?; + + // 7 - xxx - SOP-class-uid - The SOP Class or Meta SOP Class identifier + // encoded as a UID as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5. + let sop_class_uid = codec + .decode( + &read_n(&mut cursor, sop_class_uid_length as usize) + .await + .context(ReadPduFieldSnafu { + field: "SOP-class-uid", + })?, + ) + .context(DecodeTextSnafu { + field: "SOP-class-uid", + })? + .trim() + .to_string(); + + let data_length = cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "Service-class-application-information-length", + })?; + + // xxx-xxx - Service-class-application-information -This field shall contain + // the application information specific to the Service Class specification + // identified by the SOP-class-uid. The semantics and value of this field + // is defined in the identified Service Class specification. + let data = read_n(&mut cursor, data_length as usize).await.context( + ReadPduFieldSnafu { + field: "Service-class-application-information", + }, + )?; + + user_variables.push(UserVariableItem::SopClassExtendedNegotiationSubItem( + sop_class_uid, + data, + )); + } + 0x58 => { + // User Identity Negotiation + + // 5 - User Identity Type + let user_identity_type = + cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "User-Identity-type", + })?; + + // 6 - Positive-response-requested + let positive_response_requested = + cursor.read_u8().await.context(ReadPduFieldSnafu { + field: "User-Identity-positive-response-requested", + })?; + + // 7-8 - Primary Field Length + let primary_field_length = + cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "User-Identity-primary-field-length", + })?; + + // 9-n - Primary Field + let primary_field = read_n(&mut cursor, primary_field_length as usize) + .await + .context(ReadPduFieldSnafu { + field: "User-Identity-primary-field", + })?; + + // n+1-n+2 - Secondary Field Length + // Only non-zero if user identity type is 2 (username and password) + let secondary_field_length = + cursor.read_u16().await.context(ReadPduFieldSnafu { + field: "User-Identity-secondary-field-length", + })?; + + // n+3-m - Secondary Field + let secondary_field = read_n(&mut cursor, secondary_field_length as usize) + .await + .context(ReadPduFieldSnafu { + field: "User-Identity-secondary-field", + })?; + + match UserIdentityType::from(user_identity_type) { + Some(user_identity_type) => { + user_variables.push(UserVariableItem::UserIdentityItem( + UserIdentity::new( + positive_response_requested == 1, + user_identity_type, + primary_field, + secondary_field, + ), + )); + } + None => { + warn!("Unknown User Identity Type code {}", user_identity_type); + } + } + } + _ => { + user_variables.push(UserVariableItem::Unknown( + item_type, + read_n(&mut cursor, item_length as usize) + .await + .context(ReadPduFieldSnafu { field: "Unknown" })?, + )); + } + } + } + + Ok(PduVariableItem::UserVariables(user_variables)) + } + _ => Ok(PduVariableItem::Unknown(item_type)), + } +} diff --git a/ul/src/pdu/writer.rs b/ul/src/pdu/writer.rs index 5d388609a..44be42720 100644 --- a/ul/src/pdu/writer.rs +++ b/ul/src/pdu/writer.rs @@ -5,46 +5,14 @@ use dicom_encoding::text::TextCodec; use snafu::{Backtrace, ResultExt, Snafu}; use std::io::Write; -#[derive(Debug, Snafu)] -#[non_exhaustive] -pub enum Error { - #[snafu(display("Could not write chunk of {} PDU structure", name))] - WriteChunk { - /// the name of the PDU structure - name: &'static str, - source: WriteChunkError, - }, - - #[snafu(display("Could not write field `{}`", field))] - WriteField { - field: &'static str, - backtrace: Backtrace, - source: std::io::Error, - }, - - #[snafu(display("Could not write {} reserved bytes", bytes))] - WriteReserved { - bytes: u32, - backtrace: Backtrace, - source: std::io::Error, - }, - - #[snafu(display("Could not write field `{}`", field))] - EncodeField { - field: &'static str, - #[snafu(backtrace)] - source: dicom_encoding::text::EncodeTextError, - }, -} - -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Snafu)] pub enum WriteChunkError { #[snafu(display("Failed to build chunk"))] BuildChunk { #[snafu(backtrace)] - source: Box, + source: Box, }, #[snafu(display("Failed to write chunk length"))] WriteLength { diff --git a/ul/src/pdu/writer_nonblocking.rs b/ul/src/pdu/writer_nonblocking.rs new file mode 100644 index 000000000..708fa4375 --- /dev/null +++ b/ul/src/pdu/writer_nonblocking.rs @@ -0,0 +1,1365 @@ +use std::future::Future; + +/// PDU writer module +use crate::pdu::*; +use dicom_encoding::text::TextCodec; +use snafu::{Backtrace, ResultExt, Snafu}; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +pub type Result = std::result::Result; + +#[derive(Debug, Snafu)] +pub enum WriteChunkError { + #[snafu(display("Failed to build chunk"))] + BuildChunk { + #[snafu(backtrace)] + source: Box, + }, + #[snafu(display("Failed to write chunk length"))] + WriteLength { + backtrace: Backtrace, + source: std::io::Error, + }, + #[snafu(display("Failed to write chunk data"))] + WriteData { + backtrace: Backtrace, + source: std::io::Error, + }, +} + +async fn write_chunk_u32( + writer: &mut W, + func: F, +) -> std::result::Result<(), WriteChunkError> +where + W: AsyncWrite + Unpin, + F: FnOnce() -> Fut, + Fut: Future>> + Send, +{ + let data = func().await.map_err(Box::from).context(BuildChunkSnafu)?; + + let length = data.len() as u32; + writer.write_u32(length).await.context(WriteLengthSnafu)?; + + writer.write_all(&data).await.context(WriteDataSnafu)?; + + Ok(()) +} + +async fn write_chunk_u16( + writer: &mut W, + func: F, +) -> std::result::Result<(), WriteChunkError> +where + W: AsyncWrite + Unpin, + F: FnOnce() -> Fut, + Fut: Future>> + Send, +{ + // NOTE: If I kept the original design, i,e F: FnOnce(&mut Vec) -> Box> + Send + Unpin>, + // I ended up with lifetime issues, but I don't with this. Not sure how to fix those lifetime issues so I just went with this for now + let data = func().await.map_err(Box::from).context(BuildChunkSnafu)?; + + let length = data.len() as u16; + writer.write_u16(length).await.context(WriteLengthSnafu)?; + + writer.write_all(&data).await.context(WriteDataSnafu)?; + + Ok(()) +} + +pub async fn write_pdu(writer: &mut W, pdu: &Pdu) -> Result<()> +where + W: AsyncWrite + Unpin, +{ + let codec = dicom_encoding::text::DefaultCharacterSetCodec; + match pdu { + Pdu::AssociationRQ(AssociationRQ { + protocol_version, + calling_ae_title, + called_ae_title, + application_context_name, + presentation_contexts, + user_variables, + }) => { + // A-ASSOCIATE-RQ PDU Structure + + // 1 - PDU-type - 01H + writer + .write_u8(0x01) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + // 7-8 Protocol-version - This two byte field shall use one bit to identify + // each version of the DICOM UL protocol supported by the calling end-system. + // This is Version 1 and shall be identified with bit 0 set. A receiver of this + // PDU implementing only this version of the DICOM UL protocol shall only test + // that bit 0 is set. + let mut writer = vec![]; + writer + .write_u16(*protocol_version) + .await + .context(WriteFieldSnafu { + field: "Protocol-version", + })?; + + // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but + // not tested to this value when received. + writer + .write_u16(0x00) + .await + .context(WriteReservedSnafu { bytes: 2_u32 })?; + + // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be + // encoded as 16 characters as defined by the ISO 646:1990-Basic G0 Set with + // leading and trailing spaces (20H) being non-significant. The value made of 16 + // spaces (20H) meaning "no Application Name specified" shall not be used. For a + // complete description of the use of this field, see Section 7.1.1.4. + let mut ae_title_bytes = + codec.encode(called_ae_title).context(EncodeFieldSnafu { + field: "Called-AE-title", + })?; + ae_title_bytes.resize(16, b' '); + writer + .write_all(&ae_title_bytes) + .await + .context(WriteFieldSnafu { + field: "Called-AE-title", + })?; + + // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded + // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and + // trailing spaces (20H) being non-significant. The value made of 16 spaces + // (20H) meaning "no Application Name specified" shall not be used. For a + // complete description of the use of this field, see Section 7.1.1.3. + let mut ae_title_bytes = + codec.encode(calling_ae_title).context(EncodeFieldSnafu { + field: "Calling-AE-title", + })?; + ae_title_bytes.resize(16, b' '); + writer + .write_all(&ae_title_bytes) + .await + .context(WriteFieldSnafu { + field: "Called-AE-title", + })?; + + // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all + // bytes but not tested to this value when received + writer + .write_all(&[0; 32]) + .await + .context(WriteReservedSnafu { bytes: 32_u32 })?; + + write_pdu_variable_application_context_name( + &mut writer, + application_context_name, + &codec, + ) + .await?; + + for presentation_context in presentation_contexts { + write_pdu_variable_presentation_context_proposed( + &mut writer, + presentation_context, + &codec, + ) + .await?; + } + + write_pdu_variable_user_variables(&mut writer, user_variables, &codec).await?; + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "A-ASSOCIATE-RQ", + })?; + + Ok(()) + } + Pdu::AssociationAC(AssociationAC { + protocol_version, + application_context_name, + called_ae_title, + calling_ae_title, + presentation_contexts, + user_variables, + }) => { + // A-ASSOCIATE-AC PDU Structure + + // 1 - PDU-type - 02H + writer + .write_u8(0x02) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + // 7-8 - Protocol-version - This two byte field shall use one bit to identify each + // version of the DICOM UL protocol supported by the calling end-system. This is + // Version 1 and shall be identified with bit 0 set. A receiver of this PDU + // implementing only this version of the DICOM UL protocol shall only test that bit + // 0 is set. + let mut writer = vec![]; + writer + .write_u16(*protocol_version) + .await + .context(WriteFieldSnafu { + field: "Protocol-version", + })?; + + // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not + // tested to this value when received. + writer + .write_u16(0x00) + .await + .context(WriteReservedSnafu { bytes: 2_u32 })?; + + // 11-26 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + let mut ae_title_bytes = + codec.encode(called_ae_title).context(EncodeFieldSnafu { + field: "Called-AE-title", + })?; + ae_title_bytes.resize(16, b' '); + writer + .write_all(&ae_title_bytes) + .await + .context(WriteFieldSnafu { + field: "Called-AE-title", + })?; + // 27-42 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + let mut ae_title_bytes = + codec.encode(calling_ae_title).context(EncodeFieldSnafu { + field: "Calling-AE-title", + })?; + ae_title_bytes.resize(16, b' '); + writer + .write_all(&ae_title_bytes) + .await + .context(WriteFieldSnafu { + field: "Calling-AE-title", + })?; + + // 43-74 - Reserved - This reserved field shall be sent with a value identical to + // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value + // shall not be tested when received. + writer + .write_all(&[0; 32]) + .await + .context(WriteReservedSnafu { bytes: 32_u32 })?; + + // 75-xxx - Variable items - This variable field shall contain the following items: + // one Application Context Item, one or more Presentation Context Item(s) and one + // User Information Item. For a complete description of these items see Section + // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. + write_pdu_variable_application_context_name( + &mut writer, + application_context_name, + &codec, + ) + .await?; + + for presentation_context in presentation_contexts { + write_pdu_variable_presentation_context_result( + &mut writer, + presentation_context, + &codec, + ) + .await?; + } + + write_pdu_variable_user_variables(&mut writer, user_variables, &codec).await?; + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "A-ASSOCIATE-AC", + }) + } + Pdu::AssociationRJ(AssociationRJ { result, source }) => { + // 1 - PDU-type - 03H + writer + .write_u8(0x03) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 8 - Result - This Result field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used: + // - 1 - rejected-permanent + // - 2 - rejected-transient + writer.write_u8(match result { + AssociationRJResult::Permanent => { + 0x01 + } + AssociationRJResult::Transient => { + 0x02 + } + }) + .await + .context(WriteFieldSnafu { field: "AssociationRJResult" })?; + + // 9 - Source - This Source field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used: + // - 1 - DICOM UL service-user + // - 2 - DICOM UL service-provider (ACSE related function) + // - 3 - DICOM UL service-provider (Presentation related function) + // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned binary number. + // If the Source field has the value (1) "DICOM UL service-user", it shall take one of the following: + // - 1 - no-reason-given + // - 2 - application-context-name-not-supported + // - 3 - calling-AE-title-not-recognized + // - 4-6 - reserved + // - 7 - called-AE-title-not-recognized + // - 8-10 - reserved + // If the Source field has the value (2) "DICOM UL service provided (ACSE related function)", it shall take one of the following: + // - 1 - no-reason-given + // - 2 - protocol-version-not-supported + // If the Source field has the value (3) "DICOM UL service provided (Presentation related function)", it shall take one of the following: + // 0 - reserved + // 1 - temporary-congestion + // 2 - local-limit-exceeded + // 3-7 - reserved + match source { + AssociationRJSource::ServiceUser(reason) => { + writer.write_u8(0x01).await.context(WriteFieldSnafu { field: "AssociationRJServiceUserReason" })?; + writer.write_u8(match reason { + AssociationRJServiceUserReason::NoReasonGiven => { + 0x01 + } + AssociationRJServiceUserReason::ApplicationContextNameNotSupported => { + 0x02 + } + AssociationRJServiceUserReason::CallingAETitleNotRecognized => { + 0x03 + } + AssociationRJServiceUserReason::CalledAETitleNotRecognized => { + 0x07 + } + AssociationRJServiceUserReason::Reserved(data) => { + *data + } + }).await.context(WriteFieldSnafu { field: "AssociationRJServiceUserReason (2)" })?; + } + AssociationRJSource::ServiceProviderASCE(reason) => { + writer.write_u8(0x02).await.context(WriteFieldSnafu { field: "AssociationRJServiceProvider" })?; + writer.write_u8(match reason { + AssociationRJServiceProviderASCEReason::NoReasonGiven => { + 0x01 + } + AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => { + 0x02 + } + }).await.context(WriteFieldSnafu { field: "AssociationRJServiceProvider (2)" })?; + } + AssociationRJSource::ServiceProviderPresentation(reason) => { + writer.write_u8(0x03).await.context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason" })?; + writer.write_u8(match reason { + AssociationRJServiceProviderPresentationReason::TemporaryCongestion => { + 0x01 + } + AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => { + 0x02 + } + AssociationRJServiceProviderPresentationReason::Reserved(data) => { + *data + } + }).await.context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason (2)" })?; + } + } + + Ok(writer) + }).await.context(WriteChunkSnafu { name: "AssociationRJ" })?; + + Ok(()) + } + Pdu::PData { data } => { + // 1 - PDU-type - 04H + writer + .write_u8(0x04) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain + // one or more Presentation-data-value Items(s). For a complete description of the + // use of this field see Section 9.3.5.1 + + for presentation_data_value in data { + write_chunk_u32(&mut writer, || async { + let mut writer = vec![]; + // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd + // integers between 1 and 255, encoded as an unsigned binary number. For a + // complete description of the use of this field see Section 7.1.1.13. + writer.push(presentation_data_value.presentation_context_id); + + // 6-xxx - Presentation-data-value - This Presentation-data-value field + // shall contain DICOM message information (command and/or data set) with a + // message control header. For a complete description of the use of this + // field see Annex E. + + // The Message Control Header shall be made of one byte with the least + // significant bit (bit 0) taking one of the following values: + // - If bit 0 is set to 1, the following fragment shall contain Message + // Command information. + // - If bit 0 is set to 0, the following fragment shall contain Message Data + // Set information. + // The next least significant bit (bit 1) shall be defined by the following + // rules: If bit 1 is set to 1, the following fragment shall contain the + // last fragment of a Message Data Set or of a Message Command. + // - If bit 1 is set to 0, the following fragment does not contain the last + // fragment of a Message Data Set or of a Message Command. + let mut message_header = 0x00; + if let PDataValueType::Command = presentation_data_value.value_type { + message_header |= 0x01; + } + if presentation_data_value.is_last { + message_header |= 0x02; + } + writer.push(message_header); + + // Message fragment + writer.extend(&presentation_data_value.data); + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Presentation-data-value item", + })?; + } + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "PData" }) + } + Pdu::ReleaseRQ => { + // 1 - PDU-type - 05H + writer + .write_u8(0x05) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + writer.extend([0u8; 4]); + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "ReleaseRQ" })?; + + Ok(()) + } + Pdu::ReleaseRP => { + // 1 - PDU-type - 06H + writer + .write_u8(0x06) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + writer.extend([0u8; 4]); + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "ReleaseRP" })?; + + Ok(()) + } + Pdu::AbortRQ { source } => { + // 1 - PDU-type - 07H + writer + .write_u8(0x07) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested + // to this value when received. + writer.push(0); + // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested + // to this value when received. + writer.push(0); + + // 9 - Source - This Source field shall contain an integer value encoded as an + // unsigned binary number. One of the following values shall be used: + // - 0 - DICOM UL service-user (initiated abort) + // - 1 - reserved + // - 2 - DICOM UL service-provider (initiated abort) + // 10 - Reason/Diag - This field shall contain an integer value encoded as an + // unsigned binary number. If the Source field has the value (2) "DICOM UL + // service-provider", it shall take one of the following: + // - 0 - reason-not-specified1 - unrecognized-PDU + // - 2 - unexpected-PDU + // - 3 - reserved + // - 4 - unrecognized-PDU parameter + // - 5 - unexpected-PDU parameter + // - 6 - invalid-PDU-parameter value + // If the Source field has the value (0) "DICOM UL service-user", this reason field + // shall not be significant. It shall be sent with a value 00H but not tested to + // this value when received. + let source_word = match source { + AbortRQSource::ServiceUser => [0x00; 2], + AbortRQSource::Reserved => [0x01, 0x00], + AbortRQSource::ServiceProvider(reason) => match reason { + AbortRQServiceProviderReason::ReasonNotSpecified => [0x02, 0x00], + AbortRQServiceProviderReason::UnrecognizedPdu => [0x02, 0x01], + AbortRQServiceProviderReason::UnexpectedPdu => [0x02, 0x02], + AbortRQServiceProviderReason::Reserved => [0x02, 0x03], + AbortRQServiceProviderReason::UnrecognizedPduParameter => [0x02, 0x04], + AbortRQServiceProviderReason::UnexpectedPduParameter => [0x02, 0x05], + AbortRQServiceProviderReason::InvalidPduParameter => [0x02, 0x06], + }, + }; + writer.extend(source_word); + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "AbortRQ" })?; + + Ok(()) + } + Pdu::Unknown { pdu_type, data } => { + // 1 - PDU-type - XXH + writer + .write_u8(*pdu_type) + .await + .context(WriteFieldSnafu { field: "PDU-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to + // this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u32(writer, || async { + let mut writer = vec![]; + writer.extend(data); + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "Unknown" })?; + + Ok(()) + } + } +} + +async fn write_pdu_variable_application_context_name( + writer: &mut W, + application_context_name: &str, + codec: &C, +) -> Result<()> +where + W: AsyncWrite + Unpin, + C: TextCodec + Send + Sync, +{ + // Application Context Item Structure + // 1 - Item-type - 10H + writer + .write_u8(0x10) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(writer, || async move { + // 5-xxx - Application-context-name -A valid Application-context-name shall + // be encoded as defined in Annex F. For a description of the use of this + // field see Section 7.1.1.2. Application-context-names are structured as + // UIDs as defined in PS3.5 (see Annex A for an overview of this concept). + // DICOM Application-context-names are registered in PS3.7. + let mut w = vec![]; + w.write_all( + &codec + .encode(application_context_name) + .context(EncodeFieldSnafu { + field: "Application-context-name", + })?, + ) + .await + .context(WriteFieldSnafu { + field: "Application-context-name", + }); + Ok(w) + }) + .await + .context(WriteChunkSnafu { + name: "Application Context Item", + })?; + + Ok(()) +} + +async fn write_pdu_variable_presentation_context_proposed( + writer: &mut W, + presentation_context: &PresentationContextProposed, + codec: &TC, +) -> Result<()> +where + W: AsyncWrite + Unpin, + TC: TextCodec + Send + Sync, +{ + // Presentation Context Item Structure + // 1 - tem-type - 20H + writer + .write_u8(0x20) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(writer, || async { + // 5 - Presentation-context-ID - Presentation-context-ID values shall be + // odd integers between 1 and 255, encoded as an unsigned binary number. + // For a complete description of the use of this field see Section + // 7.1.1.13. + let mut writer = vec![]; + writer + .write_u8(presentation_context.id) + .await + .context(WriteFieldSnafu { + field: "Presentation-context-ID", + })?; + + // 6 - Reserved - This reserved field shall be sent with a value 00H but + // not tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 7 - Reserved - This reserved field shall be sent with a value 00H but + // not tested to this value when received + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 8 - Reserved - This reserved field shall be sent with a value 00H but + // not tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field + // shall contain the following sub-items: one Abstract Syntax and one or + // more Transfer Syntax(es). For a complete description of the use and + // encoding of these sub-items see Section 9.3.2.2.1 and Section + // 9.3.2.2.2. + + // Abstract Syntax Sub-Item Structure + // 1 - Item-type 30H + writer + .write_u8(0x30) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H + // but not tested to this value when + // received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5-xxx - Abstract-syntax-name - This variable field shall + // contain + // the Abstract-syntax-name related to the proposed presentation + // context. A valid Abstract-syntax-name shall be encoded as + // defined in Annex F. For a + // description of the use of this field see + // Section 7.1.1.13. Abstract-syntax-names are structured as + // UIDs as defined in PS3.5 + // (see Annex B for an overview of this concept). + // DICOM Abstract-syntax-names are registered in PS3.4. + writer + .write_all( + &codec + .encode(&presentation_context.abstract_syntax) + .context(EncodeFieldSnafu { + field: "Abstract-syntax-name", + })?, + ) + .await + .context(WriteFieldSnafu { + field: "Abstract-syntax-name", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Abstract Syntax Item", + })?; + + for transfer_syntax in &presentation_context.transfer_syntaxes { + // Transfer Syntax Sub-Item Structure + // 1 - Item-type - 40H + writer.write_u8(0x40).await.context(WriteFieldSnafu { + field: "Presentation-context Item-type", + })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H + // but not tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5-xxx - Transfer-syntax-name(s) - This variable field shall + // contain the Transfer-syntax-name proposed for this + // presentation context. A valid Transfer-syntax-name shall be + // encoded as defined in Annex F. For a description of the use + // of this field see Section 7.1.1.13. Transfer-syntax-names are + // structured as UIDs as defined in PS3.5 (see Annex B for an + // overview of this concept). DICOM Transfer-syntax-names are + // registered in PS3.5. + writer + .write_all(&codec.encode(transfer_syntax).context(EncodeFieldSnafu { + field: "Transfer-syntax-name", + })?) + .await + .context(WriteFieldSnafu { + field: "Transfer-syntax-name", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Transfer Syntax Sub-Item", + })?; + } + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Presentation Context Item", + })?; + + Ok(()) +} + +async fn write_pdu_variable_presentation_context_result( + writer: &mut W, + presentation_context: &PresentationContextResult, + codec: &TC, +) -> Result<()> +where + W: AsyncWrite + Unpin, + TC: TextCodec + Send + Sync, +{ + // 1 - Item-type - 21H + writer + .write_u8(0x21) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this + // value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(writer, || async { + let mut writer = vec![]; + // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers + // between 1 and 255, encoded as an unsigned binary number. For a complete description of + // the use of this field see Section 7.1.1.13. + writer + .write_u8(presentation_context.id) + .await + .context(WriteFieldSnafu { + field: "Presentation-context-ID", + })?; + + // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to this + // value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded as an + // unsigned binary number. One of the following values shall be used: + // 0 - acceptance + // 1 - user-rejection + // 2 - no-reason (provider rejection) + // 3 - abstract-syntax-not-supported (provider rejection) + // 4 - transfer-syntaxes-not-supported (provider rejection) + writer + .write_u8(match &presentation_context.reason { + PresentationContextResultReason::Acceptance => 0, + PresentationContextResultReason::UserRejection => 1, + PresentationContextResultReason::NoReason => 2, + PresentationContextResultReason::AbstractSyntaxNotSupported => 3, + PresentationContextResultReason::TransferSyntaxesNotSupported => 4, + }) + .await + .context(WriteFieldSnafu { + field: "Presentation Context Result/Reason", + })?; + + // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to this + // value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer Syntax + // Sub-Item. When the Result/Reason field has a value other than acceptance (0), this field + // shall not be significant and its value shall not be tested when received. For a complete + // description of the use and encoding of this item see Section 9.3.3.2.1. + + // 1 - Item-type - 40H + writer + .write_u8(0x40) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this + // value when received. + writer + .write_u8(0x40) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5-xxx - Transfer-syntax-name - This variable field shall contain the + // Transfer-syntax-name proposed for this presentation context. A valid + // Transfer-syntax-name shall be encoded as defined in Annex F. For a description of the + // use of this field see Section 7.1.1.14. Transfer-syntax-names are structured as UIDs + // as defined in PS3.5 (see Annex B for an overview of this concept). DICOM + // Transfer-syntax-names are registered in PS3.5. + writer + .write_all( + &codec + .encode(&presentation_context.transfer_syntax) + .context(EncodeFieldSnafu { + field: "Transfer-syntax-name", + })?, + ) + .await + .context(WriteFieldSnafu { + field: "Transfer-syntax-name", + })?; + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Transfer Syntax sub-item", + })?; + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Presentation-context", + }) +} + +async fn write_pdu_variable_user_variables( + writer: &mut W, + user_variables: &[UserVariableItem], + codec: &TC, +) -> Result<()> +where + W: AsyncWrite + Unpin, + TC: TextCodec + Send + Sync, +{ + if user_variables.is_empty() { + return Ok(()); + } + + // 1 - Item-type - 50H + writer + .write_u8(0x50) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this + // value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(writer, || async { + let mut writer = vec![]; + // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined by + // the DICOM Application Entity. The structure and content of these sub-items is defined in + // Annex D. + for user_variable in user_variables { + match user_variable { + UserVariableItem::MaxLength(max_length) => { + // 1 - Item-type - 51H + writer + .write_u8(0x51) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5-8 - Maximum-length-received - This parameter allows the + // association-requestor to restrict the maximum length of the variable + // field of the P-DATA-TF PDUs sent by the acceptor on the association once + // established. This length value is indicated as a number of bytes encoded + // as an unsigned binary number. The value of (0) indicates that no maximum + // length is specified. This maximum length value shall never be exceeded by + // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs + // received by the association-requestor. Otherwise, it shall be a protocol + // error. + writer + .write_u32(*max_length) + .await + .context(WriteFieldSnafu { + field: "Maximum-length-received", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Maximum-length-received", + })?; + } + UserVariableItem::ImplementationVersionName(implementation_version_name) => { + // 1 - Item-type - 55H + writer + .write_u8(0x55) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5 - xxx - Implementation-version-name - This variable field shall contain + // the Implementation-version-name of the Association-acceptor as defined in + // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990 + // (basic G0 set) characters. + writer + .write_all(&codec.encode(implementation_version_name).context( + EncodeFieldSnafu { + field: "Implementation-version-name", + }, + )?) + .await + .context(WriteFieldSnafu { + field: "Implementation-version-name", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Implementation-version-name", + })?; + } + UserVariableItem::ImplementationClassUID(implementation_class_uid) => { + // 1 - Item-type - 52H + writer + .write_u8(0x52) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + //5 - xxx - Implementation-class-uid - This variable field shall contain + // the Implementation-class-uid of the Association-acceptor as defined in + // Section D.3.3.2. The Implementation-class-uid field is structured as a + // UID as defined in PS3.5. + writer + .write_all(&codec.encode(implementation_class_uid).context( + EncodeFieldSnafu { + field: "Implementation-class-uid", + }, + )?) + .await + .context(WriteFieldSnafu { + field: "Implementation-class-uid", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Implementation-class-uid", + })?; + } + UserVariableItem::SopClassExtendedNegotiationSubItem(sop_class_uid, data) => { + // 1 - Item-type - 56H + writer + .write_u8(0x56) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 7-xxx - The SOP Class or Meta SOP Class identifier encoded as a UID + // as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5. + writer + .write_all(&codec.encode(sop_class_uid).context( + EncodeFieldSnafu { + field: "SOP-class-uid", + }, + )?) + .await + .context(WriteFieldSnafu { + field: "SOP-class-uid", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "SOP-class-uid", + })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // xxx-xxx Service-class-application-information - This field shall contain + // the application information specific to the Service Class specification + // identified by the SOP-class-uid. The semantics and value of this field is + // defined in the identified Service Class specification. + writer.write_all(data).await.context(WriteFieldSnafu { + field: "Service-class-application-information", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Service-class-application-information", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "Sub-item" })?; + } + UserVariableItem::UserIdentityItem(user_identity) => { + // 1 - Item-type - 58H + writer + .write_u8(0x58) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + // 2 - Reserved - This reserved field shall be sent with a value 00H but not + // tested to this value when received. + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + // 3-4 - Item-length + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 5 - User-Identity-Type + writer + .write_u8(user_identity.identity_type().to_u8()) + .await + .context(WriteFieldSnafu { + field: "User-Identity-Type", + })?; + + // 6 - Positive-response-requested + let positive_response_requested_out: u8 = + if user_identity.positive_response_requested() { + 1 + } else { + 0 + }; + writer + .write_u8(positive_response_requested_out) + .await + .context(WriteFieldSnafu { + field: "Positive-response-requested", + })?; + + // 7-8 - Primary-field-length + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // 9-n - Primary-field + writer + .write_all(user_identity.primary_field().as_slice()) + .await + .context(WriteFieldSnafu { + field: "Primary-field", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Primary-field", + })?; + + // n+1-n+2 - Secondary-field-length + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + // n+3-m - Secondary-field + writer + .write_all(user_identity.secondary_field().as_slice()) + .await + .context(WriteFieldSnafu { + field: "Secondary-field", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Secondary-field", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { + name: "Item-length", + })?; + } + UserVariableItem::Unknown(item_type, data) => { + writer + .write_u8(*item_type) + .await + .context(WriteFieldSnafu { field: "Item-type" })?; + + writer + .write_u8(0x00) + .await + .context(WriteReservedSnafu { bytes: 1_u32 })?; + + write_chunk_u16(&mut writer, || async { + let mut writer = vec![]; + writer.write_all(data).await.context(WriteFieldSnafu { + field: "Unknown Data", + })?; + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "Unknown" })?; + } + } + } + + Ok(writer) + }) + .await + .context(WriteChunkSnafu { name: "User-data" }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_write_chunks_with_preceding_u32_length() -> Result<()> { + let mut bytes = vec![0u8; 0]; + write_chunk_u32(&mut bytes, |writer| { + writer + .write_u8(0x02) + .context(WriteFieldSnafu { field: "Field1" })?; + write_chunk_u32(writer, |writer| { + writer + .write_u8(0x03) + .context(WriteFieldSnafu { field: "Field2" })?; + Ok(()) + }) + .context(WriteChunkSnafu { name: "Chunk2" }) + }) + .context(WriteChunkSnafu { name: "Chunk1" })?; + + assert_eq!(bytes.len(), 10); + assert_eq!(bytes, &[0, 0, 0, 6, 2, 0, 0, 0, 1, 3]); + + Ok(()) + } + + #[test] + fn can_write_chunks_with_preceding_u16_length() -> Result<()> { + let mut bytes = vec![0u8; 0]; + write_chunk_u16(&mut bytes, |writer| { + writer + .write_u8(0x02) + .context(WriteFieldSnafu { field: "Field1" })?; + write_chunk_u16(writer, |writer| { + writer + .write_u8(0x03) + .context(WriteFieldSnafu { field: "Field2" })?; + Ok(()) + }) + .context(WriteChunkSnafu { name: "Chunk2" }) + }) + .context(WriteChunkSnafu { name: "Chunk1" })?; + + assert_eq!(bytes.len(), 6); + assert_eq!(bytes, &[0, 4, 2, 0, 1, 3]); + + Ok(()) + } + + #[test] + fn write_abort_rq() { + let mut out = vec![]; + + // abort by request of SCU + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }; + write_pdu(&mut out, &pdu).unwrap(); + assert_eq!( + &out, + &[ + // code 7 + reserved byte + 0x07, 0x00, // + // PDU length: 4 bytes + 0x00, 0x00, 0x00, 0x04, // + // reserved 2 bytes + source: service user (0) + reason (0) + 0x00, 0x00, 0x00, 0x00, + ] + ); + out.clear(); + + // Reserved + let pdu = Pdu::AbortRQ { + source: AbortRQSource::Reserved, + }; + write_pdu(&mut out, &pdu).unwrap(); + assert_eq!( + &out, + &[ + // code 7 + reserved byte + 0x07, 0x00, // + // PDU length: 4 bytes + 0x00, 0x00, 0x00, 0x04, // + // reserved 2 bytes + source: reserved (1) + reason (0) + 0x00, 0x00, 0x01, 0x00, + ] + ); + out.clear(); + + // abort by request of SCP + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceProvider( + AbortRQServiceProviderReason::InvalidPduParameter, + ), + }; + write_pdu(&mut out, &pdu).unwrap(); + assert_eq!( + &out, + &[ + // code 7 + reserved byte + 0x07, 0x00, // + // PDU length: 4 bytes + 0x00, 0x00, 0x00, 0x04, // + // reserved 2 bytes + 0x00, 0x00, // + // source: service provider (2), invalid parameter value (6) + 0x02, 0x06, + ] + ); + } +} From 31408225ec129d0f7ef4bee49afe10a7714110ee Mon Sep 17 00:00:00 2001 From: Nathan Richman Date: Thu, 25 Jul 2024 12:49:02 -0500 Subject: [PATCH 02/28] MAIN: Modify read_pdu to take in Buf trait. Change is working with async storescp, still need to try how it would work with the sync version --- ul/Cargo.toml | 1 + ul/src/association/client.rs | 63 +- ul/src/association/server.rs | 84 +- ul/src/pdu/mod.rs | 14 +- ul/src/pdu/reader.rs | 480 ++++------- ul/src/pdu/reader_nonblocking.rs | 959 --------------------- ul/src/pdu/writer_nonblocking.rs | 1365 ------------------------------ 7 files changed, 291 insertions(+), 2675 deletions(-) delete mode 100644 ul/src/pdu/reader_nonblocking.rs delete mode 100644 ul/src/pdu/writer_nonblocking.rs diff --git a/ul/Cargo.toml b/ul/Cargo.toml index 24da2cc9c..b6cf54e19 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -12,6 +12,7 @@ readme = "README.md" [dependencies] byteordered = "0.6" +bytes = "1.6.1" dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.0", default-features = false } snafu = "0.8" diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 78ad01172..49f98b978 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -10,6 +10,7 @@ use std::{ io::Write, net::{TcpStream, ToSocketAddrs}, }; +use bytes::BytesMut; #[cfg(feature = "tokio")] use tokio::{ io::{AsyncRead, AsyncWriteExt}, @@ -124,6 +125,10 @@ pub enum Error { #[snafu(backtrace)] source: crate::pdu::ReadError, }, + #[snafu(display("Other error: {}", msg))] + Other{ + msg: String + } } pub type Result = std::result::Result; @@ -776,14 +781,21 @@ impl<'a> ClientAssociationOptions<'a> { // send request write_pdu(&mut buffer, &msg) - .await .context(SendRequestSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; buffer.clear(); // receive response - let msg = read_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict) - .await - .context(ReceiveResponseSnafu)?; + use tokio::io::AsyncReadExt; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + + let msg = loop { + if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { + break pdu + } + if 0 == socket.read_buf(&mut read_buffer).await.unwrap() { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + }; match msg { Pdu::AssociationAC(AssociationAC { @@ -841,6 +853,7 @@ impl<'a> ClientAssociationOptions<'a> { buffer, strict, timeout, + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), }) } Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), @@ -961,6 +974,8 @@ pub struct ClientAssociation { strict: bool, /// Send/Receive operation timeout timeout: Option, + /// Buffer to assemble PDU before parsing + read_buffer: BytesMut } impl ClientAssociation { @@ -1003,7 +1018,7 @@ impl ClientAssociation { /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); - write_pdu(&mut self.buffer, msg).await.context(SendSnafu)?; + write_pdu(&mut self.buffer, msg).context(SendSnafu)?; if self.buffer.len() > self.acceptor_max_pdu_length as usize { return SendTooLongPduSnafu { length: self.buffer.len(), @@ -1024,9 +1039,28 @@ impl ClientAssociation { #[cfg(feature = "tokio")] /// Read a PDU message from the other intervenient. pub async fn receive(&mut self) -> Result { - read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict) - .await - .context(ReceiveSnafu) + use std::io::Cursor; + + use bytes::Buf; + use tokio::io::AsyncReadExt; + + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu) + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self.socket.read_buf(&mut self.read_buffer).await.unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + } } #[cfg(not(feature = "tokio"))] @@ -1140,10 +1174,17 @@ impl ClientAssociation { async fn release_impl(&mut self) -> Result<()> { let pdu = Pdu::ReleaseRQ; self.send(&pdu).await?; - let pdu = read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict) - .await - .context(ReceiveSnafu)?; + use tokio::io::AsyncReadExt; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + let pdu = loop { + if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { + break pdu + } + if 0 == self.socket.read_buf(&mut read_buffer).await.unwrap() { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + }; match pdu { Pdu::ReleaseRP => {} pdu @ Pdu::AbortRQ { .. } diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 3b9aa8933..5242d6248 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -91,6 +91,9 @@ pub enum Error { ))] #[non_exhaustive] SendTooLongPdu { length: usize, backtrace: Backtrace }, + Other{ + msg: String + } } pub type Result = std::result::Result; @@ -539,10 +542,21 @@ where ); let max_pdu_length = self.max_pdu_length; + use bytes::BytesMut; + use tokio::io::AsyncReadExt; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + + let pdu = loop { + match read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => break pdu, + None => {} + } + if 0 == socket.read_buf(&mut read_buffer).await.unwrap() { + println!("Here {}", read_buffer.len()); + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + }; - let pdu = read_pdu(&mut socket, max_pdu_length, self.strict) - .await - .context(ReceiveRequestSnafu)?; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); match pdu { Pdu::AssociationRQ(AssociationRQ { @@ -563,7 +577,6 @@ where ), }), ) - .await .context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; return RejectedSnafu.fail(); @@ -579,13 +592,12 @@ where ), }), ) - .await .context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; return RejectedSnafu.fail(); } - self.ae_access_control + match self.ae_access_control .check_access( &self.ae_title, &calling_ae_title, @@ -598,10 +610,9 @@ where } _ => None, }), - ) - .map(Ok) - .unwrap_or_else(|reason| { - async { + ){ + Ok(()) => {}, + Err(reason) => { write_pdu( &mut buffer, &Pdu::AssociationRJ(AssociationRJ { @@ -609,13 +620,12 @@ where source: AssociationRJSource::ServiceUser(reason), }), ) - .await .context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; - return Err::<(), Error>(RejectedSnafu.build()); - }; - Ok(()) - })?; + return Err(RejectedSnafu.build()); + + } + } // fetch requested maximum PDU length let requestor_max_pdu_length = user_variables @@ -685,7 +695,6 @@ where ], }), ) - .await .context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; @@ -697,11 +706,11 @@ where client_ae_title: calling_ae_title, buffer, strict: self.strict, + read_buffer: bytes::BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize) }) } Pdu::ReleaseRQ => { write_pdu(&mut buffer, &Pdu::ReleaseRP) - .await .context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; AbortedSnafu.fail() @@ -769,6 +778,8 @@ pub struct ServerAssociation { buffer: Vec, /// whether to receive PDUs in strict mode strict: bool, + /// Read buffer from the socket + read_buffer: bytes::BytesMut } impl ServerAssociation { @@ -800,7 +811,7 @@ impl ServerAssociation { /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); - write_pdu(&mut self.buffer, msg).await.context(SendSnafu)?; + write_pdu(&mut self.buffer, msg).context(SendSnafu)?; if self.buffer.len() > self.requestor_max_pdu_length as usize { return SendTooLongPduSnafu { length: self.buffer.len(), @@ -813,18 +824,41 @@ impl ServerAssociation { .context(WireSendSnafu) } - #[cfg(not(feature = "tokio"))] /// Read a PDU message from the other intervenient. + #[cfg(not(feature = "tokio"))] pub fn receive(&mut self) -> Result { - read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict).context(ReceiveSnafu) + match read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict).context(ReceiveSnafu){ + Ok(Some(pdu)) => Ok(pdu), + Ok(None) => self.receive(), + Err(e) => Err(e) + } } #[cfg(feature = "tokio")] /// Read a PDU message from the other intervenient. - pub async fn receive(&mut self) -> Result { - read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict) - .await - .context(ReceiveSnafu) + pub async fn receive_async(&mut self) -> Result { + use std::io::Cursor; + + use bytes::Buf; + use tokio::io::AsyncReadExt; + + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu) + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self.socket.read_buf(&mut self.read_buffer).await.unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + } } /// Send a provider initiated abort message @@ -853,7 +887,7 @@ impl ServerAssociation { ), }; let out = self.send(&pdu).await; - let _ = self.socket.shutdown().await; + let _ = self.socket.pubshutdown().await; out } diff --git a/ul/src/pdu/mod.rs b/ul/src/pdu/mod.rs index e2e342e38..37a96473a 100644 --- a/ul/src/pdu/mod.rs +++ b/ul/src/pdu/mod.rs @@ -4,26 +4,14 @@ //! protocol data units (PDUs) according to //! the standard message exchange mechanisms, //! as well as readers and writers of PDUs from arbitrary data sources. -#[cfg(not(feature = "tokio"))] pub mod reader; -#[cfg(feature = "tokio")] -pub mod reader_nonblocking; -#[cfg(not(feature = "tokio"))] pub mod writer; -#[cfg(feature = "tokio")] -pub mod writer_nonblocking; use std::fmt::Display; -#[cfg(not(feature = "tokio"))] pub use reader::read_pdu; -#[cfg(feature = "tokio")] -pub use reader_nonblocking::read_pdu; +pub use writer::{write_pdu, WriteChunkError}; use snafu::{Backtrace, Snafu}; -#[cfg(not(feature = "tokio"))] -pub use writer::write_pdu; -#[cfg(feature = "tokio")] -pub use writer_nonblocking::{write_pdu, WriteChunkError}; /// The default maximum PDU size pub const DEFAULT_MAX_PDU: u32 = 16_384; diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index 98c8a987a..dad8fa769 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -1,16 +1,13 @@ /// PDU reader module use crate::pdu::*; -use byteordered::byteorder::{BigEndian, ReadBytesExt}; use dicom_encoding::text::{DefaultCharacterSetCodec, TextCodec}; -use snafu::{ensure, Backtrace, OptionExt, ResultExt, Snafu}; -use std::io::{Cursor, ErrorKind, Read, Seek, SeekFrom}; +use snafu::{ensure, OptionExt, ResultExt}; use tracing::warn; +use bytes::Buf; pub type Result = std::result::Result; -pub fn read_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result -where - R: Read, +pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result> { ensure!( (MINIMUM_PDU_SIZE..=MAXIMUM_PDU_SIZE).contains(&max_pdu_length), @@ -22,16 +19,15 @@ where // this method can block and wake up when stream is closed, so in this case, we // want to know if we had trouble even beginning to read a PDU. We still return // UnexpectedEof if we get after we have already began reading a PDU message. - let mut bytes = [0; 2]; - if let Err(e) = reader.read_exact(&mut bytes) { - ensure!(e.kind() != ErrorKind::UnexpectedEof, NoPduAvailableSnafu); - return Err(e).context(ReadPduFieldSnafu { field: "type" }); + if buf.remaining() < 2 { + return Ok(None); } - + let bytes = buf.copy_to_bytes(2); let pdu_type = bytes[0]; - let pdu_length = reader - .read_u32::() - .context(ReadPduFieldSnafu { field: "length" })?; + if buf.remaining() < 4 { + return Ok(None); + } + let pdu_length = buf.get_u32(); // Check max_pdu_length if strict { @@ -56,9 +52,8 @@ where max_pdu_length ); } - - let bytes = read_n(reader, pdu_length as usize).context(ReadPduSnafu)?; - let mut cursor = Cursor::new(bytes); + if buf.remaining() < pdu_length as usize { return Ok(None); } + let mut bytes = buf.copy_to_bytes(pdu_length as usize); let codec = DefaultCharacterSetCodec; match pdu_type { @@ -74,29 +69,22 @@ where // Version 1 and shall be identified with bit 0 set. A receiver of this PDU // implementing only this version of the DICOM UL protocol shall only test that bit // 0 is set. - let protocol_version = cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Protocol-version", - })?; + if bytes.remaining() < 2 { return Ok(None) } + let protocol_version = bytes.get_u16(); // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not // tested to this value when received. - cursor - .read_u16::() - .context(ReadReservedSnafu { bytes: 2_u32 })?; + if bytes.remaining() < 2 { return Ok(None) } + bytes.get_u16(); // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be encoded // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) // meaning "no Application Name specified" shall not be used. For a complete // description of the use of this field, see Section 7.1.1.4. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .context(ReadPduFieldSnafu { - field: "Called-AE-title", - })?; + let ae_bytes = bytes.copy_to_bytes(16); let called_ae_title = codec - .decode(&ae_bytes) + .decode(ae_bytes.as_ref()) .context(DecodeTextSnafu { field: "Called-AE-title", })? @@ -108,14 +96,9 @@ where // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) // meaning "no Application Name specified" shall not be used. For a complete // description of the use of this field, see Section 7.1.1.3. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .context(ReadPduFieldSnafu { - field: "Calling-AE-title", - })?; + let ae_bytes = bytes.copy_to_bytes(16); let calling_ae_title = codec - .decode(&ae_bytes) + .decode(ae_bytes.as_ref()) .context(DecodeTextSnafu { field: "Calling-AE-title", })? @@ -124,32 +107,34 @@ where // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all // bytes but not tested to this value when received - cursor - .seek(SeekFrom::Current(32)) - .context(ReadReservedSnafu { bytes: 32_u32 })?; + bytes.advance(32); // 75-xxx - Variable items - This variable field shall contain the following items: // one Application Context Item, one or more Presentation Context Items and one User // Information Item. For a complete description of the use of these items see // Section 7.1.1.2, Section 7.1.1.13, and Section 7.1.1.6. - while cursor.position() < cursor.get_ref().len() as u64 { - match read_pdu_variable(&mut cursor, &codec)? { - PduVariableItem::ApplicationContext(val) => { + while bytes.has_remaining() { + match read_pdu_variable(&mut bytes, &codec)? { + Some(PduVariableItem::ApplicationContext(val)) => { application_context_name = Some(val); } - PduVariableItem::PresentationContextProposed(val) => { + Some(PduVariableItem::PresentationContextProposed(val)) => { presentation_contexts.push(val); } - PduVariableItem::UserVariables(val) => { + Some(PduVariableItem::UserVariables(val)) => { user_variables = val; } - var_item => { + Some(var_item) => { return InvalidPduVariableSnafu { var_item }.fail(); + }, + None => { + println!("PDU variable none"); + return Ok(None) } } } - Ok(Pdu::AssociationRQ(AssociationRQ { + Ok(Some(Pdu::AssociationRQ(AssociationRQ { protocol_version, application_context_name: application_context_name .context(MissingApplicationContextNameSnafu)?, @@ -157,7 +142,7 @@ where calling_ae_title, presentation_contexts, user_variables, - })) + }))) } 0x02 => { // A-ASSOCIATE-AC PDU Structure @@ -171,25 +156,18 @@ where // Version 1 and shall be identified with bit 0 set. A receiver of this PDU // implementing only this version of the DICOM UL protocol shall only test that bit // 0 is set. - let protocol_version = cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Protocol-version", - })?; + if bytes.remaining() < 2 { return Ok(None) } + let protocol_version = bytes.get_u16(); // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not // tested to this value when received. - cursor - .read_u16::() - .context(ReadReservedSnafu { bytes: 2_u32 })?; + if bytes.remaining() < 2 { return Ok(None) } + bytes.get_u16(); // 11-26 - Reserved - This reserved field shall be sent with a value identical to // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value // shall not be tested when received. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .context(ReadPduFieldSnafu { - field: "Called-AE-title", - })?; + let ae_bytes = bytes.copy_to_bytes(16); let called_ae_title = codec .decode(&ae_bytes) .context(DecodeTextSnafu { @@ -201,12 +179,7 @@ where // 27-42 - Reserved - This reserved field shall be sent with a value identical to // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value // shall not be tested when received. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .context(ReadPduFieldSnafu { - field: "Calling-AE-title", - })?; + let ae_bytes = bytes.copy_to_bytes(16); let calling_ae_title = codec .decode(&ae_bytes) .context(DecodeTextSnafu { @@ -218,32 +191,31 @@ where // 43-74 - Reserved - This reserved field shall be sent with a value identical to // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value // shall not be tested when received. - cursor - .seek(SeekFrom::Current(32)) - .context(ReadReservedSnafu { bytes: 32_u32 })?; + bytes.advance(32); // 75-xxx - Variable items - This variable field shall contain the following items: // one Application Context Item, one or more Presentation Context Item(s) and one // User Information Item. For a complete description of these items see Section // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. - while cursor.position() < cursor.get_ref().len() as u64 { - match read_pdu_variable(&mut cursor, &codec)? { - PduVariableItem::ApplicationContext(val) => { + while bytes.has_remaining() { + match read_pdu_variable(bytes.clone(), &codec)? { + Some(PduVariableItem::ApplicationContext(val)) => { application_context_name = Some(val); } - PduVariableItem::PresentationContextResult(val) => { + Some(PduVariableItem::PresentationContextResult(val)) => { presentation_contexts.push(val); } - PduVariableItem::UserVariables(val) => { + Some(PduVariableItem::UserVariables(val)) => { user_variables = val; } - var_item => { + Some(var_item) => { return InvalidPduVariableSnafu { var_item }.fail(); - } + }, + None => return Ok(None) } } - Ok(Pdu::AssociationAC(AssociationAC { + Ok(Some(Pdu::AssociationAC(AssociationAC { protocol_version, application_context_name: application_context_name .context(MissingApplicationContextNameSnafu)?, @@ -251,27 +223,23 @@ where calling_ae_title, presentation_contexts, user_variables, - })) + }))) } 0x03 => { // A-ASSOCIATE-RJ PDU Structure // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None) } + bytes.get_u8(); // 8 - Result - This Result field shall contain an integer value encoded as an unsigned // binary number. One of the following values shall be used: // 1 - rejected-permanent // 2 - rejected-transient - let result = AssociationRJResult::from( - cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Result" })?, - ) - .context(InvalidRejectSourceOrReasonSnafu)?; + if bytes.remaining() < 1 { return Ok(None) } + let result = AssociationRJResult::from(bytes.get_u8()) + .context(InvalidRejectSourceOrReasonSnafu)?; // 9 - Source - This Source field shall contain an integer value encoded as an unsigned // binary number. One of the following values shall be used: 1 - DICOM UL @@ -294,17 +262,14 @@ where // 1 - temporary-congestio // 2 - local-limit-exceeded // 3-7 - reserved + if bytes.remaining() < 2 { return Ok(None) } let source = AssociationRJSource::from( - cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Source" })?, - cursor.read_u8().context(ReadPduFieldSnafu { - field: "Reason/Diag.", - })?, + bytes.get_u8(), + bytes.get_u8() ) .context(InvalidRejectSourceOrReasonSnafu)?; - Ok(Pdu::AssociationRJ(AssociationRJ { result, source })) + Ok(Some(Pdu::AssociationRJ(AssociationRJ { result, source }))) } 0x04 => { // P-DATA-TF PDU Structure @@ -313,15 +278,14 @@ where // or more Presentation-data-value Items(s). For a complete description of the use of // this field see Section 9.3.5.1 let mut values = vec![]; - while cursor.position() < cursor.get_ref().len() as u64 { + while bytes.has_remaining() { // Presentation Data Value Item Structure // 1-4 - Item-length - This Item-length shall be the number of bytes from the first // byte of the following field to the last byte of the Presentation-data-value // field. It shall be encoded as an unsigned binary number. - let item_length = cursor.read_u32::().context(ReadPduFieldSnafu { - field: "Item-Length", - })?; + if bytes.remaining() < 4 { return Ok(None) } + let item_length = bytes.get_u32(); ensure!( item_length >= 2, @@ -333,9 +297,8 @@ where // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd // integers between 1 and 255, encoded as an unsigned binary number. For a complete // description of the use of this field see Section 7.1.1.13. - let presentation_context_id = cursor.read_u8().context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; + if bytes.remaining() < 1 { return Ok(None) } + let presentation_context_id = bytes.get_u8(); // 6-xxx - Presentation-data-value - This Presentation-data-value field shall // contain DICOM message information (command and/or data set) with a message @@ -350,9 +313,8 @@ where // following fragment shall contain the last fragment of a Message Data Set or of a // Message Command. If bit 1 is set to 0, the following fragment // does not contain the last fragment of a Message Data Set or of a Message Command. - let header = cursor.read_u8().context(ReadPduFieldSnafu { - field: "Message Control Header", - })?; + if bytes.remaining() < 1 { return Ok(None) } + let header = bytes.get_u8(); let value_type = if header & 0x01 > 0 { PDataValueType::Command @@ -360,43 +322,35 @@ where PDataValueType::Data }; let is_last = (header & 0x02) > 0; - - let data = - read_n(&mut cursor, (item_length - 2) as usize).context(ReadPduFieldSnafu { - field: "Presentation-data-value", - })?; - + if bytes.remaining() < (item_length - 2) as usize { return Ok(None) } values.push(PDataValue { presentation_context_id, value_type, is_last, - data, - }) + data: bytes.copy_to_bytes((item_length - 2) as usize).to_vec(), + }); } - Ok(Pdu::PData { data: values }) + Ok(Some(Pdu::PData { data: values })) } 0x05 => { // A-RELEASE-RQ PDU Structure // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not // tested to this value when received. - cursor - .seek(SeekFrom::Current(4)) - .context(ReadReservedSnafu { bytes: 4_u32 })?; + bytes.advance(4); - Ok(Pdu::ReleaseRQ) + Ok(Some(Pdu::ReleaseRQ)) } 0x06 => { // A-RELEASE-RP PDU Structure // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not // tested to this value when received. - cursor - .seek(SeekFrom::Current(4)) - .context(ReadReservedSnafu { bytes: 4_u32 })?; + if bytes.remaining() < 4 { return Ok(None) } + bytes.advance(4); - Ok(Pdu::ReleaseRP) + Ok(Some(Pdu::ReleaseRP)) } 0x07 => { // A-ABORT PDU Structure @@ -405,10 +359,8 @@ where // this value when received. // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - let mut buf = [0u8; 2]; - cursor - .read_exact(&mut buf) - .context(ReadReservedSnafu { bytes: 2_u32 })?; + if bytes.remaining() < 2 { return Ok(None) } + let mut buf = bytes.copy_to_bytes(2); // 9 - Source - This Source field shall contain an integer value encoded as an unsigned // binary number. One of the following values shall be used: @@ -424,57 +376,41 @@ where // - 4 - unrecognized-PDU parameter // - 5 - unexpected-PDU parameter // - 6 - invalid-PDU-parameter value + if bytes.remaining() < 2 { return Ok(None) } let source = AbortRQSource::from( - cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Source" })?, - cursor.read_u8().context(ReadPduFieldSnafu { - field: "Reason/Diag", - })?, + bytes.get_u8(), + bytes.get_u8() ) .context(InvalidAbortSourceOrReasonSnafu)?; - Ok(Pdu::AbortRQ { source }) + Ok(Some(Pdu::AbortRQ { source })) } _ => { - let data = read_n(&mut cursor, pdu_length as usize) - .context(ReadPduFieldSnafu { field: "Unknown" })?; - Ok(Pdu::Unknown { pdu_type, data }) + if bytes.remaining() < pdu_length as usize {return Ok(None);} + Ok(Some(Pdu::Unknown { + pdu_type, + data: bytes.copy_to_bytes(pdu_length as usize).to_vec() + })) } } } -fn read_n(reader: &mut R, bytes_to_read: usize) -> std::io::Result> -where - R: Read, -{ - let mut result = Vec::new(); - reader.take(bytes_to_read as u64).read_to_end(&mut result)?; - Ok(result) -} - -fn read_pdu_variable(reader: &mut R, codec: &dyn TextCodec) -> Result -where - R: Read, +fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result> { // 1 - Item-type - XXH - let item_type = reader - .read_u8() - .context(ReadPduFieldSnafu { field: "Item-type" })?; + if buf.remaining() < 1 { return Ok(None); } + let item_type = buf.get_u8(); // 2 - Reserved - reader - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if buf.remaining() < 1 { return Ok(None); } + buf.get_u8(); // 3-4 - Item-length - let item_length = reader.read_u16::().context(ReadPduFieldSnafu { - field: "Item-length", - })?; - - let bytes = read_n(reader, item_length as usize).context(ReadPduItemSnafu)?; - let mut cursor = Cursor::new(bytes); + if buf.remaining() < 2 { return Ok(None); } + let item_length = buf.get_u16(); + if buf.remaining() < item_length as usize { return Ok(None); } + let mut bytes = buf.copy_to_bytes(item_length as usize); match item_type { 0x10 => { // Application Context Item Structure @@ -485,11 +421,11 @@ where // Annex A for an overview of this concept). DICOM Application-context-names are // registered in PS3.7. let val = codec - .decode(&cursor.into_inner()) + .decode(&bytes.as_ref()) .context(DecodeTextSnafu { field: "Application-context-name", })?; - Ok(PduVariableItem::ApplicationContext(val)) + Ok(Some(PduVariableItem::ApplicationContext(val))) } 0x20 => { // Presentation Context Item Structure (proposed) @@ -500,48 +436,41 @@ where // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers // between 1 and 255, encoded as an unsigned binary number. For a complete description // of the use of this field see Section 7.1.1.13. - let presentation_context_id = cursor.read_u8().context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; + if bytes.remaining() < 1 { return Ok(None); } + let presentation_context_id = bytes.get_u8(); // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field shall contain the // following sub-items: one Abstract Syntax and one or more Transfer Syntax(es). For a // complete description of the use and encoding of these sub-items see Section 9.3.2.2.1 // and Section 9.3.2.2.2. - while cursor.position() < cursor.get_ref().len() as u64 { + while bytes.has_remaining() { // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Item-type" })?; + if bytes.remaining() < 1 { return Ok(None); } + let item_type = bytes.get_u8(); // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested // to this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 3-4 - Item-length - let item_length = cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Item-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let item_length = bytes.get_u16(); match item_type { 0x30 => { @@ -554,13 +483,10 @@ where // Abstract-syntax-names are structured as UIDs as defined in PS3.5 (see // Annex B for an overview of this concept). DICOM Abstract-syntax-names are // registered in PS3.4. + if bytes.remaining() < item_length as usize { return Ok(None); } abstract_syntax = Some( codec - .decode(&read_n(&mut cursor, item_length as usize).context( - ReadPduFieldSnafu { - field: "Abstract-syntax-name", - }, - )?) + .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) .context(DecodeTextSnafu { field: "Abstract-syntax-name", })? @@ -578,13 +504,10 @@ where // Transfer-syntax-names are structured as UIDs as defined in PS3.5 (see // Annex B for an overview of this concept). DICOM Transfer-syntax-names are // registered in PS3.5. + if bytes.remaining() < item_length as usize { return Ok(None); } transfer_syntaxes.push( codec - .decode(&read_n(&mut cursor, item_length as usize).context( - ReadPduFieldSnafu { - field: "Transfer-syntax-name", - }, - )?) + .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) .context(DecodeTextSnafu { field: "Transfer-syntax-name", })? @@ -598,13 +521,13 @@ where } } - Ok(PduVariableItem::PresentationContextProposed( + Ok(Some(PduVariableItem::PresentationContextProposed( PresentationContextProposed { id: presentation_context_id, abstract_syntax: abstract_syntax.context(MissingAbstractSyntaxSnafu)?, transfer_syntaxes, }, - )) + ))) } 0x21 => { // Presentation Context Item Structure (result) @@ -614,15 +537,13 @@ where // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers // between 1 and 255, encoded as an unsigned binary number. For a complete description // of the use of this field see Section 7.1.1.13. - let presentation_context_id = cursor.read_u8().context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; + if bytes.remaining() < 1 { return Ok(None); } + let presentation_context_id = bytes.get_u8(); // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded // as an unsigned binary number. One of the following values shall be used: @@ -631,40 +552,33 @@ where // 2 - no-reason (provider rejection) // 3 - abstract-syntax-not-supported (provider rejection) // 4 - transfer-syntaxes-not-supported (provider rejection) - let reason = PresentationContextResultReason::from(cursor.read_u8().context( - ReadPduFieldSnafu { - field: "Result/Reason", - }, - )?) - .context(InvalidPresentationContextResultReasonSnafu)?; + if bytes.remaining() < 1 { return Ok(None); } + let reason = PresentationContextResultReason::from(bytes.get_u8()) + .context(InvalidPresentationContextResultReasonSnafu)?; // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer // Syntax Sub-Item. When the Result/Reason field has a value other than acceptance (0), // this field shall not be significant and its value shall not be tested when received. // For a complete description of the use and encoding of this item see Section // 9.3.3.2.1. - while cursor.position() < cursor.get_ref().len() as u64 { + while bytes.has_remaining() { // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Item-type" })?; + if bytes.remaining() < 1 { return Ok(None); } + let item_type = bytes.get_u8(); // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested // to this value when received. - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 3-4 - Item-length - let item_length = cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Item-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let item_length = bytes.get_u16(); match item_type { 0x40 => { @@ -683,15 +597,10 @@ where return MultipleTransferSyntaxesAcceptedSnafu.fail(); } None => { + if bytes.remaining() < item_length as usize { return Ok(None); } transfer_syntax = Some( codec - .decode( - &read_n(&mut cursor, item_length as usize).context( - ReadPduFieldSnafu { - field: "Transfer-syntax-name", - }, - )?, - ) + .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) .context(DecodeTextSnafu { field: "Transfer-syntax-name", })? @@ -707,13 +616,13 @@ where } } - Ok(PduVariableItem::PresentationContextResult( + Ok(Some(PduVariableItem::PresentationContextResult( PresentationContextResult { id: presentation_context_id, reason, transfer_syntax: transfer_syntax.context(MissingTransferSyntaxSnafu)?, }, - )) + ))) } 0x50 => { // User Information Item Structure @@ -723,21 +632,18 @@ where // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined // by the DICOM Application Entity. The structure and content of these sub-items is // defined in Annex D. - while cursor.position() < cursor.get_ref().len() as u64 { + while bytes.has_remaining(){ // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .context(ReadPduFieldSnafu { field: "Item-type" })?; + if bytes.remaining() < 1 { return Ok(None); } + let item_type = bytes.get_u8(); // 2 - Reserved - cursor - .read_u8() - .context(ReadReservedSnafu { bytes: 1_u32 })?; + if bytes.remaining() < 1 { return Ok(None); } + bytes.get_u8(); // 3-4 - Item-length - let item_length = cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Item-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let item_length = bytes.get_u16(); match item_type { 0x51 => { @@ -752,10 +658,9 @@ where // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs // received by the association-requestor. Otherwise, it shall be a protocol // error. + if bytes.remaining() < 4 { return Ok(None); } user_variables.push(UserVariableItem::MaxLength( - cursor.read_u32::().context(ReadPduFieldSnafu { - field: "Maximum-length-received", - })?, + bytes.get_u32() )); } 0x52 => { @@ -765,12 +670,9 @@ where // the Implementation-class-uid of the Association-acceptor as defined in // Section D.3.3.2. The Implementation-class-uid field is structured as a // UID as defined in PS3.5. + if bytes.remaining() < item_length as usize { return Ok(None); } let implementation_class_uid = codec - .decode(&read_n(&mut cursor, item_length as usize).context( - ReadPduFieldSnafu { - field: "Implementation-class-uid", - }, - )?) + .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) .context(DecodeTextSnafu { field: "Implementation-class-uid", })? @@ -787,12 +689,9 @@ where // the Implementation-version-name of the Association-acceptor as defined in // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990 // (basic G0 set) characters. + if bytes.remaining() < item_length as usize { return Ok(None); } let implementation_version_name = codec - .decode(&read_n(&mut cursor, item_length as usize).context( - ReadPduFieldSnafu { - field: "Implementation-version-name", - }, - )?) + .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) .context(DecodeTextSnafu { field: "Implementation-version-name", })? @@ -808,83 +707,60 @@ where // 5-6 - SOP-class-uid-length - The SOP-class-uid-length shall be the number // of bytes from the first byte of the following field to the last byte of the // SOP-class-uid field. It shall be encoded as an unsigned binary number. - let sop_class_uid_length = - cursor.read_u16::().context(ReadPduFieldSnafu { - field: "SOP-class-uid-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let sop_class_uid_length = bytes.get_u16(); // 7 - xxx - SOP-class-uid - The SOP Class or Meta SOP Class identifier // encoded as a UID as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5. + if bytes.remaining() < sop_class_uid_length as usize { return Ok(None); } let sop_class_uid = codec - .decode(&read_n(&mut cursor, sop_class_uid_length as usize).context( - ReadPduFieldSnafu { - field: "SOP-class-uid", - }, - )?) + .decode(bytes.copy_to_bytes(sop_class_uid_length as usize).as_ref()) .context(DecodeTextSnafu { field: "SOP-class-uid", })? .trim() .to_string(); - let data_length = - cursor.read_u16::().context(ReadPduFieldSnafu { - field: "Service-class-application-information-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let data_length = bytes.get_u16(); // xxx-xxx - Service-class-application-information -This field shall contain // the application information specific to the Service Class specification // identified by the SOP-class-uid. The semantics and value of this field // is defined in the identified Service Class specification. - let data = read_n(&mut cursor, data_length as usize).context( - ReadPduFieldSnafu { - field: "Service-class-application-information", - }, - )?; - + if bytes.remaining() < data_length as usize { return Ok(None); } + let data = bytes.copy_to_bytes(data_length as usize); user_variables.push(UserVariableItem::SopClassExtendedNegotiationSubItem( sop_class_uid, - data, + data.to_vec(), )); } 0x58 => { // User Identity Negotiation // 5 - User Identity Type - let user_identity_type = cursor.read_u8().context(ReadPduFieldSnafu { - field: "User-Identity-type", - })?; + if bytes.remaining() < 1 { return Ok(None); } + let user_identity_type = bytes.get_u8(); // 6 - Positive-response-requested - let positive_response_requested = - cursor.read_u8().context(ReadPduFieldSnafu { - field: "User-Identity-positive-response-requested", - })?; + if bytes.remaining() < 1 { return Ok(None); } + let positive_response_requested = bytes.get_u8(); // 7-8 - Primary Field Length - let primary_field_length = - cursor.read_u16::().context(ReadPduFieldSnafu { - field: "User-Identity-primary-field-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let primary_field_length = bytes.get_u16(); // 9-n - Primary Field - let primary_field = read_n(&mut cursor, primary_field_length as usize) - .context(ReadPduFieldSnafu { - field: "User-Identity-primary-field", - })?; - + if bytes.remaining() < primary_field_length as usize { return Ok(None); } + let primary_field = bytes.copy_to_bytes(primary_field_length as usize); // n+1-n+2 - Secondary Field Length // Only non-zero if user identity type is 2 (username and password) - let secondary_field_length = - cursor.read_u16::().context(ReadPduFieldSnafu { - field: "User-Identity-secondary-field-length", - })?; + if bytes.remaining() < 2 { return Ok(None); } + let secondary_field_length = bytes.get_u16(); // n+3-m - Secondary Field - let secondary_field = read_n(&mut cursor, secondary_field_length as usize) - .context(ReadPduFieldSnafu { - field: "User-Identity-secondary-field", - })?; + if bytes.remaining() < secondary_field_length as usize { return Ok(None); } + let secondary_field = bytes.copy_to_bytes(secondary_field_length as usize); match UserIdentityType::from(user_identity_type) { Some(user_identity_type) => { @@ -892,8 +768,8 @@ where UserIdentity::new( positive_response_requested == 1, user_identity_type, - primary_field, - secondary_field, + primary_field.to_vec(), + secondary_field.to_vec(), ), )); } @@ -903,17 +779,17 @@ where } } _ => { + if bytes.remaining() < item_length as usize { return Ok(None); } user_variables.push(UserVariableItem::Unknown( item_type, - read_n(&mut cursor, item_length as usize) - .context(ReadPduFieldSnafu { field: "Unknown" })?, + bytes.copy_to_bytes(item_length as usize).to_vec() )); } } } - Ok(PduVariableItem::UserVariables(user_variables)) + Ok(Some(PduVariableItem::UserVariables(user_variables))) } - _ => Ok(PduVariableItem::Unknown(item_type)), + _ => Ok(Some(PduVariableItem::Unknown(item_type))), } } diff --git a/ul/src/pdu/reader_nonblocking.rs b/ul/src/pdu/reader_nonblocking.rs deleted file mode 100644 index fa746661c..000000000 --- a/ul/src/pdu/reader_nonblocking.rs +++ /dev/null @@ -1,959 +0,0 @@ -/// PDU reader module -use crate::pdu::*; -use dicom_encoding::text::{DefaultCharacterSetCodec, TextCodec}; -use snafu::{ensure, OptionExt, ResultExt}; -use std::io::{Cursor, ErrorKind, Seek, SeekFrom}; -use tokio::io::{AsyncRead, AsyncReadExt}; -use tracing::warn; - -pub type Result = std::result::Result; - -pub async fn read_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result -where - R: AsyncRead + Unpin, -{ - ensure!( - (MINIMUM_PDU_SIZE..=MAXIMUM_PDU_SIZE).contains(&max_pdu_length), - InvalidMaxPduSnafu { max_pdu_length } - ); - - // If we can't read 2 bytes here, that means that there is no PDU - // available. Normally, we want to just return the UnexpectedEof error. However, - // this method can block and wake up when stream is closed, so in this case, we - // want to know if we had trouble even beginning to read a PDU. We still return - // UnexpectedEof if we get after we have already began reading a PDU message. - let mut bytes = [0; 2]; - if let Err(e) = reader.read_exact(&mut bytes).await { - ensure!(e.kind() != ErrorKind::UnexpectedEof, NoPduAvailableSnafu); - return Err(e).context(ReadPduFieldSnafu { field: "type" }); - } - - let pdu_type = bytes[0]; - let pdu_length = reader - .read_u32() - .await - .context(ReadPduFieldSnafu { field: "length" })?; - - // Check max_pdu_length - if strict { - ensure!( - pdu_length <= max_pdu_length, - PduTooLargeSnafu { - pdu_length, - max_pdu_length - } - ); - } else if pdu_length > max_pdu_length { - ensure!( - pdu_length <= MAXIMUM_PDU_SIZE, - PduTooLargeSnafu { - pdu_length, - max_pdu_length: MAXIMUM_PDU_SIZE - } - ); - tracing::warn!( - "Incoming pdu was too large: length {}, maximum is {}", - pdu_length, - max_pdu_length - ); - } - - let bytes = read_n(reader, pdu_length as usize) - .await - .context(ReadPduSnafu)?; - let mut cursor = Cursor::new(bytes); - let codec = DefaultCharacterSetCodec; - - match pdu_type { - 0x01 => { - // A-ASSOCIATE-RQ PDU Structure - - let mut application_context_name: Option = None; - let mut presentation_contexts = vec![]; - let mut user_variables = vec![]; - - // 7-8 - Protocol-version - This two byte field shall use one bit to identify each - // version of the DICOM UL protocol supported by the calling end-system. This is - // Version 1 and shall be identified with bit 0 set. A receiver of this PDU - // implementing only this version of the DICOM UL protocol shall only test that bit - // 0 is set. - let protocol_version = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Protocol-version", - })?; - - // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not - // tested to this value when received. - cursor - .read_u16() - .await - .context(ReadReservedSnafu { bytes: 2_u32 })?; - - // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be encoded - // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and - // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) - // meaning "no Application Name specified" shall not be used. For a complete - // description of the use of this field, see Section 7.1.1.4. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .await - .context(ReadPduFieldSnafu { - field: "Called-AE-title", - })?; - let called_ae_title = codec - .decode(&ae_bytes) - .context(DecodeTextSnafu { - field: "Called-AE-title", - })? - .trim() - .to_string(); - - // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded as - // 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and - // trailing spaces (20H) being non-significant. The value made of 16 spaces (20H) - // meaning "no Application Name specified" shall not be used. For a complete - // description of the use of this field, see Section 7.1.1.3. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .await - .context(ReadPduFieldSnafu { - field: "Calling-AE-title", - })?; - let calling_ae_title = codec - .decode(&ae_bytes) - .context(DecodeTextSnafu { - field: "Calling-AE-title", - })? - .trim() - .to_string(); - - // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all - // bytes but not tested to this value when received - cursor - .seek(SeekFrom::Current(32)) - .context(ReadReservedSnafu { bytes: 32_u32 })?; - - // 75-xxx - Variable items - This variable field shall contain the following items: - // one Application Context Item, one or more Presentation Context Items and one User - // Information Item. For a complete description of the use of these items see - // Section 7.1.1.2, Section 7.1.1.13, and Section 7.1.1.6. - while cursor.position() < cursor.get_ref().len() as u64 { - match read_pdu_variable(&mut cursor, &codec).await? { - PduVariableItem::ApplicationContext(val) => { - application_context_name = Some(val); - } - PduVariableItem::PresentationContextProposed(val) => { - presentation_contexts.push(val); - } - PduVariableItem::UserVariables(val) => { - user_variables = val; - } - var_item => { - return InvalidPduVariableSnafu { var_item }.fail(); - } - } - } - - Ok(Pdu::AssociationRQ(AssociationRQ { - protocol_version, - application_context_name: application_context_name - .context(MissingApplicationContextNameSnafu)?, - called_ae_title, - calling_ae_title, - presentation_contexts, - user_variables, - })) - } - 0x02 => { - // A-ASSOCIATE-AC PDU Structure - - let mut application_context_name: Option = None; - let mut presentation_contexts = vec![]; - let mut user_variables = vec![]; - - // 7-8 - Protocol-version - This two byte field shall use one bit to identify each - // version of the DICOM UL protocol supported by the calling end-system. This is - // Version 1 and shall be identified with bit 0 set. A receiver of this PDU - // implementing only this version of the DICOM UL protocol shall only test that bit - // 0 is set. - let protocol_version = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Protocol-version", - })?; - - // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not - // tested to this value when received. - cursor - .read_u16() - .await - .context(ReadReservedSnafu { bytes: 2_u32 })?; - - // 11-26 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .await - .context(ReadPduFieldSnafu { - field: "Called-AE-title", - })?; - let called_ae_title = codec - .decode(&ae_bytes) - .context(DecodeTextSnafu { - field: "Called-AE-title", - })? - .trim() - .to_string(); - - // 27-42 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - let mut ae_bytes = [0; 16]; - cursor - .read_exact(&mut ae_bytes) - .await - .context(ReadPduFieldSnafu { - field: "Calling-AE-title", - })?; - let calling_ae_title = codec - .decode(&ae_bytes) - .context(DecodeTextSnafu { - field: "Calling-AE-title", - })? - .trim() - .to_string(); - - // 43-74 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - cursor - .seek(SeekFrom::Current(32)) - .context(ReadReservedSnafu { bytes: 32_u32 })?; - - // 75-xxx - Variable items - This variable field shall contain the following items: - // one Application Context Item, one or more Presentation Context Item(s) and one - // User Information Item. For a complete description of these items see Section - // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. - while cursor.position() < cursor.get_ref().len() as u64 { - match read_pdu_variable(&mut cursor, &codec).await? { - PduVariableItem::ApplicationContext(val) => { - application_context_name = Some(val); - } - PduVariableItem::PresentationContextResult(val) => { - presentation_contexts.push(val); - } - PduVariableItem::UserVariables(val) => { - user_variables = val; - } - var_item => { - return InvalidPduVariableSnafu { var_item }.fail(); - } - } - } - - Ok(Pdu::AssociationAC(AssociationAC { - protocol_version, - application_context_name: application_context_name - .context(MissingApplicationContextNameSnafu)?, - called_ae_title, - calling_ae_title, - presentation_contexts, - user_variables, - })) - } - 0x03 => { - // A-ASSOCIATE-RJ PDU Structure - - // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 8 - Result - This Result field shall contain an integer value encoded as an unsigned - // binary number. One of the following values shall be used: - // 1 - rejected-permanent - // 2 - rejected-transient - let result = AssociationRJResult::from( - cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Result" })?, - ) - .context(InvalidRejectSourceOrReasonSnafu)?; - - // 9 - Source - This Source field shall contain an integer value encoded as an unsigned - // binary number. One of the following values shall be used: 1 - DICOM UL - // service-user 2 - DICOM UL service-provider (ACSE related function) - // 3 - DICOM UL service-provider (Presentation related function) - // 10 - Reason/Diag. - This field shall contain an integer value encoded as an unsigned - // binary number. If the Source field has the value (1) "DICOM UL - // service-user", it shall take one of the following: - // 1 - no-reason-given - // 2 - application-context-name-not-supported - // 3 - calling-AE-title-not-recognized - // 4-6 - reserved - // 7 - called-AE-title-not-recognized - // 8-10 - reserved - // If the Source field has the value (2) "DICOM UL service provided (ACSE related - // function)", it shall take one of the following: 1 - no-reason-given - // 2 - protocol-version-not-supported - // If the Source field has the value (3) "DICOM UL service provided (Presentation - // related function)", it shall take one of the following: 0 - reserved - // 1 - temporary-congestio - // 2 - local-limit-exceeded - // 3-7 - reserved - let source = AssociationRJSource::from( - cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Source" })?, - cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Reason/Diag.", - })?, - ) - .context(InvalidRejectSourceOrReasonSnafu)?; - - Ok(Pdu::AssociationRJ(AssociationRJ { result, source })) - } - 0x04 => { - // P-DATA-TF PDU Structure - - // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain one - // or more Presentation-data-value Items(s). For a complete description of the use of - // this field see Section 9.3.5.1 - let mut values = vec![]; - while cursor.position() < cursor.get_ref().len() as u64 { - // Presentation Data Value Item Structure - - // 1-4 - Item-length - This Item-length shall be the number of bytes from the first - // byte of the following field to the last byte of the Presentation-data-value - // field. It shall be encoded as an unsigned binary number. - let item_length = cursor.read_u32().await.context(ReadPduFieldSnafu { - field: "Item-Length", - })?; - - ensure!( - item_length >= 2, - InvalidItemLengthSnafu { - length: item_length - } - ); - - // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd - // integers between 1 and 255, encoded as an unsigned binary number. For a complete - // description of the use of this field see Section 7.1.1.13. - let presentation_context_id = - cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; - - // 6-xxx - Presentation-data-value - This Presentation-data-value field shall - // contain DICOM message information (command and/or data set) with a message - // control header. For a complete description of the use of this field see Annex E. - - // The Message Control Header shall be made of one byte with the least significant - // bit (bit 0) taking one of the following values: If bit 0 is set - // to 1, the following fragment shall contain Message Command information. - // If bit 0 is set to 0, the following fragment shall contain Message Data Set - // information. The next least significant bit (bit 1) shall be - // defined by the following rules: If bit 1 is set to 1, the - // following fragment shall contain the last fragment of a Message Data Set or of a - // Message Command. If bit 1 is set to 0, the following fragment - // does not contain the last fragment of a Message Data Set or of a Message Command. - let header = cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Message Control Header", - })?; - - let value_type = if header & 0x01 > 0 { - PDataValueType::Command - } else { - PDataValueType::Data - }; - let is_last = (header & 0x02) > 0; - - let data = read_n(&mut cursor, (item_length - 2) as usize) - .await - .context(ReadPduFieldSnafu { - field: "Presentation-data-value", - })?; - - values.push(PDataValue { - presentation_context_id, - value_type, - is_last, - data, - }) - } - - Ok(Pdu::PData { data: values }) - } - 0x05 => { - // A-RELEASE-RQ PDU Structure - - // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not - // tested to this value when received. - cursor - .seek(SeekFrom::Current(4)) - .context(ReadReservedSnafu { bytes: 4_u32 })?; - - Ok(Pdu::ReleaseRQ) - } - 0x06 => { - // A-RELEASE-RP PDU Structure - - // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not - // tested to this value when received. - cursor - .seek(SeekFrom::Current(4)) - .context(ReadReservedSnafu { bytes: 4_u32 })?; - - Ok(Pdu::ReleaseRP) - } - 0x07 => { - // A-ABORT PDU Structure - - // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - let mut buf = [0u8; 2]; - cursor - .read_exact(&mut buf) - .await - .context(ReadReservedSnafu { bytes: 2_u32 })?; - - // 9 - Source - This Source field shall contain an integer value encoded as an unsigned - // binary number. One of the following values shall be used: - // - 0 - DICOM UL service-user (initiated abort) - // - 1 - reserved - // - 2 - DICOM UL service-provider (initiated abort) - // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned - // binary number. If the Source field has the value (2) "DICOM UL - // service-provider", it shall take one of the following: - // - 0 - reason-not-specified1 - unrecognized-PDU - // - 2 - unexpected-PDU - // - 3 - reserved - // - 4 - unrecognized-PDU parameter - // - 5 - unexpected-PDU parameter - // - 6 - invalid-PDU-parameter value - let source = AbortRQSource::from( - cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Source" })?, - cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Reason/Diag", - })?, - ) - .context(InvalidAbortSourceOrReasonSnafu)?; - - Ok(Pdu::AbortRQ { source }) - } - _ => { - let data = read_n(&mut cursor, pdu_length as usize) - .await - .context(ReadPduFieldSnafu { field: "Unknown" })?; - Ok(Pdu::Unknown { pdu_type, data }) - } - } -} - -async fn read_n(reader: &mut R, bytes_to_read: usize) -> std::io::Result> -where - R: AsyncRead + Unpin, -{ - let mut result = Vec::new(); - reader - .take(bytes_to_read as u64) - .read_to_end(&mut result) - .await?; - Ok(result) -} - -async fn read_pdu_variable(reader: &mut R, codec: &dyn TextCodec) -> Result -where - R: AsyncRead + Unpin, -{ - // 1 - Item-type - XXH - let item_type = reader - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - reader - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 3-4 - Item-length - let item_length = reader.read_u16().await.context(ReadPduFieldSnafu { - field: "Item-length", - })?; - - let bytes = read_n(reader, item_length as usize) - .await - .context(ReadPduItemSnafu)?; - let mut cursor = Cursor::new(bytes); - - match item_type { - 0x10 => { - // Application Context Item Structure - - // 5-xxx - Application-context-name - A valid Application-context-name shall be encoded - // as defined in Annex F. For a description of the use of this field see Section - // 7.1.1.2. Application-context-names are structured as UIDs as defined in PS3.5 (see - // Annex A for an overview of this concept). DICOM Application-context-names are - // registered in PS3.7. - let val = codec - .decode(&cursor.into_inner()) - .context(DecodeTextSnafu { - field: "Application-context-name", - })?; - Ok(PduVariableItem::ApplicationContext(val)) - } - 0x20 => { - // Presentation Context Item Structure (proposed) - - let mut abstract_syntax: Option = None; - let mut transfer_syntaxes = vec![]; - - // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers - // between 1 and 255, encoded as an unsigned binary number. For a complete description - // of the use of this field see Section 7.1.1.13. - let presentation_context_id = cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; - - // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field shall contain the - // following sub-items: one Abstract Syntax and one or more Transfer Syntax(es). For a - // complete description of the use and encoding of these sub-items see Section 9.3.2.2.1 - // and Section 9.3.2.2.2. - while cursor.position() < cursor.get_ref().len() as u64 { - // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested - // to this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 3-4 - Item-length - let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Item-length", - })?; - - match item_type { - 0x30 => { - // Abstract Syntax Sub-Item Structure - - // 5-xxx - Abstract-syntax-name - This variable field shall contain the - // Abstract-syntax-name related to the proposed presentation context. A - // valid Abstract-syntax-name shall be encoded as defined in Annex F. For a - // description of the use of this field see Section 7.1.1.13. - // Abstract-syntax-names are structured as UIDs as defined in PS3.5 (see - // Annex B for an overview of this concept). DICOM Abstract-syntax-names are - // registered in PS3.4. - abstract_syntax = Some( - codec - .decode(&read_n(&mut cursor, item_length as usize).await.context( - ReadPduFieldSnafu { - field: "Abstract-syntax-name", - }, - )?) - .context(DecodeTextSnafu { - field: "Abstract-syntax-name", - })? - .trim() - .to_string(), - ); - } - 0x40 => { - // Transfer Syntax Sub-Item Structure - - // 5-xxx - Transfer-syntax-name(s) - This variable field shall contain the - // Transfer-syntax-name proposed for this presentation context. A valid - // Transfer-syntax-name shall be encoded as defined in Annex F. For a - // description of the use of this field see Section 7.1.1.13. - // Transfer-syntax-names are structured as UIDs as defined in PS3.5 (see - // Annex B for an overview of this concept). DICOM Transfer-syntax-names are - // registered in PS3.5. - transfer_syntaxes.push( - codec - .decode(&read_n(&mut cursor, item_length as usize).await.context( - ReadPduFieldSnafu { - field: "Transfer-syntax-name", - }, - )?) - .context(DecodeTextSnafu { - field: "Transfer-syntax-name", - })? - .trim() - .to_string(), - ); - } - _ => { - return UnknownPresentationContextSubItemSnafu.fail(); - } - } - } - - Ok(PduVariableItem::PresentationContextProposed( - PresentationContextProposed { - id: presentation_context_id, - abstract_syntax: abstract_syntax.context(MissingAbstractSyntaxSnafu)?, - transfer_syntaxes, - }, - )) - } - 0x21 => { - // Presentation Context Item Structure (result) - - let mut transfer_syntax: Option = None; - - // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers - // between 1 and 255, encoded as an unsigned binary number. For a complete description - // of the use of this field see Section 7.1.1.13. - let presentation_context_id = cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "Presentation-context-ID", - })?; - - // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded - // as an unsigned binary number. One of the following values shall be used: - // 0 - acceptance - // 1 - user-rejection - // 2 - no-reason (provider rejection) - // 3 - abstract-syntax-not-supported (provider rejection) - // 4 - transfer-syntaxes-not-supported (provider rejection) - let reason = PresentationContextResultReason::from(cursor.read_u8().await.context( - ReadPduFieldSnafu { - field: "Result/Reason", - }, - )?) - .context(InvalidPresentationContextResultReasonSnafu)?; - - // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer - // Syntax Sub-Item. When the Result/Reason field has a value other than acceptance (0), - // this field shall not be significant and its value shall not be tested when received. - // For a complete description of the use and encoding of this item see Section - // 9.3.3.2.1. - while cursor.position() < cursor.get_ref().len() as u64 { - // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested - // to this value when received. - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 3-4 - Item-length - let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Item-length", - })?; - - match item_type { - 0x40 => { - // Transfer Syntax Sub-Item Structure - - // 5-xxx - Transfer-syntax-name(s) - This variable field shall contain the - // Transfer-syntax-name proposed for this presentation context. A valid - // Transfer-syntax-name shall be encoded as defined in Annex F. For a - // description of the use of this field see Section 7.1.1.13. - // Transfer-syntax-names are structured as UIDs as defined in PS3.5 (see - // Annex B for an overview of this concept). DICOM Transfer-syntax-names are - // registered in PS3.5. - match transfer_syntax { - Some(_) => { - // Multiple transfer syntax values cannot be proposed. - return MultipleTransferSyntaxesAcceptedSnafu.fail(); - } - None => { - transfer_syntax = Some( - codec - .decode( - &read_n(&mut cursor, item_length as usize) - .await - .context(ReadPduFieldSnafu { - field: "Transfer-syntax-name", - })?, - ) - .context(DecodeTextSnafu { - field: "Transfer-syntax-name", - })? - .trim() - .to_string(), - ); - } - } - } - _ => { - return InvalidTransferSyntaxSubItemSnafu.fail(); - } - } - } - - Ok(PduVariableItem::PresentationContextResult( - PresentationContextResult { - id: presentation_context_id, - reason, - transfer_syntax: transfer_syntax.context(MissingTransferSyntaxSnafu)?, - }, - )) - } - 0x50 => { - // User Information Item Structure - - let mut user_variables = vec![]; - - // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined - // by the DICOM Application Entity. The structure and content of these sub-items is - // defined in Annex D. - while cursor.position() < cursor.get_ref().len() as u64 { - // 1 - Item-type - XXH - let item_type = cursor - .read_u8() - .await - .context(ReadPduFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - cursor - .read_u8() - .await - .context(ReadReservedSnafu { bytes: 1_u32 })?; - - // 3-4 - Item-length - let item_length = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Item-length", - })?; - - match item_type { - 0x51 => { - // Maximum Length Sub-Item Structure - - // 5-8 - Maximum-length-received - This parameter allows the - // association-requestor to restrict the maximum length of the variable - // field of the P-DATA-TF PDUs sent by the acceptor on the association once - // established. This length value is indicated as a number of bytes encoded - // as an unsigned binary number. The value of (0) indicates that no maximum - // length is specified. This maximum length value shall never be exceeded by - // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs - // received by the association-requestor. Otherwise, it shall be a protocol - // error. - user_variables.push(UserVariableItem::MaxLength( - cursor.read_u32().await.context(ReadPduFieldSnafu { - field: "Maximum-length-received", - })?, - )); - } - 0x52 => { - // Implementation Class UID Sub-Item Structure - - // 5 - xxx - Implementation-class-uid - This variable field shall contain - // the Implementation-class-uid of the Association-acceptor as defined in - // Section D.3.3.2. The Implementation-class-uid field is structured as a - // UID as defined in PS3.5. - let implementation_class_uid = codec - .decode(&read_n(&mut cursor, item_length as usize).await.context( - ReadPduFieldSnafu { - field: "Implementation-class-uid", - }, - )?) - .context(DecodeTextSnafu { - field: "Implementation-class-uid", - })? - .trim() - .to_string(); - user_variables.push(UserVariableItem::ImplementationClassUID( - implementation_class_uid, - )); - } - 0x55 => { - // Implementation Version Name Structure - - // 5 - xxx - Implementation-version-name - This variable field shall contain - // the Implementation-version-name of the Association-acceptor as defined in - // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990 - // (basic G0 set) characters. - let implementation_version_name = codec - .decode(&read_n(&mut cursor, item_length as usize).await.context( - ReadPduFieldSnafu { - field: "Implementation-version-name", - }, - )?) - .context(DecodeTextSnafu { - field: "Implementation-version-name", - })? - .trim() - .to_string(); - user_variables.push(UserVariableItem::ImplementationVersionName( - implementation_version_name, - )); - } - 0x56 => { - // SOP Class Extended Negotiation Sub-Item - - // 5-6 - SOP-class-uid-length - The SOP-class-uid-length shall be the number - // of bytes from the first byte of the following field to the last byte of the - // SOP-class-uid field. It shall be encoded as an unsigned binary number. - let sop_class_uid_length = - cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "SOP-class-uid-length", - })?; - - // 7 - xxx - SOP-class-uid - The SOP Class or Meta SOP Class identifier - // encoded as a UID as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5. - let sop_class_uid = codec - .decode( - &read_n(&mut cursor, sop_class_uid_length as usize) - .await - .context(ReadPduFieldSnafu { - field: "SOP-class-uid", - })?, - ) - .context(DecodeTextSnafu { - field: "SOP-class-uid", - })? - .trim() - .to_string(); - - let data_length = cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "Service-class-application-information-length", - })?; - - // xxx-xxx - Service-class-application-information -This field shall contain - // the application information specific to the Service Class specification - // identified by the SOP-class-uid. The semantics and value of this field - // is defined in the identified Service Class specification. - let data = read_n(&mut cursor, data_length as usize).await.context( - ReadPduFieldSnafu { - field: "Service-class-application-information", - }, - )?; - - user_variables.push(UserVariableItem::SopClassExtendedNegotiationSubItem( - sop_class_uid, - data, - )); - } - 0x58 => { - // User Identity Negotiation - - // 5 - User Identity Type - let user_identity_type = - cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "User-Identity-type", - })?; - - // 6 - Positive-response-requested - let positive_response_requested = - cursor.read_u8().await.context(ReadPduFieldSnafu { - field: "User-Identity-positive-response-requested", - })?; - - // 7-8 - Primary Field Length - let primary_field_length = - cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "User-Identity-primary-field-length", - })?; - - // 9-n - Primary Field - let primary_field = read_n(&mut cursor, primary_field_length as usize) - .await - .context(ReadPduFieldSnafu { - field: "User-Identity-primary-field", - })?; - - // n+1-n+2 - Secondary Field Length - // Only non-zero if user identity type is 2 (username and password) - let secondary_field_length = - cursor.read_u16().await.context(ReadPduFieldSnafu { - field: "User-Identity-secondary-field-length", - })?; - - // n+3-m - Secondary Field - let secondary_field = read_n(&mut cursor, secondary_field_length as usize) - .await - .context(ReadPduFieldSnafu { - field: "User-Identity-secondary-field", - })?; - - match UserIdentityType::from(user_identity_type) { - Some(user_identity_type) => { - user_variables.push(UserVariableItem::UserIdentityItem( - UserIdentity::new( - positive_response_requested == 1, - user_identity_type, - primary_field, - secondary_field, - ), - )); - } - None => { - warn!("Unknown User Identity Type code {}", user_identity_type); - } - } - } - _ => { - user_variables.push(UserVariableItem::Unknown( - item_type, - read_n(&mut cursor, item_length as usize) - .await - .context(ReadPduFieldSnafu { field: "Unknown" })?, - )); - } - } - } - - Ok(PduVariableItem::UserVariables(user_variables)) - } - _ => Ok(PduVariableItem::Unknown(item_type)), - } -} diff --git a/ul/src/pdu/writer_nonblocking.rs b/ul/src/pdu/writer_nonblocking.rs deleted file mode 100644 index 708fa4375..000000000 --- a/ul/src/pdu/writer_nonblocking.rs +++ /dev/null @@ -1,1365 +0,0 @@ -use std::future::Future; - -/// PDU writer module -use crate::pdu::*; -use dicom_encoding::text::TextCodec; -use snafu::{Backtrace, ResultExt, Snafu}; -use tokio::io::{AsyncWrite, AsyncWriteExt}; - -pub type Result = std::result::Result; - -#[derive(Debug, Snafu)] -pub enum WriteChunkError { - #[snafu(display("Failed to build chunk"))] - BuildChunk { - #[snafu(backtrace)] - source: Box, - }, - #[snafu(display("Failed to write chunk length"))] - WriteLength { - backtrace: Backtrace, - source: std::io::Error, - }, - #[snafu(display("Failed to write chunk data"))] - WriteData { - backtrace: Backtrace, - source: std::io::Error, - }, -} - -async fn write_chunk_u32( - writer: &mut W, - func: F, -) -> std::result::Result<(), WriteChunkError> -where - W: AsyncWrite + Unpin, - F: FnOnce() -> Fut, - Fut: Future>> + Send, -{ - let data = func().await.map_err(Box::from).context(BuildChunkSnafu)?; - - let length = data.len() as u32; - writer.write_u32(length).await.context(WriteLengthSnafu)?; - - writer.write_all(&data).await.context(WriteDataSnafu)?; - - Ok(()) -} - -async fn write_chunk_u16( - writer: &mut W, - func: F, -) -> std::result::Result<(), WriteChunkError> -where - W: AsyncWrite + Unpin, - F: FnOnce() -> Fut, - Fut: Future>> + Send, -{ - // NOTE: If I kept the original design, i,e F: FnOnce(&mut Vec) -> Box> + Send + Unpin>, - // I ended up with lifetime issues, but I don't with this. Not sure how to fix those lifetime issues so I just went with this for now - let data = func().await.map_err(Box::from).context(BuildChunkSnafu)?; - - let length = data.len() as u16; - writer.write_u16(length).await.context(WriteLengthSnafu)?; - - writer.write_all(&data).await.context(WriteDataSnafu)?; - - Ok(()) -} - -pub async fn write_pdu(writer: &mut W, pdu: &Pdu) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - let codec = dicom_encoding::text::DefaultCharacterSetCodec; - match pdu { - Pdu::AssociationRQ(AssociationRQ { - protocol_version, - calling_ae_title, - called_ae_title, - application_context_name, - presentation_contexts, - user_variables, - }) => { - // A-ASSOCIATE-RQ PDU Structure - - // 1 - PDU-type - 01H - writer - .write_u8(0x01) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - // 7-8 Protocol-version - This two byte field shall use one bit to identify - // each version of the DICOM UL protocol supported by the calling end-system. - // This is Version 1 and shall be identified with bit 0 set. A receiver of this - // PDU implementing only this version of the DICOM UL protocol shall only test - // that bit 0 is set. - let mut writer = vec![]; - writer - .write_u16(*protocol_version) - .await - .context(WriteFieldSnafu { - field: "Protocol-version", - })?; - - // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but - // not tested to this value when received. - writer - .write_u16(0x00) - .await - .context(WriteReservedSnafu { bytes: 2_u32 })?; - - // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be - // encoded as 16 characters as defined by the ISO 646:1990-Basic G0 Set with - // leading and trailing spaces (20H) being non-significant. The value made of 16 - // spaces (20H) meaning "no Application Name specified" shall not be used. For a - // complete description of the use of this field, see Section 7.1.1.4. - let mut ae_title_bytes = - codec.encode(called_ae_title).context(EncodeFieldSnafu { - field: "Called-AE-title", - })?; - ae_title_bytes.resize(16, b' '); - writer - .write_all(&ae_title_bytes) - .await - .context(WriteFieldSnafu { - field: "Called-AE-title", - })?; - - // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded - // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and - // trailing spaces (20H) being non-significant. The value made of 16 spaces - // (20H) meaning "no Application Name specified" shall not be used. For a - // complete description of the use of this field, see Section 7.1.1.3. - let mut ae_title_bytes = - codec.encode(calling_ae_title).context(EncodeFieldSnafu { - field: "Calling-AE-title", - })?; - ae_title_bytes.resize(16, b' '); - writer - .write_all(&ae_title_bytes) - .await - .context(WriteFieldSnafu { - field: "Called-AE-title", - })?; - - // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all - // bytes but not tested to this value when received - writer - .write_all(&[0; 32]) - .await - .context(WriteReservedSnafu { bytes: 32_u32 })?; - - write_pdu_variable_application_context_name( - &mut writer, - application_context_name, - &codec, - ) - .await?; - - for presentation_context in presentation_contexts { - write_pdu_variable_presentation_context_proposed( - &mut writer, - presentation_context, - &codec, - ) - .await?; - } - - write_pdu_variable_user_variables(&mut writer, user_variables, &codec).await?; - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "A-ASSOCIATE-RQ", - })?; - - Ok(()) - } - Pdu::AssociationAC(AssociationAC { - protocol_version, - application_context_name, - called_ae_title, - calling_ae_title, - presentation_contexts, - user_variables, - }) => { - // A-ASSOCIATE-AC PDU Structure - - // 1 - PDU-type - 02H - writer - .write_u8(0x02) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - // 7-8 - Protocol-version - This two byte field shall use one bit to identify each - // version of the DICOM UL protocol supported by the calling end-system. This is - // Version 1 and shall be identified with bit 0 set. A receiver of this PDU - // implementing only this version of the DICOM UL protocol shall only test that bit - // 0 is set. - let mut writer = vec![]; - writer - .write_u16(*protocol_version) - .await - .context(WriteFieldSnafu { - field: "Protocol-version", - })?; - - // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not - // tested to this value when received. - writer - .write_u16(0x00) - .await - .context(WriteReservedSnafu { bytes: 2_u32 })?; - - // 11-26 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - let mut ae_title_bytes = - codec.encode(called_ae_title).context(EncodeFieldSnafu { - field: "Called-AE-title", - })?; - ae_title_bytes.resize(16, b' '); - writer - .write_all(&ae_title_bytes) - .await - .context(WriteFieldSnafu { - field: "Called-AE-title", - })?; - // 27-42 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - let mut ae_title_bytes = - codec.encode(calling_ae_title).context(EncodeFieldSnafu { - field: "Calling-AE-title", - })?; - ae_title_bytes.resize(16, b' '); - writer - .write_all(&ae_title_bytes) - .await - .context(WriteFieldSnafu { - field: "Calling-AE-title", - })?; - - // 43-74 - Reserved - This reserved field shall be sent with a value identical to - // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value - // shall not be tested when received. - writer - .write_all(&[0; 32]) - .await - .context(WriteReservedSnafu { bytes: 32_u32 })?; - - // 75-xxx - Variable items - This variable field shall contain the following items: - // one Application Context Item, one or more Presentation Context Item(s) and one - // User Information Item. For a complete description of these items see Section - // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. - write_pdu_variable_application_context_name( - &mut writer, - application_context_name, - &codec, - ) - .await?; - - for presentation_context in presentation_contexts { - write_pdu_variable_presentation_context_result( - &mut writer, - presentation_context, - &codec, - ) - .await?; - } - - write_pdu_variable_user_variables(&mut writer, user_variables, &codec).await?; - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "A-ASSOCIATE-AC", - }) - } - Pdu::AssociationRJ(AssociationRJ { result, source }) => { - // 1 - PDU-type - 03H - writer - .write_u8(0x03) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 8 - Result - This Result field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used: - // - 1 - rejected-permanent - // - 2 - rejected-transient - writer.write_u8(match result { - AssociationRJResult::Permanent => { - 0x01 - } - AssociationRJResult::Transient => { - 0x02 - } - }) - .await - .context(WriteFieldSnafu { field: "AssociationRJResult" })?; - - // 9 - Source - This Source field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used: - // - 1 - DICOM UL service-user - // - 2 - DICOM UL service-provider (ACSE related function) - // - 3 - DICOM UL service-provider (Presentation related function) - // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned binary number. - // If the Source field has the value (1) "DICOM UL service-user", it shall take one of the following: - // - 1 - no-reason-given - // - 2 - application-context-name-not-supported - // - 3 - calling-AE-title-not-recognized - // - 4-6 - reserved - // - 7 - called-AE-title-not-recognized - // - 8-10 - reserved - // If the Source field has the value (2) "DICOM UL service provided (ACSE related function)", it shall take one of the following: - // - 1 - no-reason-given - // - 2 - protocol-version-not-supported - // If the Source field has the value (3) "DICOM UL service provided (Presentation related function)", it shall take one of the following: - // 0 - reserved - // 1 - temporary-congestion - // 2 - local-limit-exceeded - // 3-7 - reserved - match source { - AssociationRJSource::ServiceUser(reason) => { - writer.write_u8(0x01).await.context(WriteFieldSnafu { field: "AssociationRJServiceUserReason" })?; - writer.write_u8(match reason { - AssociationRJServiceUserReason::NoReasonGiven => { - 0x01 - } - AssociationRJServiceUserReason::ApplicationContextNameNotSupported => { - 0x02 - } - AssociationRJServiceUserReason::CallingAETitleNotRecognized => { - 0x03 - } - AssociationRJServiceUserReason::CalledAETitleNotRecognized => { - 0x07 - } - AssociationRJServiceUserReason::Reserved(data) => { - *data - } - }).await.context(WriteFieldSnafu { field: "AssociationRJServiceUserReason (2)" })?; - } - AssociationRJSource::ServiceProviderASCE(reason) => { - writer.write_u8(0x02).await.context(WriteFieldSnafu { field: "AssociationRJServiceProvider" })?; - writer.write_u8(match reason { - AssociationRJServiceProviderASCEReason::NoReasonGiven => { - 0x01 - } - AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => { - 0x02 - } - }).await.context(WriteFieldSnafu { field: "AssociationRJServiceProvider (2)" })?; - } - AssociationRJSource::ServiceProviderPresentation(reason) => { - writer.write_u8(0x03).await.context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason" })?; - writer.write_u8(match reason { - AssociationRJServiceProviderPresentationReason::TemporaryCongestion => { - 0x01 - } - AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => { - 0x02 - } - AssociationRJServiceProviderPresentationReason::Reserved(data) => { - *data - } - }).await.context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason (2)" })?; - } - } - - Ok(writer) - }).await.context(WriteChunkSnafu { name: "AssociationRJ" })?; - - Ok(()) - } - Pdu::PData { data } => { - // 1 - PDU-type - 04H - writer - .write_u8(0x04) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain - // one or more Presentation-data-value Items(s). For a complete description of the - // use of this field see Section 9.3.5.1 - - for presentation_data_value in data { - write_chunk_u32(&mut writer, || async { - let mut writer = vec![]; - // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd - // integers between 1 and 255, encoded as an unsigned binary number. For a - // complete description of the use of this field see Section 7.1.1.13. - writer.push(presentation_data_value.presentation_context_id); - - // 6-xxx - Presentation-data-value - This Presentation-data-value field - // shall contain DICOM message information (command and/or data set) with a - // message control header. For a complete description of the use of this - // field see Annex E. - - // The Message Control Header shall be made of one byte with the least - // significant bit (bit 0) taking one of the following values: - // - If bit 0 is set to 1, the following fragment shall contain Message - // Command information. - // - If bit 0 is set to 0, the following fragment shall contain Message Data - // Set information. - // The next least significant bit (bit 1) shall be defined by the following - // rules: If bit 1 is set to 1, the following fragment shall contain the - // last fragment of a Message Data Set or of a Message Command. - // - If bit 1 is set to 0, the following fragment does not contain the last - // fragment of a Message Data Set or of a Message Command. - let mut message_header = 0x00; - if let PDataValueType::Command = presentation_data_value.value_type { - message_header |= 0x01; - } - if presentation_data_value.is_last { - message_header |= 0x02; - } - writer.push(message_header); - - // Message fragment - writer.extend(&presentation_data_value.data); - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Presentation-data-value item", - })?; - } - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "PData" }) - } - Pdu::ReleaseRQ => { - // 1 - PDU-type - 05H - writer - .write_u8(0x05) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - writer.extend([0u8; 4]); - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "ReleaseRQ" })?; - - Ok(()) - } - Pdu::ReleaseRP => { - // 1 - PDU-type - 06H - writer - .write_u8(0x06) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - writer.extend([0u8; 4]); - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "ReleaseRP" })?; - - Ok(()) - } - Pdu::AbortRQ { source } => { - // 1 - PDU-type - 07H - writer - .write_u8(0x07) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested - // to this value when received. - writer.push(0); - // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested - // to this value when received. - writer.push(0); - - // 9 - Source - This Source field shall contain an integer value encoded as an - // unsigned binary number. One of the following values shall be used: - // - 0 - DICOM UL service-user (initiated abort) - // - 1 - reserved - // - 2 - DICOM UL service-provider (initiated abort) - // 10 - Reason/Diag - This field shall contain an integer value encoded as an - // unsigned binary number. If the Source field has the value (2) "DICOM UL - // service-provider", it shall take one of the following: - // - 0 - reason-not-specified1 - unrecognized-PDU - // - 2 - unexpected-PDU - // - 3 - reserved - // - 4 - unrecognized-PDU parameter - // - 5 - unexpected-PDU parameter - // - 6 - invalid-PDU-parameter value - // If the Source field has the value (0) "DICOM UL service-user", this reason field - // shall not be significant. It shall be sent with a value 00H but not tested to - // this value when received. - let source_word = match source { - AbortRQSource::ServiceUser => [0x00; 2], - AbortRQSource::Reserved => [0x01, 0x00], - AbortRQSource::ServiceProvider(reason) => match reason { - AbortRQServiceProviderReason::ReasonNotSpecified => [0x02, 0x00], - AbortRQServiceProviderReason::UnrecognizedPdu => [0x02, 0x01], - AbortRQServiceProviderReason::UnexpectedPdu => [0x02, 0x02], - AbortRQServiceProviderReason::Reserved => [0x02, 0x03], - AbortRQServiceProviderReason::UnrecognizedPduParameter => [0x02, 0x04], - AbortRQServiceProviderReason::UnexpectedPduParameter => [0x02, 0x05], - AbortRQServiceProviderReason::InvalidPduParameter => [0x02, 0x06], - }, - }; - writer.extend(source_word); - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "AbortRQ" })?; - - Ok(()) - } - Pdu::Unknown { pdu_type, data } => { - // 1 - PDU-type - XXH - writer - .write_u8(*pdu_type) - .await - .context(WriteFieldSnafu { field: "PDU-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to - // this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u32(writer, || async { - let mut writer = vec![]; - writer.extend(data); - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "Unknown" })?; - - Ok(()) - } - } -} - -async fn write_pdu_variable_application_context_name( - writer: &mut W, - application_context_name: &str, - codec: &C, -) -> Result<()> -where - W: AsyncWrite + Unpin, - C: TextCodec + Send + Sync, -{ - // Application Context Item Structure - // 1 - Item-type - 10H - writer - .write_u8(0x10) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(writer, || async move { - // 5-xxx - Application-context-name -A valid Application-context-name shall - // be encoded as defined in Annex F. For a description of the use of this - // field see Section 7.1.1.2. Application-context-names are structured as - // UIDs as defined in PS3.5 (see Annex A for an overview of this concept). - // DICOM Application-context-names are registered in PS3.7. - let mut w = vec![]; - w.write_all( - &codec - .encode(application_context_name) - .context(EncodeFieldSnafu { - field: "Application-context-name", - })?, - ) - .await - .context(WriteFieldSnafu { - field: "Application-context-name", - }); - Ok(w) - }) - .await - .context(WriteChunkSnafu { - name: "Application Context Item", - })?; - - Ok(()) -} - -async fn write_pdu_variable_presentation_context_proposed( - writer: &mut W, - presentation_context: &PresentationContextProposed, - codec: &TC, -) -> Result<()> -where - W: AsyncWrite + Unpin, - TC: TextCodec + Send + Sync, -{ - // Presentation Context Item Structure - // 1 - tem-type - 20H - writer - .write_u8(0x20) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(writer, || async { - // 5 - Presentation-context-ID - Presentation-context-ID values shall be - // odd integers between 1 and 255, encoded as an unsigned binary number. - // For a complete description of the use of this field see Section - // 7.1.1.13. - let mut writer = vec![]; - writer - .write_u8(presentation_context.id) - .await - .context(WriteFieldSnafu { - field: "Presentation-context-ID", - })?; - - // 6 - Reserved - This reserved field shall be sent with a value 00H but - // not tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 7 - Reserved - This reserved field shall be sent with a value 00H but - // not tested to this value when received - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 8 - Reserved - This reserved field shall be sent with a value 00H but - // not tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field - // shall contain the following sub-items: one Abstract Syntax and one or - // more Transfer Syntax(es). For a complete description of the use and - // encoding of these sub-items see Section 9.3.2.2.1 and Section - // 9.3.2.2.2. - - // Abstract Syntax Sub-Item Structure - // 1 - Item-type 30H - writer - .write_u8(0x30) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H - // but not tested to this value when - // received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5-xxx - Abstract-syntax-name - This variable field shall - // contain - // the Abstract-syntax-name related to the proposed presentation - // context. A valid Abstract-syntax-name shall be encoded as - // defined in Annex F. For a - // description of the use of this field see - // Section 7.1.1.13. Abstract-syntax-names are structured as - // UIDs as defined in PS3.5 - // (see Annex B for an overview of this concept). - // DICOM Abstract-syntax-names are registered in PS3.4. - writer - .write_all( - &codec - .encode(&presentation_context.abstract_syntax) - .context(EncodeFieldSnafu { - field: "Abstract-syntax-name", - })?, - ) - .await - .context(WriteFieldSnafu { - field: "Abstract-syntax-name", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Abstract Syntax Item", - })?; - - for transfer_syntax in &presentation_context.transfer_syntaxes { - // Transfer Syntax Sub-Item Structure - // 1 - Item-type - 40H - writer.write_u8(0x40).await.context(WriteFieldSnafu { - field: "Presentation-context Item-type", - })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H - // but not tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5-xxx - Transfer-syntax-name(s) - This variable field shall - // contain the Transfer-syntax-name proposed for this - // presentation context. A valid Transfer-syntax-name shall be - // encoded as defined in Annex F. For a description of the use - // of this field see Section 7.1.1.13. Transfer-syntax-names are - // structured as UIDs as defined in PS3.5 (see Annex B for an - // overview of this concept). DICOM Transfer-syntax-names are - // registered in PS3.5. - writer - .write_all(&codec.encode(transfer_syntax).context(EncodeFieldSnafu { - field: "Transfer-syntax-name", - })?) - .await - .context(WriteFieldSnafu { - field: "Transfer-syntax-name", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Transfer Syntax Sub-Item", - })?; - } - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Presentation Context Item", - })?; - - Ok(()) -} - -async fn write_pdu_variable_presentation_context_result( - writer: &mut W, - presentation_context: &PresentationContextResult, - codec: &TC, -) -> Result<()> -where - W: AsyncWrite + Unpin, - TC: TextCodec + Send + Sync, -{ - // 1 - Item-type - 21H - writer - .write_u8(0x21) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this - // value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(writer, || async { - let mut writer = vec![]; - // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers - // between 1 and 255, encoded as an unsigned binary number. For a complete description of - // the use of this field see Section 7.1.1.13. - writer - .write_u8(presentation_context.id) - .await - .context(WriteFieldSnafu { - field: "Presentation-context-ID", - })?; - - // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to this - // value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded as an - // unsigned binary number. One of the following values shall be used: - // 0 - acceptance - // 1 - user-rejection - // 2 - no-reason (provider rejection) - // 3 - abstract-syntax-not-supported (provider rejection) - // 4 - transfer-syntaxes-not-supported (provider rejection) - writer - .write_u8(match &presentation_context.reason { - PresentationContextResultReason::Acceptance => 0, - PresentationContextResultReason::UserRejection => 1, - PresentationContextResultReason::NoReason => 2, - PresentationContextResultReason::AbstractSyntaxNotSupported => 3, - PresentationContextResultReason::TransferSyntaxesNotSupported => 4, - }) - .await - .context(WriteFieldSnafu { - field: "Presentation Context Result/Reason", - })?; - - // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to this - // value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer Syntax - // Sub-Item. When the Result/Reason field has a value other than acceptance (0), this field - // shall not be significant and its value shall not be tested when received. For a complete - // description of the use and encoding of this item see Section 9.3.3.2.1. - - // 1 - Item-type - 40H - writer - .write_u8(0x40) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this - // value when received. - writer - .write_u8(0x40) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5-xxx - Transfer-syntax-name - This variable field shall contain the - // Transfer-syntax-name proposed for this presentation context. A valid - // Transfer-syntax-name shall be encoded as defined in Annex F. For a description of the - // use of this field see Section 7.1.1.14. Transfer-syntax-names are structured as UIDs - // as defined in PS3.5 (see Annex B for an overview of this concept). DICOM - // Transfer-syntax-names are registered in PS3.5. - writer - .write_all( - &codec - .encode(&presentation_context.transfer_syntax) - .context(EncodeFieldSnafu { - field: "Transfer-syntax-name", - })?, - ) - .await - .context(WriteFieldSnafu { - field: "Transfer-syntax-name", - })?; - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Transfer Syntax sub-item", - })?; - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Presentation-context", - }) -} - -async fn write_pdu_variable_user_variables( - writer: &mut W, - user_variables: &[UserVariableItem], - codec: &TC, -) -> Result<()> -where - W: AsyncWrite + Unpin, - TC: TextCodec + Send + Sync, -{ - if user_variables.is_empty() { - return Ok(()); - } - - // 1 - Item-type - 50H - writer - .write_u8(0x50) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this - // value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(writer, || async { - let mut writer = vec![]; - // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined by - // the DICOM Application Entity. The structure and content of these sub-items is defined in - // Annex D. - for user_variable in user_variables { - match user_variable { - UserVariableItem::MaxLength(max_length) => { - // 1 - Item-type - 51H - writer - .write_u8(0x51) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5-8 - Maximum-length-received - This parameter allows the - // association-requestor to restrict the maximum length of the variable - // field of the P-DATA-TF PDUs sent by the acceptor on the association once - // established. This length value is indicated as a number of bytes encoded - // as an unsigned binary number. The value of (0) indicates that no maximum - // length is specified. This maximum length value shall never be exceeded by - // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs - // received by the association-requestor. Otherwise, it shall be a protocol - // error. - writer - .write_u32(*max_length) - .await - .context(WriteFieldSnafu { - field: "Maximum-length-received", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Maximum-length-received", - })?; - } - UserVariableItem::ImplementationVersionName(implementation_version_name) => { - // 1 - Item-type - 55H - writer - .write_u8(0x55) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5 - xxx - Implementation-version-name - This variable field shall contain - // the Implementation-version-name of the Association-acceptor as defined in - // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990 - // (basic G0 set) characters. - writer - .write_all(&codec.encode(implementation_version_name).context( - EncodeFieldSnafu { - field: "Implementation-version-name", - }, - )?) - .await - .context(WriteFieldSnafu { - field: "Implementation-version-name", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Implementation-version-name", - })?; - } - UserVariableItem::ImplementationClassUID(implementation_class_uid) => { - // 1 - Item-type - 52H - writer - .write_u8(0x52) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - //5 - xxx - Implementation-class-uid - This variable field shall contain - // the Implementation-class-uid of the Association-acceptor as defined in - // Section D.3.3.2. The Implementation-class-uid field is structured as a - // UID as defined in PS3.5. - writer - .write_all(&codec.encode(implementation_class_uid).context( - EncodeFieldSnafu { - field: "Implementation-class-uid", - }, - )?) - .await - .context(WriteFieldSnafu { - field: "Implementation-class-uid", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Implementation-class-uid", - })?; - } - UserVariableItem::SopClassExtendedNegotiationSubItem(sop_class_uid, data) => { - // 1 - Item-type - 56H - writer - .write_u8(0x56) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 7-xxx - The SOP Class or Meta SOP Class identifier encoded as a UID - // as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5. - writer - .write_all(&codec.encode(sop_class_uid).context( - EncodeFieldSnafu { - field: "SOP-class-uid", - }, - )?) - .await - .context(WriteFieldSnafu { - field: "SOP-class-uid", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "SOP-class-uid", - })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // xxx-xxx Service-class-application-information - This field shall contain - // the application information specific to the Service Class specification - // identified by the SOP-class-uid. The semantics and value of this field is - // defined in the identified Service Class specification. - writer.write_all(data).await.context(WriteFieldSnafu { - field: "Service-class-application-information", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Service-class-application-information", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "Sub-item" })?; - } - UserVariableItem::UserIdentityItem(user_identity) => { - // 1 - Item-type - 58H - writer - .write_u8(0x58) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - // 2 - Reserved - This reserved field shall be sent with a value 00H but not - // tested to this value when received. - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - // 3-4 - Item-length - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 5 - User-Identity-Type - writer - .write_u8(user_identity.identity_type().to_u8()) - .await - .context(WriteFieldSnafu { - field: "User-Identity-Type", - })?; - - // 6 - Positive-response-requested - let positive_response_requested_out: u8 = - if user_identity.positive_response_requested() { - 1 - } else { - 0 - }; - writer - .write_u8(positive_response_requested_out) - .await - .context(WriteFieldSnafu { - field: "Positive-response-requested", - })?; - - // 7-8 - Primary-field-length - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // 9-n - Primary-field - writer - .write_all(user_identity.primary_field().as_slice()) - .await - .context(WriteFieldSnafu { - field: "Primary-field", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Primary-field", - })?; - - // n+1-n+2 - Secondary-field-length - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - // n+3-m - Secondary-field - writer - .write_all(user_identity.secondary_field().as_slice()) - .await - .context(WriteFieldSnafu { - field: "Secondary-field", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Secondary-field", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { - name: "Item-length", - })?; - } - UserVariableItem::Unknown(item_type, data) => { - writer - .write_u8(*item_type) - .await - .context(WriteFieldSnafu { field: "Item-type" })?; - - writer - .write_u8(0x00) - .await - .context(WriteReservedSnafu { bytes: 1_u32 })?; - - write_chunk_u16(&mut writer, || async { - let mut writer = vec![]; - writer.write_all(data).await.context(WriteFieldSnafu { - field: "Unknown Data", - })?; - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "Unknown" })?; - } - } - } - - Ok(writer) - }) - .await - .context(WriteChunkSnafu { name: "User-data" }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_write_chunks_with_preceding_u32_length() -> Result<()> { - let mut bytes = vec![0u8; 0]; - write_chunk_u32(&mut bytes, |writer| { - writer - .write_u8(0x02) - .context(WriteFieldSnafu { field: "Field1" })?; - write_chunk_u32(writer, |writer| { - writer - .write_u8(0x03) - .context(WriteFieldSnafu { field: "Field2" })?; - Ok(()) - }) - .context(WriteChunkSnafu { name: "Chunk2" }) - }) - .context(WriteChunkSnafu { name: "Chunk1" })?; - - assert_eq!(bytes.len(), 10); - assert_eq!(bytes, &[0, 0, 0, 6, 2, 0, 0, 0, 1, 3]); - - Ok(()) - } - - #[test] - fn can_write_chunks_with_preceding_u16_length() -> Result<()> { - let mut bytes = vec![0u8; 0]; - write_chunk_u16(&mut bytes, |writer| { - writer - .write_u8(0x02) - .context(WriteFieldSnafu { field: "Field1" })?; - write_chunk_u16(writer, |writer| { - writer - .write_u8(0x03) - .context(WriteFieldSnafu { field: "Field2" })?; - Ok(()) - }) - .context(WriteChunkSnafu { name: "Chunk2" }) - }) - .context(WriteChunkSnafu { name: "Chunk1" })?; - - assert_eq!(bytes.len(), 6); - assert_eq!(bytes, &[0, 4, 2, 0, 1, 3]); - - Ok(()) - } - - #[test] - fn write_abort_rq() { - let mut out = vec![]; - - // abort by request of SCU - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }; - write_pdu(&mut out, &pdu).unwrap(); - assert_eq!( - &out, - &[ - // code 7 + reserved byte - 0x07, 0x00, // - // PDU length: 4 bytes - 0x00, 0x00, 0x00, 0x04, // - // reserved 2 bytes + source: service user (0) + reason (0) - 0x00, 0x00, 0x00, 0x00, - ] - ); - out.clear(); - - // Reserved - let pdu = Pdu::AbortRQ { - source: AbortRQSource::Reserved, - }; - write_pdu(&mut out, &pdu).unwrap(); - assert_eq!( - &out, - &[ - // code 7 + reserved byte - 0x07, 0x00, // - // PDU length: 4 bytes - 0x00, 0x00, 0x00, 0x04, // - // reserved 2 bytes + source: reserved (1) + reason (0) - 0x00, 0x00, 0x01, 0x00, - ] - ); - out.clear(); - - // abort by request of SCP - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceProvider( - AbortRQServiceProviderReason::InvalidPduParameter, - ), - }; - write_pdu(&mut out, &pdu).unwrap(); - assert_eq!( - &out, - &[ - // code 7 + reserved byte - 0x07, 0x00, // - // PDU length: 4 bytes - 0x00, 0x00, 0x00, 0x04, // - // reserved 2 bytes - 0x00, 0x00, // - // source: service provider (2), invalid parameter value (6) - 0x02, 0x06, - ] - ); - } -} From b8d82002cbadf8603320bc2166c320b9e83a330c Mon Sep 17 00:00:00 2001 From: Nathan Richman Date: Mon, 5 Aug 2024 09:44:11 -0500 Subject: [PATCH 03/28] MAIN: Implement PDataReader and PDataWriter with async --- Cargo.lock | 1 + Cargo.toml | 4 - storescp/Cargo.toml | 2 +- ul/Cargo.toml | 5 +- ul/src/address.rs | 1 - ul/src/association/client.rs | 100 ++++++++---- ul/src/association/mod.rs | 4 +- ul/src/association/pdata.rs | 306 +++++++++++++++++++++++++++++++---- ul/src/association/server.rs | 78 ++++++--- ul/src/pdu/reader.rs | 2 +- 10 files changed, 404 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2533248d4..3b7882966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,7 @@ name = "dicom-ul" version = "0.7.0" dependencies = [ "byteordered", + "bytes", "dicom-encoding", "dicom-transfer-syntax-registry", "matches", diff --git a/Cargo.toml b/Cargo.toml index 153aecf5e..fa7291330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,3 @@ opt-level = 2 # optimize JPEG 2000 decoder to run tests faster [profile.dev.package.jpeg2k] opt-level = 2 - -# optimize flate2 to run tests faster -[profile.dev.package."flate2"] -opt-level = 2 diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index ecbba55e0..57bbcf187 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] clap = { version = "4.0.18", features = ["derive"] } dicom-core = { path = '../core', version = "0.7.0" } -dicom-ul = { path = '../ul', version = "0.7.0", features = ["tokio"] } +dicom-ul = { path = '../ul', version = "0.7.0", features = [] } dicom-object = { path = '../object', version = "0.7.0" } dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-dictionary-std = { path = "../dictionary-std/", version = "0.7.0" } diff --git a/ul/Cargo.toml b/ul/Cargo.toml index b6cf54e19..3ba406526 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -23,5 +23,6 @@ tracing = "0.1.34" matches = "0.1.8" [features] -tokio = ["dep:tokio"] -default = ["tokio"] +async = ["dep:tokio"] +#default = ["tokio"] +default = ["async"] diff --git a/ul/src/address.rs b/ul/src/address.rs index 5251e47cf..8e4cb5f03 100644 --- a/ul/src/address.rs +++ b/ul/src/address.rs @@ -7,7 +7,6 @@ //! The syntax is `«ae_title»@«network_address»:«port»`, //! which works not only with IPv4 and IPv6 addresses, //! but also with domain names. -#[cfg(feature = "tokio")] use snafu::{ensure, AsErrorSource, ResultExt, Snafu}; use std::{ convert::TryFrom, diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 49f98b978..185d8a202 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -5,13 +5,13 @@ //! See [`ClientAssociationOptions`] //! for details and examples on how to create an association. use std::{borrow::Cow, convert::TryInto, net::ToSocketAddrs, time::Duration}; -#[cfg(not(feature = "tokio"))] +#[cfg(not(feature = "async"))] use std::{ - io::Write, - net::{TcpStream, ToSocketAddrs}, + io::{Read, Write}, + net::TcpStream, }; use bytes::BytesMut; -#[cfg(feature = "tokio")] +#[cfg(feature = "async")] use tokio::{ io::{AsyncRead, AsyncWriteExt}, net::TcpStream, @@ -26,6 +26,9 @@ use crate::{ AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; use snafu::{ensure, Backtrace, ResultExt, Snafu}; +use std::io::Cursor; + +use bytes::Buf; use super::{ //pdata::{PDataReader, PDataWriter}, @@ -417,7 +420,7 @@ impl<'a> ClientAssociationOptions<'a> { self } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -425,7 +428,7 @@ impl<'a> ClientAssociationOptions<'a> { self.establish_impl(AeAddr::new_socket_addr(address)) } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -433,7 +436,7 @@ impl<'a> ClientAssociationOptions<'a> { self.establish_impl(AeAddr::new_socket_addr(address)).await } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -464,7 +467,7 @@ impl<'a> ClientAssociationOptions<'a> { } } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -506,7 +509,7 @@ impl<'a> ClientAssociationOptions<'a> { } } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] fn establish_impl(self, ae_address: AeAddr) -> Result where T: ToSocketAddrs, @@ -524,8 +527,7 @@ impl<'a> ClientAssociationOptions<'a> { kerberos_service_ticket, saml_assertion, jwt, - read_timeout, - write_timeout, + timeout, } = self; // fail if no presentation contexts were provided: they represent intent, @@ -590,10 +592,10 @@ impl<'a> ClientAssociationOptions<'a> { let mut socket = std::net::TcpStream::connect(ae_address).context(ConnectSnafu)?; socket - .set_read_timeout(read_timeout) + .set_read_timeout(timeout.clone()) .context(SetReadTimeoutSnafu)?; socket - .set_write_timeout(write_timeout) + .set_write_timeout(timeout) .context(SetWriteTimeoutSnafu)?; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); // send request @@ -601,9 +603,25 @@ impl<'a> ClientAssociationOptions<'a> { write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; socket.write_all(&buffer).context(WireSendSnafu)?; buffer.clear(); + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); // receive response - let msg = - read_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)?; + let msg = loop{ + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = socket.read(&mut read_buffer).unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + }; match msg { Pdu::AssociationAC(AssociationAC { @@ -660,6 +678,8 @@ impl<'a> ClientAssociationOptions<'a> { socket, buffer, strict, + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), + timeout }) } Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), @@ -692,7 +712,7 @@ impl<'a> ClientAssociationOptions<'a> { } } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] async fn establish_impl(self, ae_address: AeAddr) -> Result where T: ToSocketAddrs, @@ -1000,7 +1020,7 @@ impl ClientAssociation { self.requestor_max_pdu_length } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -1014,7 +1034,7 @@ impl ClientAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -1031,12 +1051,29 @@ impl ClientAssociation { .context(WireSendSnafu) } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Read a PDU message from the other intervenient. pub fn receive(&mut self) -> Result { - read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict).context(ReceiveSnafu) + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu) + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self.socket.read(&mut self.read_buffer).unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + + } } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Read a PDU message from the other intervenient. pub async fn receive(&mut self) -> Result { use std::io::Cursor; @@ -1046,7 +1083,7 @@ impl ClientAssociation { loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); return Ok(pdu) @@ -1063,7 +1100,7 @@ impl ClientAssociation { } } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Gracefully terminate the association by exchanging release messages /// and then shutting down the TCP connection. pub fn release(mut self) -> Result<()> { @@ -1072,7 +1109,7 @@ impl ClientAssociation { out } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Gracefully terminate the association by exchanging release messages /// and then shutting down the TCP connection. pub async fn release(mut self) -> Result<()> { @@ -1081,7 +1118,7 @@ impl ClientAssociation { out } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Send an abort message and shut down the TCP connection, /// terminating the association. pub fn abort(mut self) -> Result<()> { @@ -1093,7 +1130,7 @@ impl ClientAssociation { out } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Send an abort message and shut down the TCP connection, /// terminating the association. pub async fn abort(mut self) -> Result<()> { @@ -1140,7 +1177,7 @@ impl ClientAssociation { // PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) // } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Release implementation function, /// which tries to send a release request and receive a release response. /// This is in a separate private function because @@ -1149,8 +1186,7 @@ impl ClientAssociation { fn release_impl(&mut self) -> Result<()> { let pdu = Pdu::ReleaseRQ; self.send(&pdu)?; - let pdu = read_pdu(&mut self.socket, self.requestor_max_pdu_length, self.strict) - .context(ReceiveSnafu)?; + let pdu = self.receive()?; match pdu { Pdu::ReleaseRP => {} @@ -1165,7 +1201,7 @@ impl ClientAssociation { Ok(()) } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Release implementation function, /// which tries to send a release request and receive a release response. /// This is in a separate private function because @@ -1199,7 +1235,7 @@ impl ClientAssociation { } } -#[cfg(not(feature = "tokio"))] +#[cfg(not(feature = "async"))] /// Automatically release the association and shut down the connection. impl Drop for ClientAssociation { fn drop(&mut self) { @@ -1208,7 +1244,7 @@ impl Drop for ClientAssociation { } } -#[cfg(feature = "tokio")] +#[cfg(feature = "async")] /// Automatically release the association and shut down the connection. impl Drop for ClientAssociation { fn drop(&mut self) { diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index 7c7025c9b..ba1cead91 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -19,8 +19,8 @@ pub mod client; pub mod server; mod uid; -//pub(crate) mod pdata; +pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; -//pub use pdata::{PDataReader, PDataWriter}; +pub use pdata::{PDataReader, PDataWriter}; pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index f3e0f2a5f..5ad6489fd 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,12 +1,43 @@ use std::{ - collections::VecDeque, - io::{Read, Write}, + collections::VecDeque, fmt::Result, io::{Cursor, Read, Write}, pin::Pin, task::{Context, Poll} }; +use bytes::{Buf, BytesMut}; +use tokio::io::ReadBuf; use tracing::warn; use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; +#[cfg(feature = "async")] +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; + +/// Set up the P-Data PDU header for sending. +fn setup_pdata_header(buffer: &mut Vec, is_last: bool) { + let data_len = (buffer.len() - 12) as u32; + + // full PDU length (minus PDU type and reserved byte) + let pdu_len = data_len + 4 + 2; + let pdu_len_bytes = pdu_len.to_be_bytes(); + + buffer[2] = pdu_len_bytes[0]; + buffer[3] = pdu_len_bytes[1]; + buffer[4] = pdu_len_bytes[2]; + buffer[5] = pdu_len_bytes[3]; + + // presentation data length (data + 2 properties below) + let pdv_data_len = data_len + 2; + let data_len_bytes = pdv_data_len.to_be_bytes(); + + buffer[6] = data_len_bytes[0]; + buffer[7] = data_len_bytes[1]; + buffer[8] = data_len_bytes[2]; + buffer[9] = data_len_bytes[3]; + + // message control header + buffer[11] = if is_last { 0x02 } else { 0x00 }; +} + + /// A P-Data value writer. /// /// This exposes an API to iteratively construct and send Data messages @@ -104,36 +135,10 @@ where Ok(()) } - /// Set up the P-Data PDU header for sending. - fn setup_pdata_header(&mut self, is_last: bool) { - let data_len = (self.buffer.len() - 12) as u32; - - // full PDU length (minus PDU type and reserved byte) - let pdu_len = data_len + 4 + 2; - let pdu_len_bytes = pdu_len.to_be_bytes(); - - self.buffer[2] = pdu_len_bytes[0]; - self.buffer[3] = pdu_len_bytes[1]; - self.buffer[4] = pdu_len_bytes[2]; - self.buffer[5] = pdu_len_bytes[3]; - - // presentation data length (data + 2 properties below) - let pdv_data_len = data_len + 2; - let data_len_bytes = pdv_data_len.to_be_bytes(); - - self.buffer[6] = data_len_bytes[0]; - self.buffer[7] = data_len_bytes[1]; - self.buffer[8] = data_len_bytes[2]; - self.buffer[9] = data_len_bytes[3]; - - // message control header - self.buffer[11] = if is_last { 0x02 } else { 0x00 }; - } - fn finish_impl(&mut self) -> std::io::Result<()> { if !self.buffer.is_empty() { // send last PDU - self.setup_pdata_header(true); + setup_pdata_header(&mut self.buffer, true); self.stream.write_all(&self.buffer[..])?; // clear buffer so that subsequent calls to `finish_impl` // do not send any more PDUs @@ -149,7 +154,7 @@ where fn dispatch_pdu(&mut self) -> std::io::Result<()> { debug_assert!(self.buffer.len() >= 12); // send PDU now - self.setup_pdata_header(false); + setup_pdata_header(&mut self.buffer, false); self.stream.write_all(&self.buffer)?; // back to just the header @@ -199,6 +204,186 @@ where } } +/// A P-Data async value writer. +/// +/// This exposes an API to iteratively construct and send Data messages +/// to another node. +/// Using this as a [standard writer](std::io::Write) +/// will automatically split the incoming bytes +/// into separate PDUs if they do not fit in a single one. +/// +/// # Example +/// +/// Use an association's `send_pdata` method +/// to create a new P-Data value writer. +/// +/// ```no_run +/// # use std::io::Write; +/// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; +/// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; +/// # fn command_data() -> Vec { unimplemented!() } +/// # fn dicom_data() -> &'static [u8] { unimplemented!() } +/// # fn main() -> Result<(), Box> { +/// let mut association = ClientAssociationOptions::new() +/// .establish("129.168.0.5:104")?; +/// +/// let presentation_context_id = association.presentation_contexts()[0].id; +/// +/// // send a command first +/// association.send(&Pdu::PData { +/// data: vec![PDataValue { +/// presentation_context_id, +/// value_type: PDataValueType::Command, +/// is_last: true, +/// data: command_data(), +/// }], +/// }); +/// +/// // then send a DICOM object which may be split into multiple PDUs +/// let mut pdata = association.send_pdata(presentation_context_id); +/// pdata.write_all(dicom_data())?; +/// pdata.finish()?; +/// +/// let pdu_ac = association.receive()?; +/// # Ok(()) +/// # } +#[cfg(feature = "async")] +#[must_use] +pub struct AsyncPDataWriter { + buffer: Vec, + stream: W, + max_data_len: u32, +} + +#[cfg(feature = "async")] +impl AsyncPDataWriter +where + W: AsyncWrite + Unpin, +{ + /// Construct a new P-Data value writer. + /// + /// `max_pdu_length` is the maximum value of the PDU-length property. + pub(crate) fn new(stream: W, presentation_context_id: u8, max_pdu_length: u32) -> Self { + let max_data_length = calculate_max_data_len_single(max_pdu_length); + let mut buffer = Vec::with_capacity((max_data_length + PDU_HEADER_SIZE) as usize); + // initial buffer set up + buffer.extend([ + // PDU-type + reserved byte + 0x04, + 0x00, + // full PDU length, unknown at this point + 0xFF, + 0xFF, + 0xFF, + 0xFF, + // presentation data length, unknown at this point + 0xFF, + 0xFF, + 0xFF, + 0xFF, + // presentation context id + presentation_context_id, + // message control header, unknown at this point + 0xFF, + ]); + + PDataWriter { + stream, + max_data_len: max_data_length, + buffer, + } + } + + /// Declare to have finished sending P-Data fragments, + /// thus emitting the last P-Data fragment PDU. + /// + /// This is also done automatically once the P-Data writer is dropped. + pub async fn finish(mut self) -> std::io::Result<()> { + self.finish_impl().await?; + Ok(()) + } + + async fn finish_impl(&mut self) -> std::io::Result<()> { + if !self.buffer.is_empty() { + // send last PDU + setup_pdata_header(&mut self.buffer, true); + self.stream.write_all(&self.buffer[..]).await?; + // clear buffer so that subsequent calls to `finish_impl` + // do not send any more PDUs + self.buffer.clear(); + } + Ok(()) + } +} + +#[cfg(feature = "async")] +impl AsyncWrite for AsyncPDataWriter +where + W: AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll>{ + let total_len = self.max_data_len as usize + 12; + if self.buffer.len() + buf.len() <= total_len { + // accumulate into buffer, do nothing + self.buffer.extend(buf); + Poll::Ready(Ok(buf.len())) + } else { + // fill in the rest of the buffer, send PDU, + // and leave out the rest for subsequent writes + let buf = &buf[..total_len - self.buffer.len()]; + self.buffer.extend(buf); + debug_assert_eq!(self.buffer.len(), total_len); + setup_pdata_header(&mut self.buffer, false); + let res = Pin::new(&mut self.stream).poll_write(cx, &self.buffer); + match res { + Poll::Ready(Ok(_)) => { + self.buffer.truncate(12); + Poll::Ready(Ok(buf.len())) + }, + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending + } + } + + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>{ + Pin::new(&mut self.stream).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>{ + Pin::new(&mut self.stream).poll_shutdown(cx) + } +} + +/// With the P-Data writer dropped, +/// this `Drop` implementation +/// will construct and emit the last P-Data fragment PDU +/// if there is any data left to send. +#[cfg(feature = "async")] +impl Drop for AsyncPDataWriter +where + W: AsyncWrite + Unpin, +{ + fn drop(&mut self) { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + let _ = self.finish_impl().await; + }) + }) + } +} + /// A P-Data value reader. /// /// This exposes an API which provides a byte stream of data @@ -243,7 +428,6 @@ pub struct PDataReader { impl PDataReader where - R: Read, { pub fn new(stream: R, max_data_length: u32) -> Self { PDataReader { @@ -277,10 +461,29 @@ where return Ok(0); } - let pdu = read_pdu(&mut self.stream, self.max_data_length, false) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); + let msg = loop{ + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, self.max_data_length, false) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self.stream.read(&mut read_buffer).unwrap(); + if recv == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, "Connection closed by peer" + )); + } + }; - match pdu { + match msg { Pdu::PData { data } => { for pdata_value in data { self.presentation_context_id = match self.presentation_context_id { @@ -307,6 +510,41 @@ where } } + +#[cfg(feature = "async")] +impl AsyncRead for PDataReader where R: AsyncRead + Unpin { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll>{ + let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); + let msg = loop{ + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, self.max_data_length, false) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } + } + match Pin::new(&mut self.stream).poll_read(cx, &mut read_buffer){ + Poll::Pending => return Poll::Pending, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Ready(Ok(0)) => return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::Other, "Connection closed by peer" + ))), + Poll::Ready(Ok(_)) => {} + } + } + Poll::Ready(Ok(())) + } + +} /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 5242d6248..2739cff26 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -4,12 +4,15 @@ //! in which this application entity listens to incoming association requests. //! See [`ServerAssociationOptions`] //! for details and examples on how to create an association. -use std::borrow::Cow; -#[cfg(not(feature = "tokio"))] -use std::{io::Write, net::TcpStream}; -#[cfg(feature = "tokio")] +use std::{borrow::Cow, io::Cursor}; +#[cfg(not(feature = "async"))] +use std::{io::{Write, Read}, net::TcpStream}; +#[cfg(feature = "async")] use tokio::{io::AsyncWriteExt, net::TcpStream}; +use bytes::{Buf, BytesMut}; + + use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use snafu::{ensure, Backtrace, ResultExt, Snafu}; @@ -25,7 +28,7 @@ use crate::{ }; use super::{ - //pdata::{PDataReader, PDataWriter}, + pdata::{PDataReader, PDataWriter}, uid::trim_uid, }; @@ -358,9 +361,10 @@ where self } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Negotiate an association with the given TCP stream. pub fn establish(&self, mut socket: TcpStream) -> Result { + ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, MissingAbstractSyntaxSnafu @@ -368,10 +372,26 @@ where let max_pdu_length = self.max_pdu_length; - let pdu = - read_pdu(&mut socket, max_pdu_length, self.strict).context(ReceiveRequestSnafu)?; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + let msg = loop{ + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = socket.read(&mut read_buffer).unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + }; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); - match pdu { + match msg { Pdu::AssociationRQ(AssociationRQ { protocol_version, calling_ae_title, @@ -517,6 +537,7 @@ where client_ae_title: calling_ae_title, buffer, strict: self.strict, + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), }) } Pdu::ReleaseRQ => { @@ -533,7 +554,7 @@ where } } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Negotiate an association with the given TCP stream. pub async fn establish(&self, mut socket: TcpStream) -> Result { ensure!( @@ -793,7 +814,7 @@ impl ServerAssociation { &self.client_ae_title } - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -807,7 +828,7 @@ impl ServerAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -825,16 +846,29 @@ impl ServerAssociation { } /// Read a PDU message from the other intervenient. - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] pub fn receive(&mut self) -> Result { - match read_pdu(&mut self.socket, self.acceptor_max_pdu_length, self.strict).context(ReceiveSnafu){ - Ok(Some(pdu)) => Ok(pdu), - Ok(None) => self.receive(), - Err(e) => Err(e) + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu) + }, + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self.socket.read(&mut self.read_buffer).unwrap(); + if recv == 0 { + return OtherSnafu{msg: "Connection closed by peer"}.fail(); + } + } } - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] /// Read a PDU message from the other intervenient. pub async fn receive_async(&mut self) -> Result { use std::io::Cursor; @@ -864,7 +898,7 @@ impl ServerAssociation { /// Send a provider initiated abort message /// and shut down the TCP connection, /// terminating the association. - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] pub fn abort(mut self) -> Result<()> { let pdu = Pdu::AbortRQ { source: AbortRQSource::ServiceProvider( @@ -879,7 +913,7 @@ impl ServerAssociation { /// Send a provider initiated abort message /// and shut down the TCP connection, /// terminating the association. - #[cfg(feature = "tokio")] + #[cfg(feature = "async")] pub async fn abort(mut self) -> Result<()> { let pdu = Pdu::AbortRQ { source: AbortRQSource::ServiceProvider( @@ -896,7 +930,7 @@ impl ServerAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - #[cfg(not(feature = "tokio"))] + #[cfg(not(feature = "async"))] pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { PDataWriter::new( &mut self.socket, @@ -910,7 +944,7 @@ impl ServerAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - // #[cfg(feature = "tokio")] + // #[cfg(feature = "async")] // pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { // PDataWriter::new( // &mut self.socket, diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index dad8fa769..3dcdcf23c 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -360,7 +360,7 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. if bytes.remaining() < 2 { return Ok(None) } - let mut buf = bytes.copy_to_bytes(2); + let _ = bytes.copy_to_bytes(2); // 9 - Source - This Source field shall contain an integer value encoded as an unsigned // binary number. One of the following values shall be used: From 1f79816dcb57781c0666e07bd3a3619f4c8497bd Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Tue, 6 Aug 2024 09:59:22 -0500 Subject: [PATCH 04/28] MAIN: Finish implementing framing for reading --- ul/Cargo.toml | 3 +- ul/src/association/client.rs | 82 ++++++++++----- ul/src/association/mod.rs | 1 + ul/src/association/pdata.rs | 197 ++++++++++++++++++----------------- ul/src/association/server.rs | 61 +++++++---- ul/src/pdu/mod.rs | 2 +- ul/src/pdu/reader.rs | 2 +- ul/tests/pdu.rs | 13 ++- 8 files changed, 212 insertions(+), 149 deletions(-) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index 3ba406526..d4fa30a28 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -24,5 +24,4 @@ matches = "0.1.8" [features] async = ["dep:tokio"] -#default = ["tokio"] -default = ["async"] +default = [] diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 185d8a202..a7a0b7112 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -4,7 +4,7 @@ //! in which this application entity is the one requesting the association. //! See [`ClientAssociationOptions`] //! for details and examples on how to create an association. -use std::{borrow::Cow, convert::TryInto, net::ToSocketAddrs, time::Duration}; +use std::{borrow::Cow, io::Cursor, convert::TryInto, net::ToSocketAddrs, time::Duration}; #[cfg(not(feature = "async"))] use std::{ io::{Read, Write}, @@ -26,13 +26,12 @@ use crate::{ AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; use snafu::{ensure, Backtrace, ResultExt, Snafu}; -use std::io::Cursor; use bytes::Buf; use super::{ //pdata::{PDataReader, PDataWriter}, - uid::trim_uid, + uid::trim_uid, PDataReader, PDataWriter, }; #[derive(Debug, Snafu)] @@ -514,6 +513,10 @@ impl<'a> ClientAssociationOptions<'a> { where T: ToSocketAddrs, { + use std::io::{BufRead, BufReader}; + + use crate::pdu::ReadPduSnafu; + let ClientAssociationOptions { calling_ae_title, called_ae_title, @@ -603,9 +606,12 @@ impl<'a> ClientAssociationOptions<'a> { write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; socket.write_all(&buffer).context(WireSendSnafu)?; buffer.clear(); + + // Receive response let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - // receive response - let msg = loop{ + let mut reader = BufReader::new(&mut socket); + + let msg = loop { let mut buf = Cursor::new(&read_buffer[..]); match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { Some(pdu) => { @@ -617,8 +623,11 @@ impl<'a> ClientAssociationOptions<'a> { buf.set_position(0) } } - let recv = socket.read(&mut read_buffer).unwrap(); - if recv == 0 { + // Use BufReader to get similar behavior to AsyncRead read_buf + let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + reader.consume(recv.len()); + read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } }; @@ -809,10 +818,19 @@ impl<'a> ClientAssociationOptions<'a> { let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); let msg = loop { - if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { - break pdu + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } } - if 0 == socket.read_buf(&mut read_buffer).await.unwrap() { + let recv = socket.read_buf(&mut read_buffer).await.unwrap(); + if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } }; @@ -1054,9 +1072,14 @@ impl ClientAssociation { #[cfg(not(feature = "async"))] /// Read a PDU message from the other intervenient. pub fn receive(&mut self) -> Result { + use std::io::{BufRead, BufReader, Cursor}; + + use crate::pdu::ReadPduSnafu; + let mut reader = BufReader::new(&mut self.socket); + loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { + match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); return Ok(pdu) @@ -1066,8 +1089,11 @@ impl ClientAssociation { buf.set_position(0) } } - let recv = self.socket.read(&mut self.read_buffer).unwrap(); - if recv == 0 { + // Use BufReader to get similar behavior to AsyncRead read_buf + let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + reader.consume(recv.len()); + self.read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } @@ -1160,22 +1186,22 @@ impl ClientAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - // pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - // PDataWriter::new( - // &mut self.socket, - // presentation_context_id, - // self.acceptor_max_pdu_length, - // ) - // } + pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + PDataWriter::new( + &mut self.socket, + presentation_context_id, + self.acceptor_max_pdu_length, + ) + } /// 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) -> PDataReader<&mut TcpStream> { - // PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) - // } + pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) + } #[cfg(not(feature = "async"))] /// Release implementation function, @@ -1248,9 +1274,11 @@ impl Drop for ClientAssociation { /// Automatically release the association and shut down the connection. impl Drop for ClientAssociation { fn drop(&mut self) { - async { - let _ = self.release_impl().await; - let _ = self.socket.shutdown().await; - }; + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + let _ = self.release_impl().await; + let _ = self.socket.shutdown().await; + }) + }) } } diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index ba1cead91..94642fb9c 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -24,3 +24,4 @@ pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; pub use pdata::{PDataReader, PDataWriter}; pub use server::{ServerAssociation, ServerAssociationOptions}; + diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 5ad6489fd..aa450727b 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,12 +1,16 @@ use std::{ - collections::VecDeque, fmt::Result, io::{Cursor, Read, Write}, pin::Pin, task::{Context, Poll} + collections::VecDeque, io::{BufRead, BufReader, Cursor, Read, Write} }; use bytes::{Buf, BytesMut}; +use snafu::ResultExt; +#[cfg(feature = "async")] use tokio::io::ReadBuf; +#[cfg(feature = "async")] +use std::{pin::Pin, task::{Context, Poll}}; use tracing::warn; -use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; +use crate::{pdu::{ReadPduSnafu, PDU_HEADER_SIZE}, read_pdu, Pdu}; #[cfg(feature = "async")] use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; @@ -287,7 +291,7 @@ where 0xFF, ]); - PDataWriter { + AsyncPDataWriter { stream, max_data_len: max_data_length, buffer, @@ -316,55 +320,56 @@ where } } -#[cfg(feature = "async")] -impl AsyncWrite for AsyncPDataWriter -where - W: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll>{ - let total_len = self.max_data_len as usize + 12; - if self.buffer.len() + buf.len() <= total_len { - // accumulate into buffer, do nothing - self.buffer.extend(buf); - Poll::Ready(Ok(buf.len())) - } else { - // fill in the rest of the buffer, send PDU, - // and leave out the rest for subsequent writes - let buf = &buf[..total_len - self.buffer.len()]; - self.buffer.extend(buf); - debug_assert_eq!(self.buffer.len(), total_len); - setup_pdata_header(&mut self.buffer, false); - let res = Pin::new(&mut self.stream).poll_write(cx, &self.buffer); - match res { - Poll::Ready(Ok(_)) => { - self.buffer.truncate(12); - Poll::Ready(Ok(buf.len())) - }, - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending - } - } - - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>{ - Pin::new(&mut self.stream).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>{ - Pin::new(&mut self.stream).poll_shutdown(cx) - } -} +// TODO +// #[cfg(feature = "async")] +// impl AsyncWrite for AsyncPDataWriter +// where +// W: AsyncWrite + Unpin, +// { +// fn poll_write( +// mut self: Pin<&mut Self>, +// cx: &mut Context<'_>, +// buf: &[u8], +// ) -> Poll>{ +// let total_len = self.max_data_len as usize + 12; +// if self.buffer.len() + buf.len() <= total_len { +// // accumulate into buffer, do nothing +// self.buffer.extend(buf); +// Poll::Ready(Ok(buf.len())) +// } else { +// // fill in the rest of the buffer, send PDU, +// // and leave out the rest for subsequent writes +// let buf = &buf[..total_len - self.buffer.len()]; +// self.buffer.extend(buf); +// debug_assert_eq!(self.buffer.len(), total_len); +// setup_pdata_header(&mut self.buffer, false); +// let res = Pin::new(&mut self.stream).poll_write(cx, &mut self.buffer); +// match res { +// Poll::Ready(Ok(_)) => { +// self.buffer.truncate(12); +// Poll::Ready(Ok(buf.len())) +// }, +// Poll::Ready(Err(e)) => Poll::Ready(Err(e)), +// Poll::Pending => Poll::Pending +// } +// } + +// } + +// fn poll_flush( +// mut self: Pin<&mut Self>, +// cx: &mut Context<'_>, +// ) -> Poll>{ +// Pin::new(&mut self.stream).poll_flush(cx) +// } + +// fn poll_shutdown( +// mut self: Pin<&mut Self>, +// cx: &mut Context<'_>, +// ) -> Poll>{ +// Pin::new(&mut self.stream).poll_shutdown(cx) +// } +// } /// With the P-Data writer dropped, /// this `Drop` implementation @@ -424,6 +429,7 @@ pub struct PDataReader { presentation_context_id: Option, max_data_length: u32, last_pdu: bool, + read_buffer: BytesMut, } impl PDataReader @@ -436,6 +442,7 @@ where presentation_context_id: None, max_data_length, last_pdu: false, + read_buffer: BytesMut::with_capacity(max_data_length as usize), } } @@ -461,13 +468,13 @@ where return Ok(0); } - let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); + let mut reader = BufReader::new(&mut self.stream); let msg = loop{ - let mut buf = Cursor::new(&read_buffer[..]); + let mut buf = Cursor::new(&self.read_buffer[..]); match read_pdu(&mut buf, self.max_data_length, false) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { Some(pdu) => { - read_buffer.advance(buf.position() as usize); + self.read_buffer.advance(buf.position() as usize); break pdu }, None => { @@ -475,8 +482,10 @@ where buf.set_position(0) } } - let recv = self.stream.read(&mut read_buffer).unwrap(); - if recv == 0 { + let recv = reader.fill_buf()?.to_vec(); + reader.consume(recv.len()); + self.read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { return Err(std::io::Error::new( std::io::ErrorKind::Other, "Connection closed by peer" )); @@ -510,41 +519,41 @@ where } } - -#[cfg(feature = "async")] -impl AsyncRead for PDataReader where R: AsyncRead + Unpin { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll>{ - let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); - let msg = loop{ - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, self.max_data_length, false) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu - }, - None => { - // Reset position - buf.set_position(0) - } - } - match Pin::new(&mut self.stream).poll_read(cx, &mut read_buffer){ - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(std::io::Error::new( - std::io::ErrorKind::Other, "Connection closed by peer" - ))), - Poll::Ready(Ok(_)) => {} - } - } - Poll::Ready(Ok(())) - } - -} +// TODO +// #[cfg(feature = "async")] +// impl AsyncRead for PDataReader where R: AsyncRead + Unpin { +// fn poll_read( +// mut self: Pin<&mut Self>, +// cx: &mut Context<'_>, +// buf: &mut ReadBuf<'_>, +// ) -> Poll>{ +// let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); +// let msg = loop{ +// let mut buf = Cursor::new(&read_buffer[..]); +// match read_pdu(&mut buf, self.max_data_length, false) +// .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { +// Some(pdu) => { +// read_buffer.advance(buf.position() as usize); +// break pdu +// }, +// None => { +// // Reset position +// buf.set_position(0) +// } +// } +// match Pin::new(&mut self.stream).poll_read(cx, &mut ReadBuf::new(read_buffer.as_mut())){ +// Poll::Pending => return Poll::Pending, +// Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), +// Poll::Ready(Ok(_)) => return Poll::Ready(Err(std::io::Error::new( +// std::io::ErrorKind::Other, "Connection closed by peer" +// ))), +// Poll::Ready(Ok(_)) => {} +// } +// } +// Poll::Ready(Ok(())) +// } + +// } /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). @@ -582,7 +591,7 @@ mod tests { // concatenate data chunks, compare with all data - match same_pdu { + match same_pdu.unwrap() { Pdu::PData { data: data_1 } => { let data_1 = &data_1[0]; @@ -618,7 +627,7 @@ mod tests { // concatenate data chunks, compare with all data - match (pdu_1, pdu_2, pdu_3) { + match (pdu_1.unwrap(), pdu_2.unwrap(), pdu_3.unwrap()) { ( Pdu::PData { data: data_1 }, Pdu::PData { data: data_2 }, diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 2739cff26..d6470078e 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -4,14 +4,13 @@ //! in which this application entity listens to incoming association requests. //! See [`ServerAssociationOptions`] //! for details and examples on how to create an association. +use bytes::{BytesMut, Buf}; use std::{borrow::Cow, io::Cursor}; #[cfg(not(feature = "async"))] -use std::{io::{Write, Read}, net::TcpStream}; +use std::{io::Write, net::TcpStream}; #[cfg(feature = "async")] use tokio::{io::AsyncWriteExt, net::TcpStream}; -use bytes::{Buf, BytesMut}; - use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; @@ -364,6 +363,9 @@ where #[cfg(not(feature = "async"))] /// Negotiate an association with the given TCP stream. pub fn establish(&self, mut socket: TcpStream) -> Result { + use std::io::{BufRead, BufReader}; + + use crate::pdu::ReadPduSnafu; ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, @@ -373,7 +375,9 @@ where let max_pdu_length = self.max_pdu_length; let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - let msg = loop{ + let mut reader = BufReader::new(&mut socket); + + let msg = loop { let mut buf = Cursor::new(&read_buffer[..]); match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { Some(pdu) => { @@ -385,10 +389,14 @@ where buf.set_position(0) } } - let recv = socket.read(&mut read_buffer).unwrap(); - if recv == 0 { + // Use BufReader to get similar behavior to AsyncRead read_buf + let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + reader.consume(recv.len()); + read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } + }; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); match msg { @@ -568,12 +576,19 @@ where let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); let pdu = loop { - match read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { - Some(pdu) => break pdu, - None => {} + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } } - if 0 == socket.read_buf(&mut read_buffer).await.unwrap() { - println!("Here {}", read_buffer.len()); + let recv = socket.read_buf(&mut read_buffer).await.unwrap(); + if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } }; @@ -848,9 +863,14 @@ impl ServerAssociation { /// Read a PDU message from the other intervenient. #[cfg(not(feature = "async"))] pub fn receive(&mut self) -> Result { + use std::io::{BufRead, BufReader, Cursor}; + + use crate::pdu::ReadPduSnafu; + let mut reader = BufReader::new(&mut self.socket); + loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); return Ok(pdu) @@ -860,8 +880,11 @@ impl ServerAssociation { buf.set_position(0) } } - let recv = self.socket.read(&mut self.read_buffer).unwrap(); - if recv == 0 { + // Use BufReader to get similar behavior to AsyncRead read_buf + let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + reader.consume(recv.len()); + self.read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } @@ -870,7 +893,7 @@ impl ServerAssociation { #[cfg(feature = "async")] /// Read a PDU message from the other intervenient. - pub async fn receive_async(&mut self) -> Result { + pub async fn receive(&mut self) -> Result { use std::io::Cursor; use bytes::Buf; @@ -921,7 +944,7 @@ impl ServerAssociation { ), }; let out = self.send(&pdu).await; - let _ = self.socket.pubshutdown().await; + let _ = self.socket.shutdown().await; out } @@ -958,9 +981,9 @@ impl ServerAssociation { /// /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. - // pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - // PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) - // } + pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) + } /// Obtain access to the inner TCP stream /// connected to the association acceptor. diff --git a/ul/src/pdu/mod.rs b/ul/src/pdu/mod.rs index 37a96473a..00ba5a2d4 100644 --- a/ul/src/pdu/mod.rs +++ b/ul/src/pdu/mod.rs @@ -74,7 +74,7 @@ pub enum ReadError { #[snafu(display("No PDU available"))] NoPduAvailable { backtrace: Backtrace }, - #[snafu(display("Could not read PDU"))] + #[snafu(display("Could not read PDU"),visibility(pub(crate)))] ReadPdu { source: std::io::Error, backtrace: Backtrace, diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index 3dcdcf23c..b8a8354da 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -198,7 +198,7 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // User Information Item. For a complete description of these items see Section // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6. while bytes.has_remaining() { - match read_pdu_variable(bytes.clone(), &codec)? { + match read_pdu_variable(&mut bytes, &codec)? { Some(PduVariableItem::ApplicationContext(val)) => { application_context_name = Some(val); } diff --git a/ul/tests/pdu.rs b/ul/tests/pdu.rs index 26c7fa157..037436fb0 100644 --- a/ul/tests/pdu.rs +++ b/ul/tests/pdu.rs @@ -1,8 +1,8 @@ -use dicom_ul::pdu::reader::{read_pdu, DEFAULT_MAX_PDU}; +use dicom_ul::pdu::reader::read_pdu; use dicom_ul::pdu::writer::write_pdu; use dicom_ul::pdu::{ AssociationRQ, PDataValue, PDataValueType, Pdu, PresentationContextProposed, UserIdentity, - UserIdentityType, UserVariableItem, + UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU }; use matches::matches; use std::io::Cursor; @@ -46,7 +46,8 @@ fn can_read_write_associate_rq() -> Result<(), Box> { let mut bytes = vec![0u8; 0]; write_pdu(&mut bytes, &association_rq.into())?; - let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)?; + let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)? + .unwrap(); if let Pdu::AssociationRQ(AssociationRQ { protocol_version, @@ -134,7 +135,8 @@ fn can_read_write_primary_field_only_user_identity() -> Result<(), Box Result<(), Box> { let mut bytes = Vec::new(); write_pdu(&mut bytes, &pdata_rq)?; - let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)?; + let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)? + .unwrap(); if let Pdu::PData { data } = result { assert_eq!(data.len(), 1); From b37bea4ec5d9c186b6c54663cb0fa54487c0f7dd Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 10:11:18 -0500 Subject: [PATCH 05/28] MAIN: Add POC async version of storescp and storescu * Finish exposing various methods of client/server as feature-gated async * Finish async PDataWriter (still having issues) --- Cargo.lock | 1 + storescp/Cargo.toml | 8 +- storescp/src/main.rs | 332 ++++++----------------------- storescp/src/store_async.rs | 270 +++++++++++++++++++++++ storescp/src/store_sync.rs | 270 +++++++++++++++++++++++ storescu/Cargo.toml | 2 + storescu/src/main.rs | 401 +++++++++++++++-------------------- storescu/src/store_async.rs | 243 +++++++++++++++++++++ storescu/src/store_sync.rs | 239 +++++++++++++++++++++ ul/src/association/client.rs | 19 +- ul/src/association/mod.rs | 3 + ul/src/association/pdata.rs | 112 ++++++---- 12 files changed, 1352 insertions(+), 548 deletions(-) create mode 100644 storescp/src/store_async.rs create mode 100644 storescp/src/store_sync.rs create mode 100644 storescu/src/store_async.rs create mode 100644 storescu/src/store_sync.rs diff --git a/Cargo.lock b/Cargo.lock index 3b7882966..e7c71fc81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,6 +587,7 @@ dependencies = [ "dicom-ul", "indicatif", "snafu", + "tokio", "tracing", "tracing-subscriber", "walkdir", diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index 57bbcf187..e9d8df1b0 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] clap = { version = "4.0.18", features = ["derive"] } dicom-core = { path = '../core', version = "0.7.0" } -dicom-ul = { path = '../ul', version = "0.7.0", features = [] } +dicom-ul = { path = '../ul', version = "0.7.0" } dicom-object = { path = '../object', version = "0.7.0" } dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-dictionary-std = { path = "../dictionary-std/", version = "0.7.0" } @@ -21,4 +21,8 @@ dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", versio snafu = "0.8" tracing = "0.1.36" tracing-subscriber = "0.3.15" -tokio = "1.38.0" +tokio = { version = "1.38.0", features = ["full"], optional = true } + +[features] +deafult = ["async"] +async = ["dicom-ul/async", "dep:tokio"] diff --git a/storescp/src/main.rs b/storescp/src/main.rs index e2facccbb..ced0f6c35 100644 --- a/storescp/src/main.rs +++ b/storescp/src/main.rs @@ -1,22 +1,24 @@ use std::{ - net::{Ipv4Addr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddrV4, TcpListener}, path::PathBuf, - sync::Arc, }; use clap::Parser; use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::tags; -use dicom_encoding::transfer_syntax::TransferSyntaxIndex; -use dicom_object::{FileMetaTableBuilder, InMemDicomObject, StandardDataDictionary}; -use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{pdu::PDataValueType, Pdu}; -use snafu::{OptionExt, Report, ResultExt, Whatever}; -use tracing::{debug, error, info, warn, Level}; +use dicom_object::{InMemDicomObject, StandardDataDictionary}; +use tracing::{error, info, Level}; -use crate::transfer::ABSTRACT_SYNTAXES; mod transfer; +#[cfg(feature = "async")] +mod store_async; +#[cfg(feature = "async")] +use store_async::run; +#[cfg(not(feature = "async"))] +mod store_sync; +#[cfg(not(feature = "async"))] +use store_sync::run; /// DICOM C-STORE SCP #[derive(Debug, Parser)] @@ -48,267 +50,7 @@ struct App { port: u16, } -async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { - let App { - verbose, - calling_ae_title, - strict, - uncompressed_only, - promiscuous, - max_pdu_length, - out_dir, - port: _, - } = args; - let verbose = *verbose; - let mut buffer: Vec = Vec::with_capacity(*max_pdu_length as usize); - let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); - let mut msgid = 1; - let mut sop_class_uid = "".to_string(); - let mut sop_instance_uid = "".to_string(); - - let mut options = dicom_ul::association::ServerAssociationOptions::new() - .accept_any() - .ae_title(calling_ae_title) - .strict(*strict) - .promiscuous(*promiscuous); - - if *uncompressed_only { - options = options - .with_transfer_syntax("1.2.840.10008.1.2") - .with_transfer_syntax("1.2.840.10008.1.2.1"); - } else { - for ts in TransferSyntaxRegistry.iter() { - if !ts.is_unsupported() { - options = options.with_transfer_syntax(ts.uid()); - } - } - }; - - for uid in ABSTRACT_SYNTAXES { - options = options.with_abstract_syntax(*uid); - } - - let mut association = options - .establish(scu_stream) - .await - .whatever_context("could not establish association")?; - - info!("New association from {}", association.client_ae_title()); - debug!( - "> Presentation contexts: {:?}", - association.presentation_contexts() - ); - - loop { - match association.receive().await { - Ok(mut pdu) => { - if verbose { - debug!("scu ----> scp: {}", pdu.short_description()); - } - match pdu { - Pdu::PData { ref mut data } => { - if data.is_empty() { - debug!("Ignoring empty PData PDU"); - continue; - } - - for data_value in data { - if data_value.value_type == PDataValueType::Data && !data_value.is_last - { - instance_buffer.append(&mut data_value.data); - } else if data_value.value_type == PDataValueType::Command - && data_value.is_last - { - // commands are always in implict VR LE - let ts = - dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN - .erased(); - let data_value = &data_value; - let v = &data_value.data; - - let obj = InMemDicomObject::read_dataset_with_ts(v.as_slice(), &ts) - .whatever_context("failed to read incoming DICOM command")?; - let command_field = obj - .element(tags::COMMAND_FIELD) - .whatever_context("Missing Command Field")? - .uint16() - .whatever_context("Command Field is not an integer")?; - - if command_field == 0x0030 { - // Handle C-ECHO-RQ - let cecho_response = create_cecho_response(msgid); - let mut cecho_data = Vec::new(); - - cecho_response - .write_dataset_with_ts(&mut cecho_data, &ts) - .whatever_context( - "could not write C-ECHO response object", - )?; - - let pdu_response = Pdu::PData { - data: vec![dicom_ul::pdu::PDataValue { - presentation_context_id: data_value - .presentation_context_id, - value_type: PDataValueType::Command, - is_last: true, - data: cecho_data, - }], - }; - association.send(&pdu_response).await.whatever_context( - "failed to send C-ECHO response object to SCU", - )?; - } else { - msgid = obj - .element(tags::MESSAGE_ID) - .whatever_context("Missing Message ID")? - .to_int() - .whatever_context("Message ID is not an integer")?; - sop_class_uid = obj - .element(tags::AFFECTED_SOP_CLASS_UID) - .whatever_context("missing Affected SOP Class UID")? - .to_str() - .whatever_context( - "could not retrieve Affected SOP Class UID", - )? - .to_string(); - sop_instance_uid = obj - .element(tags::AFFECTED_SOP_INSTANCE_UID) - .whatever_context("missing Affected SOP Instance UID")? - .to_str() - .whatever_context( - "could not retrieve Affected SOP Instance UID", - )? - .to_string(); - } - instance_buffer.clear(); - } else if data_value.value_type == PDataValueType::Data - && data_value.is_last - { - instance_buffer.append(&mut data_value.data); - - let presentation_context = association - .presentation_contexts() - .iter() - .find(|pc| pc.id == data_value.presentation_context_id) - .whatever_context("missing presentation context")?; - let ts = &presentation_context.transfer_syntax; - - let obj = InMemDicomObject::read_dataset_with_ts( - instance_buffer.as_slice(), - TransferSyntaxRegistry.get(ts).unwrap(), - ) - .whatever_context("failed to read DICOM data object")?; - let file_meta = FileMetaTableBuilder::new() - .media_storage_sop_class_uid( - obj.element(tags::SOP_CLASS_UID) - .whatever_context("missing SOP Class UID")? - .to_str() - .whatever_context("could not retrieve SOP Class UID")?, - ) - .media_storage_sop_instance_uid( - obj.element(tags::SOP_INSTANCE_UID) - .whatever_context("missing SOP Instance UID")? - .to_str() - .whatever_context("missing SOP Instance UID")?, - ) - .transfer_syntax(ts) - .build() - .whatever_context( - "failed to build DICOM meta file information", - )?; - let file_obj = obj.with_exact_meta(file_meta); - - // write the files to the current directory with their SOPInstanceUID as filenames - let mut file_path = out_dir.clone(); - file_path.push( - sop_instance_uid.trim_end_matches('\0').to_string() + ".dcm", - ); - file_obj - .write_to_file(&file_path) - .whatever_context("could not save DICOM object to file")?; - info!("Stored {}", file_path.display()); - - // send C-STORE-RSP object - // commands are always in implict VR LE - let ts = - dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN - .erased(); - - let obj = create_cstore_response( - msgid, - &sop_class_uid, - &sop_instance_uid, - ); - - let mut obj_data = Vec::new(); - - obj.write_dataset_with_ts(&mut obj_data, &ts) - .whatever_context("could not write response object")?; - - let pdu_response = Pdu::PData { - data: vec![dicom_ul::pdu::PDataValue { - presentation_context_id: data_value.presentation_context_id, - value_type: PDataValueType::Command, - is_last: true, - data: obj_data, - }], - }; - association - .send(&pdu_response) - .await - .whatever_context("failed to send response object to SCU")?; - } - } - } - Pdu::ReleaseRQ => { - buffer.clear(); - association.send(&Pdu::ReleaseRP).await.unwrap_or_else(|e| { - warn!( - "Failed to send association release message to SCU: {}", - snafu::Report::from_error(e) - ); - }); - info!( - "Released association with {}", - association.client_ae_title() - ); - break; - } - Pdu::AbortRQ { source } => { - warn!("Aborted connection from: {:?}", source); - break; - } - _ => {} - } - } - Err(err @ dicom_ul::association::server::Error::Receive { .. }) => { - if verbose { - info!("{}", Report::from_error(err)); - } else { - info!("{}", err); - } - break; - } - Err(err) => { - warn!("Unexpected error: {}", Report::from_error(err)); - break; - } - } - } - - if let Ok(peer_addr) = association.inner_stream().peer_addr() { - info!( - "Dropping connection with {} ({})", - association.client_ae_title(), - peer_addr - ); - } else { - info!("Dropping connection with {}", association.client_ae_title()); - } - - Ok(()) -} fn create_cstore_response( message_id: u16, @@ -358,8 +100,10 @@ fn create_cecho_response(message_id: u16) -> InMemDicomObject Result<(), Box> { + use std::sync::Arc; let args = Arc::new(App::parse()); tracing::subscriber::set_global_default( @@ -403,6 +147,56 @@ async fn main() -> Result<(), Box> { Ok(()) } +#[cfg(not(feature = "async"))] +fn main() -> Result<(), Box> { + use std::io::Read; + + let args = App::parse(); + + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(if args.verbose { + Level::DEBUG + } else { + Level::INFO + }) + .finish(), + ) + .unwrap_or_else(|e| { + eprintln!( + "Could not set up global logger: {}", + snafu::Report::from_error(e) + ); + }); + + std::fs::create_dir_all(&args.out_dir).unwrap_or_else(|e| { + error!("Could not create output directory: {}", e); + std::process::exit(-2); + }); + + let listen_addr = SocketAddrV4::new(Ipv4Addr::from(0), args.port); + let listener = TcpListener::bind(listen_addr)?; + info!( + "{} listening on: tcp://{}", + &args.calling_ae_title, listen_addr + ); + + for stream in listener.incoming() { + match stream { + Ok(scu_stream) => { + if let Err(e) = run(scu_stream, &args) { + error!("{}", snafu::Report::from_error(e)); + } + } + Err(e) => { + error!("{}", snafu::Report::from_error(e)); + } + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use crate::App; diff --git a/storescp/src/store_async.rs b/storescp/src/store_async.rs new file mode 100644 index 000000000..36352488b --- /dev/null +++ b/storescp/src/store_async.rs @@ -0,0 +1,270 @@ +use dicom_dictionary_std::tags; +use dicom_encoding::transfer_syntax::TransferSyntaxIndex; +use dicom_object::{FileMetaTableBuilder, InMemDicomObject, StandardDataDictionary}; +use dicom_transfer_syntax_registry::TransferSyntaxRegistry; +use dicom_ul::{pdu::PDataValueType, Pdu}; +use snafu::{OptionExt, Report, ResultExt, Whatever}; +use tracing::{debug, error, info, warn, Level}; + +use crate::{transfer::ABSTRACT_SYNTAXES, App, create_cecho_response, create_cstore_response}; +pub async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { + let App { + verbose, + calling_ae_title, + strict, + uncompressed_only, + promiscuous, + max_pdu_length, + out_dir, + port: _, + } = args; + let verbose = *verbose; + + let mut buffer: Vec = Vec::with_capacity(*max_pdu_length as usize); + let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); + let mut msgid = 1; + let mut sop_class_uid = "".to_string(); + let mut sop_instance_uid = "".to_string(); + + let mut options = dicom_ul::association::ServerAssociationOptions::new() + .accept_any() + .ae_title(calling_ae_title) + .strict(*strict) + .promiscuous(*promiscuous); + + if *uncompressed_only { + options = options + .with_transfer_syntax("1.2.840.10008.1.2") + .with_transfer_syntax("1.2.840.10008.1.2.1"); + } else { + for ts in TransferSyntaxRegistry.iter() { + if !ts.is_unsupported() { + options = options.with_transfer_syntax(ts.uid()); + } + } + }; + + for uid in ABSTRACT_SYNTAXES { + options = options.with_abstract_syntax(*uid); + } + + let mut association = options + .establish(scu_stream) + .await + .whatever_context("could not establish association")?; + + info!("New association from {}", association.client_ae_title()); + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + + loop { + match association.receive().await { + Ok(mut pdu) => { + if verbose { + debug!("scu ----> scp: {}", pdu.short_description()); + } + match pdu { + Pdu::PData { ref mut data } => { + if data.is_empty() { + debug!("Ignoring empty PData PDU"); + continue; + } + + for data_value in data { + if data_value.value_type == PDataValueType::Data && !data_value.is_last + { + instance_buffer.append(&mut data_value.data); + } else if data_value.value_type == PDataValueType::Command + && data_value.is_last + { + // commands are always in implict VR LE + let ts = + dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(); + let data_value = &data_value; + let v = &data_value.data; + + let obj = InMemDicomObject::read_dataset_with_ts(v.as_slice(), &ts) + .whatever_context("failed to read incoming DICOM command")?; + let command_field = obj + .element(tags::COMMAND_FIELD) + .whatever_context("Missing Command Field")? + .uint16() + .whatever_context("Command Field is not an integer")?; + + if command_field == 0x0030 { + // Handle C-ECHO-RQ + let cecho_response = create_cecho_response(msgid); + let mut cecho_data = Vec::new(); + + cecho_response + .write_dataset_with_ts(&mut cecho_data, &ts) + .whatever_context( + "could not write C-ECHO response object", + )?; + + let pdu_response = Pdu::PData { + data: vec![dicom_ul::pdu::PDataValue { + presentation_context_id: data_value + .presentation_context_id, + value_type: PDataValueType::Command, + is_last: true, + data: cecho_data, + }], + }; + association.send(&pdu_response).await.whatever_context( + "failed to send C-ECHO response object to SCU", + )?; + } else { + msgid = obj + .element(tags::MESSAGE_ID) + .whatever_context("Missing Message ID")? + .to_int() + .whatever_context("Message ID is not an integer")?; + sop_class_uid = obj + .element(tags::AFFECTED_SOP_CLASS_UID) + .whatever_context("missing Affected SOP Class UID")? + .to_str() + .whatever_context( + "could not retrieve Affected SOP Class UID", + )? + .to_string(); + sop_instance_uid = obj + .element(tags::AFFECTED_SOP_INSTANCE_UID) + .whatever_context("missing Affected SOP Instance UID")? + .to_str() + .whatever_context( + "could not retrieve Affected SOP Instance UID", + )? + .to_string(); + } + instance_buffer.clear(); + } else if data_value.value_type == PDataValueType::Data + && data_value.is_last + { + instance_buffer.append(&mut data_value.data); + + let presentation_context = association + .presentation_contexts() + .iter() + .find(|pc| pc.id == data_value.presentation_context_id) + .whatever_context("missing presentation context")?; + let ts = &presentation_context.transfer_syntax; + + let obj = InMemDicomObject::read_dataset_with_ts( + instance_buffer.as_slice(), + TransferSyntaxRegistry.get(ts).unwrap(), + ) + .whatever_context("failed to read DICOM data object")?; + let file_meta = FileMetaTableBuilder::new() + .media_storage_sop_class_uid( + obj.element(tags::SOP_CLASS_UID) + .whatever_context("missing SOP Class UID")? + .to_str() + .whatever_context("could not retrieve SOP Class UID")?, + ) + .media_storage_sop_instance_uid( + obj.element(tags::SOP_INSTANCE_UID) + .whatever_context("missing SOP Instance UID")? + .to_str() + .whatever_context("missing SOP Instance UID")?, + ) + .transfer_syntax(ts) + .build() + .whatever_context( + "failed to build DICOM meta file information", + )?; + let file_obj = obj.with_exact_meta(file_meta); + + // write the files to the current directory with their SOPInstanceUID as filenames + let mut file_path = out_dir.clone(); + file_path.push( + sop_instance_uid.trim_end_matches('\0').to_string() + ".dcm", + ); + file_obj + .write_to_file(&file_path) + .whatever_context("could not save DICOM object to file")?; + info!("Stored {}", file_path.display()); + + // send C-STORE-RSP object + // commands are always in implict VR LE + let ts = + dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(); + + let obj = create_cstore_response( + msgid, + &sop_class_uid, + &sop_instance_uid, + ); + + let mut obj_data = Vec::new(); + + obj.write_dataset_with_ts(&mut obj_data, &ts) + .whatever_context("could not write response object")?; + + let pdu_response = Pdu::PData { + data: vec![dicom_ul::pdu::PDataValue { + presentation_context_id: data_value.presentation_context_id, + value_type: PDataValueType::Command, + is_last: true, + data: obj_data, + }], + }; + association + .send(&pdu_response) + .await + .whatever_context("failed to send response object to SCU")?; + } + } + } + Pdu::ReleaseRQ => { + buffer.clear(); + association.send(&Pdu::ReleaseRP).await.unwrap_or_else(|e| { + warn!( + "Failed to send association release message to SCU: {}", + snafu::Report::from_error(e) + ); + }); + info!( + "Released association with {}", + association.client_ae_title() + ); + break; + } + Pdu::AbortRQ { source } => { + warn!("Aborted connection from: {:?}", source); + break; + } + _ => {} + } + } + Err(err @ dicom_ul::association::server::Error::Receive { .. }) => { + if verbose { + info!("{}", Report::from_error(err)); + } else { + info!("{}", err); + } + break; + } + Err(err) => { + warn!("Unexpected error: {}", Report::from_error(err)); + break; + } + } + } + + if let Ok(peer_addr) = association.inner_stream().peer_addr() { + info!( + "Dropping connection with {} ({})", + association.client_ae_title(), + peer_addr + ); + } else { + info!("Dropping connection with {}", association.client_ae_title()); + } + + Ok(()) +} \ No newline at end of file diff --git a/storescp/src/store_sync.rs b/storescp/src/store_sync.rs new file mode 100644 index 000000000..e2e57fbc9 --- /dev/null +++ b/storescp/src/store_sync.rs @@ -0,0 +1,270 @@ +use std::net::TcpStream; + +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::PDataValueType, Pdu}; +use snafu::{OptionExt, Report, ResultExt, Whatever}; +use tracing::{debug, info, warn}; + +use crate::{create_cecho_response, create_cstore_response, transfer::ABSTRACT_SYNTAXES, App}; +pub fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { + let App { + verbose, + calling_ae_title, + strict, + uncompressed_only, + promiscuous, + max_pdu_length, + out_dir, + port: _, + } = args; + let verbose = *verbose; + + let mut buffer: Vec = Vec::with_capacity(*max_pdu_length as usize); + let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); + let mut msgid = 1; + let mut sop_class_uid = "".to_string(); + let mut sop_instance_uid = "".to_string(); + + let mut options = dicom_ul::association::ServerAssociationOptions::new() + .accept_any() + .ae_title(calling_ae_title) + .strict(*strict) + .promiscuous(*promiscuous); + + if *uncompressed_only { + options = options + .with_transfer_syntax("1.2.840.10008.1.2") + .with_transfer_syntax("1.2.840.10008.1.2.1"); + } else { + for ts in TransferSyntaxRegistry.iter() { + if !ts.is_unsupported() { + options = options.with_transfer_syntax(ts.uid()); + } + } + }; + + for uid in ABSTRACT_SYNTAXES { + options = options.with_abstract_syntax(*uid); + } + + let mut association = options + .establish(scu_stream) + .whatever_context("could not establish association")?; + + info!("New association from {}", association.client_ae_title()); + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + + loop { + match association.receive() { + Ok(mut pdu) => { + if verbose { + debug!("scu ----> scp: {}", pdu.short_description()); + } + match pdu { + Pdu::PData { ref mut data } => { + if data.is_empty() { + debug!("Ignoring empty PData PDU"); + continue; + } + + for data_value in data { + if data_value.value_type == PDataValueType::Data && !data_value.is_last + { + instance_buffer.append(&mut data_value.data); + } else if data_value.value_type == PDataValueType::Command + && data_value.is_last + { + // commands are always in implict VR LE + let ts = + dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(); + let data_value = &data_value; + let v = &data_value.data; + + let obj = InMemDicomObject::read_dataset_with_ts(v.as_slice(), &ts) + .whatever_context("failed to read incoming DICOM command")?; + let command_field = obj + .element(tags::COMMAND_FIELD) + .whatever_context("Missing Command Field")? + .uint16() + .whatever_context("Command Field is not an integer")?; + + if command_field == 0x0030 { + // Handle C-ECHO-RQ + let cecho_response = create_cecho_response(msgid); + let mut cecho_data = Vec::new(); + + cecho_response + .write_dataset_with_ts(&mut cecho_data, &ts) + .whatever_context( + "could not write C-ECHO response object", + )?; + + let pdu_response = Pdu::PData { + data: vec![dicom_ul::pdu::PDataValue { + presentation_context_id: data_value + .presentation_context_id, + value_type: PDataValueType::Command, + is_last: true, + data: cecho_data, + }], + }; + association.send(&pdu_response).whatever_context( + "failed to send C-ECHO response object to SCU", + )?; + } else { + msgid = obj + .element(tags::MESSAGE_ID) + .whatever_context("Missing Message ID")? + .to_int() + .whatever_context("Message ID is not an integer")?; + sop_class_uid = obj + .element(tags::AFFECTED_SOP_CLASS_UID) + .whatever_context("missing Affected SOP Class UID")? + .to_str() + .whatever_context( + "could not retrieve Affected SOP Class UID", + )? + .to_string(); + sop_instance_uid = obj + .element(tags::AFFECTED_SOP_INSTANCE_UID) + .whatever_context("missing Affected SOP Instance UID")? + .to_str() + .whatever_context( + "could not retrieve Affected SOP Instance UID", + )? + .to_string(); + } + instance_buffer.clear(); + } else if data_value.value_type == PDataValueType::Data + && data_value.is_last + { + instance_buffer.append(&mut data_value.data); + + let presentation_context = association + .presentation_contexts() + .iter() + .find(|pc| pc.id == data_value.presentation_context_id) + .whatever_context("missing presentation context")?; + let ts = &presentation_context.transfer_syntax; + + let obj = InMemDicomObject::read_dataset_with_ts( + instance_buffer.as_slice(), + TransferSyntaxRegistry.get(ts).unwrap(), + ) + .whatever_context("failed to read DICOM data object")?; + let file_meta = FileMetaTableBuilder::new() + .media_storage_sop_class_uid( + obj.element(tags::SOP_CLASS_UID) + .whatever_context("missing SOP Class UID")? + .to_str() + .whatever_context("could not retrieve SOP Class UID")?, + ) + .media_storage_sop_instance_uid( + obj.element(tags::SOP_INSTANCE_UID) + .whatever_context("missing SOP Instance UID")? + .to_str() + .whatever_context("missing SOP Instance UID")?, + ) + .transfer_syntax(ts) + .build() + .whatever_context( + "failed to build DICOM meta file information", + )?; + let file_obj = obj.with_exact_meta(file_meta); + + // write the files to the current directory with their SOPInstanceUID as filenames + let mut file_path = out_dir.clone(); + file_path.push( + sop_instance_uid.trim_end_matches('\0').to_string() + ".dcm", + ); + file_obj + .write_to_file(&file_path) + .whatever_context("could not save DICOM object to file")?; + info!("Stored {}", file_path.display()); + + // send C-STORE-RSP object + // commands are always in implict VR LE + let ts = + dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(); + + let obj = create_cstore_response( + msgid, + &sop_class_uid, + &sop_instance_uid, + ); + + let mut obj_data = Vec::new(); + + obj.write_dataset_with_ts(&mut obj_data, &ts) + .whatever_context("could not write response object")?; + + let pdu_response = Pdu::PData { + data: vec![dicom_ul::pdu::PDataValue { + presentation_context_id: data_value.presentation_context_id, + value_type: PDataValueType::Command, + is_last: true, + data: obj_data, + }], + }; + association + .send(&pdu_response) + .whatever_context("failed to send response object to SCU")?; + } + } + } + Pdu::ReleaseRQ => { + buffer.clear(); + association.send(&Pdu::ReleaseRP).unwrap_or_else(|e| { + warn!( + "Failed to send association release message to SCU: {}", + snafu::Report::from_error(e) + ); + }); + info!( + "Released association with {}", + association.client_ae_title() + ); + break; + } + Pdu::AbortRQ { source } => { + warn!("Aborted connection from: {:?}", source); + break; + } + _ => {} + } + } + Err(err @ dicom_ul::association::server::Error::Receive { .. }) => { + if verbose { + info!("{}", Report::from_error(err)); + } else { + info!("{}", err); + } + break; + } + Err(err) => { + warn!("Unexpected error: {}", Report::from_error(err)); + break; + } + } + } + + if let Ok(peer_addr) = association.inner_stream().peer_addr() { + info!( + "Dropping connection with {} ({})", + association.client_ae_title(), + peer_addr + ); + } else { + info!("Dropping connection with {}", association.client_ae_title()); + } + + Ok(()) +} \ No newline at end of file diff --git a/storescu/Cargo.toml b/storescu/Cargo.toml index 52587d0c0..d17cd2d1b 100644 --- a/storescu/Cargo.toml +++ b/storescu/Cargo.toml @@ -14,6 +14,7 @@ readme = "README.md" default = ["transcode"] # support DICOM transcoding transcode = ["dep:dicom-pixeldata"] +async = ["dicom-ul/async", "dep:tokio"] [dependencies] clap = { version = "4.0.18", features = ["derive"] } @@ -29,3 +30,4 @@ indicatif = "0.17.0" tracing = "0.1.34" tracing-subscriber = "0.3.11" snafu = "0.8" +tokio = { version = "1.38.0", features = ["full"], optional = true } \ No newline at end of file diff --git a/storescu/src/main.rs b/storescu/src/main.rs index 8ac4e05f0..d4b03cba1 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -3,24 +3,24 @@ use dicom_core::{dicom_value, header::Tag, DataElement, VR}; use dicom_dictionary_std::{tags, uids}; use dicom_encoding::transfer_syntax; use dicom_encoding::TransferSyntax; -use dicom_object::{mem::InMemDicomObject, open_file, DefaultDicomObject, StandardDataDictionary}; +use dicom_object::{mem::InMemDicomObject, DefaultDicomObject, StandardDataDictionary}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{ - association::ClientAssociationOptions, - pdu::{PDataValue, PDataValueType, Pdu}, -}; use indicatif::{ProgressBar, ProgressStyle}; use snafu::prelude::*; use snafu::{Report, Whatever}; use std::collections::HashSet; use std::ffi::OsStr; -use std::io::Write; use std::path::{Path, PathBuf}; use std::time::Duration; use tracing::{debug, error, info, warn, Level}; use transfer_syntax::TransferSyntaxIndex; use walkdir::WalkDir; +#[cfg(not(feature = "async"))] +mod store_sync; +#[cfg(feature = "async")] +mod store_async; + /// DICOM C-STORE SCU #[derive(Debug, Parser)] #[command(version)] @@ -131,6 +131,7 @@ enum Error { }, } +#[cfg(not(feature = "async"))] fn main() { run().unwrap_or_else(|e| { error!("{}", Report::from_error(e)); @@ -138,39 +139,18 @@ fn main() { }); } -fn run() -> Result<(), Error> { - let App { - addr, - files, - verbose, - message_id, - calling_ae_title, - called_ae_title, - max_pdu_length, - fail_first, - mut never_transcode, - username, - password, - kerberos_service_ticket, - saml_assertion, - jwt, - } = App::parse(); - - // never transcode if the feature is disabled - if cfg!(not(feature = "transcode")) { - never_transcode = true; - } - - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) - .finish(), - ) - .whatever_context("Could not set up global logging subscriber") - .unwrap_or_else(|e: Whatever| { - eprintln!("[ERROR] {}", Report::from_error(e)); +#[cfg(feature = "async")] +#[tokio::main] +async fn main() { + run().await.unwrap_or_else(|e| { + error!("{}", Report::from_error(e)); + std::process::exit(-2); }); +} +fn check_files( + files: Vec, verbose: bool, never_transcode: bool +) -> (Vec, HashSet<(String, String)>){ let mut checked_files: Vec = vec![]; let mut dicom_files: Vec = vec![]; let mut presentation_contexts = HashSet::new(); @@ -227,44 +207,63 @@ fn run() -> Result<(), Error> { eprintln!("No supported files to transfer"); std::process::exit(-1); } + return (dicom_files, presentation_contexts) - if verbose { - info!("Establishing association with '{}'...", &addr); - } - - let mut scu_init = ClientAssociationOptions::new() - .calling_ae_title(calling_ae_title) - .max_pdu_length(max_pdu_length); - for (storage_sop_class_uid, transfer_syntax) in &presentation_contexts { - scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); - } - - if let Some(called_ae_title) = called_ae_title { - scu_init = scu_init.called_ae_title(called_ae_title); - } +} - if let Some(username) = username { - scu_init = scu_init.username(username); - } +#[cfg(not(feature = "async"))] +fn run() -> Result<(), Error> { + let App { + addr, + files, + verbose, + message_id, + calling_ae_title, + called_ae_title, + max_pdu_length, + fail_first, + mut never_transcode, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + } = App::parse(); - if let Some(password) = password { - scu_init = scu_init.password(password); + // never transcode if the feature is disabled + if cfg!(not(feature = "transcode")) { + never_transcode = true; } - if let Some(kerberos_service_ticket) = kerberos_service_ticket { - scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); - } + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) + .finish(), + ) + .whatever_context("Could not set up global logging subscriber") + .unwrap_or_else(|e: Whatever| { + eprintln!("[ERROR] {}", Report::from_error(e)); + }); - if let Some(saml_assertion) = saml_assertion { - scu_init = scu_init.saml_assertion(saml_assertion); + if verbose { + info!("Establishing association with '{}'...", &addr); } + let (mut dicom_files, presentation_contexts) = check_files(files, verbose, never_transcode); - if let Some(jwt) = jwt { - scu_init = scu_init.jwt(jwt); - } - let mut scu = scu_init.establish_with(&addr).context(InitScuSnafu)?; + let mut scu = get_scu( + addr, + calling_ae_title, + called_ae_title, + max_pdu_length, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + presentation_contexts + )?; if verbose { info!("Association established"); @@ -313,190 +312,134 @@ fn run() -> Result<(), Error> { } for file in dicom_files { - if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { - if let Some(pb) = &progress_bar { - pb.set_message(file.sop_instance_uid.clone()); - } - let cmd = store_req_command(&file.sop_class_uid, &file.sop_instance_uid, message_id); - - let mut cmd_data = Vec::with_capacity(128); - cmd.write_dataset_with_ts( - &mut cmd_data, - &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), - ) - .map_err(Box::from) - .context(CreateCommandSnafu)?; - - let mut object_data = Vec::with_capacity(2048); - let dicom_file = - open_file(&file.file).whatever_context("Could not open listed DICOM file")?; - let ts_selected = TransferSyntaxRegistry - .get(&ts_uid_selected) - .with_context(|| UnsupportedFileTransferSyntaxSnafu { - uid: ts_uid_selected.to_string(), - })?; - - // transcode file if necessary - let dicom_file = into_ts(dicom_file, ts_selected, verbose)?; - - dicom_file - .write_dataset_with_ts(&mut object_data, ts_selected) - .whatever_context("Could not write object dataset")?; - - let nbytes = cmd_data.len() + object_data.len(); - - if verbose { - info!( - "Sending file {} (~ {} kB), uid={}, sop={}, ts={}", - file.file.display(), - nbytes / 1_000, - &file.sop_instance_uid, - &file.sop_class_uid, - ts_uid_selected, - ); - } + // TODO + scu = store_sync::send_file(scu, file, message_id, progress_bar.as_ref(), verbose, fail_first)?; + } - if nbytes < scu.acceptor_max_pdu_length().saturating_sub(100) as usize { - let pdu = Pdu::PData { - data: vec![ - PDataValue { - presentation_context_id: pc_selected.id, - value_type: PDataValueType::Command, - is_last: true, - data: cmd_data, - }, - PDataValue { - presentation_context_id: pc_selected.id, - value_type: PDataValueType::Data, - is_last: true, - data: object_data, - }, - ], - }; - - scu.send(&pdu) - .whatever_context("Failed to send C-STORE-RQ")?; - } else { - let pdu = Pdu::PData { - data: vec![PDataValue { - presentation_context_id: pc_selected.id, - value_type: PDataValueType::Command, - is_last: true, - data: cmd_data, - }], - }; - - scu.send(&pdu) - .whatever_context("Failed to send C-STORE-RQ command")?; - - { - let mut pdata = scu.send_pdata(pc_selected.id); - pdata - .write_all(&object_data) - .whatever_context("Failed to send C-STORE-RQ P-Data")?; - } - } + if let Some(pb) = progress_bar { + pb.finish_with_message("done") + }; - if verbose { - debug!("Awaiting response..."); - } + scu.release() + .whatever_context("Failed to release SCU association")?; + Ok(()) +} - let rsp_pdu = scu - .receive() - .whatever_context("Failed to receive C-STORE-RSP")?; - - match rsp_pdu { - Pdu::PData { data } => { - let data_value = &data[0]; - - let cmd_obj = InMemDicomObject::read_dataset_with_ts( - &data_value.data[..], - &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN - .erased(), - ) - .whatever_context("Could not read response from SCP")?; - if verbose { - debug!("Full response: {:?}", cmd_obj); - } - let status = cmd_obj - .element(tags::STATUS) - .whatever_context("Could not find status code in response")? - .to_int::() - .whatever_context("Status code in response is not a valid integer")?; - let storage_sop_instance_uid = file - .sop_instance_uid - .trim_end_matches(|c: char| c.is_whitespace() || c == '\0'); - - match status { - // Success - 0 => { - if verbose { - info!("Successfully stored instance {}", storage_sop_instance_uid); - } - } - // Warning - 1 | 0x0107 | 0x0116 | 0xB000..=0xBFFF => { - warn!( - "Possible issue storing instance `{}` (status code {:04X}H)", - storage_sop_instance_uid, status - ); - } - 0xFF00 | 0xFF01 => { - warn!( - "Possible issue storing instance `{}`: status is pending (status code {:04X}H)", - storage_sop_instance_uid, status - ); - } - 0xFE00 => { - error!( - "Could not store instance `{}`: operation cancelled", - storage_sop_instance_uid - ); - if fail_first { - let _ = scu.abort(); - std::process::exit(-2); - } - } - _ => { - error!( - "Failed to store instance `{}` (status code {:04X}H)", - storage_sop_instance_uid, status - ); - if fail_first { - let _ = scu.abort(); - std::process::exit(-2); - } - } - } - } +#[cfg(feature = "async")] +async fn run() -> Result<(), Error> { + use crate::store_async::{get_scu, send_file}; + let App { + addr, + files, + verbose, + message_id, + calling_ae_title, + called_ae_title, + max_pdu_length, + fail_first, + mut never_transcode, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + } = App::parse(); + + // never transcode if the feature is disabled + if cfg!(not(feature = "transcode")) { + never_transcode = true; + } + + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) + .finish(), + ) + .whatever_context("Could not set up global logging subscriber") + .unwrap_or_else(|e: Whatever| { + eprintln!("[ERROR] {}", Report::from_error(e)); + }); + + if verbose { + info!("Establishing association with '{}'...", &addr); + } + let (mut dicom_files, presentation_contexts) = tokio::task::spawn_blocking( + move || check_files(files, verbose, never_transcode) + ).await.unwrap(); + + + let mut scu = get_scu( + addr, + calling_ae_title, + called_ae_title, + max_pdu_length, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + presentation_contexts + ).await?; + + if verbose { + info!("Association established"); + } - pdu @ Pdu::Unknown { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::ReleaseRQ - | pdu @ Pdu::ReleaseRP - | pdu @ Pdu::AbortRQ { .. } => { - error!("Unexpected SCP response: {:?}", pdu); + for file in &mut dicom_files { + // identify the right transfer syntax to use + let r: Result<_, Error> = + check_presentation_contexts(file, scu.presentation_contexts(), never_transcode) + .whatever_context::<_, _>("Could not choose a transfer syntax"); + match r { + Ok((pc, ts)) => { + if verbose { + debug!( + "{}: Selected presentation context: {:?}", + file.file.display(), + pc + ); + } + file.pc_selected = Some(pc); + file.ts_selected = Some(ts); + } + Err(e) => { + error!("{}", Report::from_error(e)); + if fail_first { let _ = scu.abort(); std::process::exit(-2); } } } + } + + let progress_bar; + if !verbose { + progress_bar = Some(ProgressBar::new(dicom_files.len() as u64)); if let Some(pb) = progress_bar.as_ref() { - pb.inc(1) + pb.set_style( + ProgressStyle::default_bar() + .template("[{elapsed_precise}] {bar:40} {pos}/{len} {wide_msg}") + .expect("Invalid progress bar template"), + ); + pb.enable_steady_tick(Duration::new(0, 480_000_000)); }; + } else { + progress_bar = None; + } + + for file in dicom_files { + // TODO + scu = send_file(scu, file, message_id, progress_bar.as_ref(), verbose, fail_first).await?; } if let Some(pb) = progress_bar { pb.finish_with_message("done") }; - scu.release() + scu.release().await .whatever_context("Failed to release SCU association")?; Ok(()) } - fn store_req_command( storage_sop_class_uid: &str, storage_sop_instance_uid: &str, diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs new file mode 100644 index 000000000..d0f2bead7 --- /dev/null +++ b/storescu/src/store_async.rs @@ -0,0 +1,243 @@ +use std::collections::HashSet; + +use dicom_dictionary_std::tags; +use dicom_encoding::TransferSyntaxIndex; +use dicom_object::{open_file, InMemDicomObject}; +use dicom_transfer_syntax_registry::TransferSyntaxRegistry; +use dicom_ul::{pdu::{PDataValue, PDataValueType}, ClientAssociation, ClientAssociationOptions, Pdu}; +use indicatif::ProgressBar; +use snafu::{OptionExt, ResultExt}; +use tokio::io::AsyncWriteExt; +use tracing::{debug, error, info, warn}; + +use crate::{into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, UnsupportedFileTransferSyntaxSnafu}; + +pub async fn get_scu( + addr: String, + calling_ae_title: String, + called_ae_title: Option, + max_pdu_length: u32, + username: Option, + password: Option, + kerberos_service_ticket: Option, + saml_assertion: Option, + jwt: Option, + presentation_contexts: HashSet<(String, String)> +) -> Result { + let mut scu_init = ClientAssociationOptions::new() + .calling_ae_title(calling_ae_title) + .max_pdu_length(max_pdu_length); + + for (storage_sop_class_uid, transfer_syntax) in &presentation_contexts { + scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); + } + + if let Some(called_ae_title) = called_ae_title { + scu_init = scu_init.called_ae_title(called_ae_title); + } + + if let Some(username) = username { + scu_init = scu_init.username(username); + } + + if let Some(password) = password { + scu_init = scu_init.password(password); + } + + if let Some(kerberos_service_ticket) = kerberos_service_ticket { + scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); + } + + if let Some(saml_assertion) = saml_assertion { + scu_init = scu_init.saml_assertion(saml_assertion); + } + + if let Some(jwt) = jwt { + scu_init = scu_init.jwt(jwt); + } + + Ok(scu_init.establish_with(&addr).await.context(InitScuSnafu)?) +} + +pub async fn send_file( + mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool +) -> Result { + if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { + if let Some(pb) = &progress_bar { + pb.set_message(file.sop_instance_uid.clone()); + } + let cmd = store_req_command(&file.sop_class_uid, &file.sop_instance_uid, message_id); + + let mut cmd_data = Vec::with_capacity(128); + cmd.write_dataset_with_ts( + &mut cmd_data, + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), + ) + .map_err(Box::from) + .context(CreateCommandSnafu)?; + + let mut object_data = Vec::with_capacity(2048); + let dicom_file = + open_file(&file.file).whatever_context("Could not open listed DICOM file")?; + let ts_selected = TransferSyntaxRegistry + .get(&ts_uid_selected) + .with_context(|| UnsupportedFileTransferSyntaxSnafu { + uid: ts_uid_selected.to_string(), + })?; + + // transcode file if necessary + let dicom_file = into_ts(dicom_file, ts_selected, verbose)?; + + dicom_file + .write_dataset_with_ts(&mut object_data, ts_selected) + .whatever_context("Could not write object dataset")?; + + let nbytes = cmd_data.len() + object_data.len(); + + if verbose { + info!( + "Sending file {} (~ {} kB), uid={}, sop={}, ts={}", + file.file.display(), + nbytes / 1_000, + &file.sop_instance_uid, + &file.sop_class_uid, + ts_uid_selected, + ); + } + + if nbytes < scu.acceptor_max_pdu_length().saturating_sub(100) as usize { + let pdu = Pdu::PData { + data: vec![ + PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Command, + is_last: true, + data: cmd_data, + }, + PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Data, + is_last: true, + data: object_data, + }, + ], + }; + + scu.send(&pdu) + .await + .whatever_context("Failed to send C-STORE-RQ")?; + } else { + let pdu = Pdu::PData { + data: vec![PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Command, + is_last: true, + data: cmd_data, + }], + }; + + scu.send(&pdu) + .await + .whatever_context("Failed to send C-STORE-RQ command")?; + + { + let mut pdata = scu.send_pdata(pc_selected.id).await; + pdata + .write_all(&object_data) + .await + .whatever_context("Failed to send C-STORE-RQ P-Data")?; + } + } + + if verbose { + debug!("Awaiting response..."); + } + + let rsp_pdu = scu + .receive() + .await + .whatever_context("Failed to receive C-STORE-RSP")?; + + match rsp_pdu { + Pdu::PData { data } => { + let data_value = &data[0]; + + let cmd_obj = InMemDicomObject::read_dataset_with_ts( + &data_value.data[..], + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(), + ) + .whatever_context("Could not read response from SCP")?; + if verbose { + debug!("Full response: {:?}", cmd_obj); + } + let status = cmd_obj + .element(tags::STATUS) + .whatever_context("Could not find status code in response")? + .to_int::() + .whatever_context("Status code in response is not a valid integer")?; + let storage_sop_instance_uid = file + .sop_instance_uid + .trim_end_matches(|c: char| c.is_whitespace() || c == '\0'); + + match status { + // Success + 0 => { + if verbose { + info!("Successfully stored instance {}", storage_sop_instance_uid); + } + } + // Warning + 1 | 0x0107 | 0x0116 | 0xB000..=0xBFFF => { + warn!( + "Possible issue storing instance `{}` (status code {:04X}H)", + storage_sop_instance_uid, status + ); + } + 0xFF00 | 0xFF01 => { + warn!( + "Possible issue storing instance `{}`: status is pending (status code {:04X}H)", + storage_sop_instance_uid, status + ); + } + 0xFE00 => { + error!( + "Could not store instance `{}`: operation cancelled", + storage_sop_instance_uid + ); + if fail_first { + let _ = scu.abort(); + std::process::exit(-2); + } + } + _ => { + error!( + "Failed to store instance `{}` (status code {:04X}H)", + storage_sop_instance_uid, status + ); + if fail_first { + let _ = scu.abort(); + std::process::exit(-2); + } + } + } + } + + pdu @ Pdu::Unknown { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::ReleaseRQ + | pdu @ Pdu::ReleaseRP + | pdu @ Pdu::AbortRQ { .. } => { + error!("Unexpected SCP response: {:?}", pdu); + let _ = scu.abort(); + std::process::exit(-2); + } + } + } + if let Some(pb) = progress_bar.as_ref() { + pb.inc(1) + }; + Ok(scu) +} \ No newline at end of file diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs new file mode 100644 index 000000000..ae0470f69 --- /dev/null +++ b/storescu/src/store_sync.rs @@ -0,0 +1,239 @@ +use std::{collections::HashSet, io::Write}; + +use dicom_dictionary_std::tags; +use dicom_encoding::TransferSyntaxIndex; +use dicom_object::{open_file, InMemDicomObject}; +use dicom_transfer_syntax_registry::TransferSyntaxRegistry; +use dicom_ul::{pdu::{PDataValue, PDataValueType}, ClientAssociation, ClientAssociationOptions, Pdu}; +use indicatif::ProgressBar; +use snafu::{OptionExt, ResultExt}; +use tracing::{debug, error, info, warn}; + +use crate::{into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, UnsupportedFileTransferSyntaxSnafu}; + +pub fn get_scu( + addr: String, + calling_ae_title: String, + called_ae_title: Option, + max_pdu_length: u32, + username: Option, + password: Option, + kerberos_service_ticket: Option, + saml_assertion: Option, + jwt: Option, + presentation_contexts: HashSet<(String, String)> +) -> Result { + let mut scu_init = ClientAssociationOptions::new() + .calling_ae_title(calling_ae_title) + .max_pdu_length(max_pdu_length); + + for (storage_sop_class_uid, transfer_syntax) in &presentation_contexts { + scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); + } + + if let Some(called_ae_title) = called_ae_title { + scu_init = scu_init.called_ae_title(called_ae_title); + } + + if let Some(username) = username { + scu_init = scu_init.username(username); + } + + if let Some(password) = password { + scu_init = scu_init.password(password); + } + + if let Some(kerberos_service_ticket) = kerberos_service_ticket { + scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); + } + + if let Some(saml_assertion) = saml_assertion { + scu_init = scu_init.saml_assertion(saml_assertion); + } + + if let Some(jwt) = jwt { + scu_init = scu_init.jwt(jwt); + } + + Ok(scu_init.establish_with(&addr).context(InitScuSnafu)?) +} + +pub fn send_file( + mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool +) -> Result { + if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { + if let Some(pb) = &progress_bar { + pb.set_message(file.sop_instance_uid.clone()); + } + let cmd = store_req_command(&file.sop_class_uid, &file.sop_instance_uid, message_id); + + let mut cmd_data = Vec::with_capacity(128); + cmd.write_dataset_with_ts( + &mut cmd_data, + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), + ) + .map_err(Box::from) + .context(CreateCommandSnafu)?; + + let mut object_data = Vec::with_capacity(2048); + let dicom_file = + open_file(&file.file).whatever_context("Could not open listed DICOM file")?; + let ts_selected = TransferSyntaxRegistry + .get(&ts_uid_selected) + .with_context(|| UnsupportedFileTransferSyntaxSnafu { + uid: ts_uid_selected.to_string(), + })?; + + // transcode file if necessary + let dicom_file = into_ts(dicom_file, ts_selected, verbose)?; + + dicom_file + .write_dataset_with_ts(&mut object_data, ts_selected) + .whatever_context("Could not write object dataset")?; + + let nbytes = cmd_data.len() + object_data.len(); + + if verbose { + info!( + "Sending file {} (~ {} kB), uid={}, sop={}, ts={}", + file.file.display(), + nbytes / 1_000, + &file.sop_instance_uid, + &file.sop_class_uid, + ts_uid_selected, + ); + } + + if nbytes < scu.acceptor_max_pdu_length().saturating_sub(100) as usize { + let pdu = Pdu::PData { + data: vec![ + PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Command, + is_last: true, + data: cmd_data, + }, + PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Data, + is_last: true, + data: object_data, + }, + ], + }; + + scu.send(&pdu) + .whatever_context("Failed to send C-STORE-RQ")?; + } else { + let pdu = Pdu::PData { + data: vec![PDataValue { + presentation_context_id: pc_selected.id, + value_type: PDataValueType::Command, + is_last: true, + data: cmd_data, + }], + }; + + scu.send(&pdu) + .whatever_context("Failed to send C-STORE-RQ command")?; + + { + let mut pdata = scu.send_pdata(pc_selected.id); + pdata + .write_all(&object_data) + .whatever_context("Failed to send C-STORE-RQ P-Data")?; + } + } + + if verbose { + debug!("Awaiting response..."); + } + + let rsp_pdu = scu + .receive() + .whatever_context("Failed to receive C-STORE-RSP")?; + + match rsp_pdu { + Pdu::PData { data } => { + let data_value = &data[0]; + + let cmd_obj = InMemDicomObject::read_dataset_with_ts( + &data_value.data[..], + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN + .erased(), + ) + .whatever_context("Could not read response from SCP")?; + if verbose { + debug!("Full response: {:?}", cmd_obj); + } + let status = cmd_obj + .element(tags::STATUS) + .whatever_context("Could not find status code in response")? + .to_int::() + .whatever_context("Status code in response is not a valid integer")?; + let storage_sop_instance_uid = file + .sop_instance_uid + .trim_end_matches(|c: char| c.is_whitespace() || c == '\0'); + + match status { + // Success + 0 => { + if verbose { + info!("Successfully stored instance {}", storage_sop_instance_uid); + } + } + // Warning + 1 | 0x0107 | 0x0116 | 0xB000..=0xBFFF => { + warn!( + "Possible issue storing instance `{}` (status code {:04X}H)", + storage_sop_instance_uid, status + ); + } + 0xFF00 | 0xFF01 => { + warn!( + "Possible issue storing instance `{}`: status is pending (status code {:04X}H)", + storage_sop_instance_uid, status + ); + } + 0xFE00 => { + error!( + "Could not store instance `{}`: operation cancelled", + storage_sop_instance_uid + ); + if fail_first { + let _ = scu.abort(); + std::process::exit(-2); + } + } + _ => { + error!( + "Failed to store instance `{}` (status code {:04X}H)", + storage_sop_instance_uid, status + ); + if fail_first { + let _ = scu.abort(); + std::process::exit(-2); + } + } + } + } + + pdu @ Pdu::Unknown { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::ReleaseRQ + | pdu @ Pdu::ReleaseRP + | pdu @ Pdu::AbortRQ { .. } => { + error!("Unexpected SCP response: {:?}", pdu); + let _ = scu.abort(); + std::process::exit(-2); + } + } + } + if let Some(pb) = progress_bar.as_ref() { + pb.inc(1) + }; + Ok(scu) + +} \ No newline at end of file diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index a7a0b7112..4d990e9e9 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -31,7 +31,7 @@ use bytes::Buf; use super::{ //pdata::{PDataReader, PDataWriter}, - uid::trim_uid, PDataReader, PDataWriter, + uid::trim_uid, PDataReader, PDataWriter }; #[derive(Debug, Snafu)] @@ -1186,6 +1186,7 @@ impl ClientAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. + #[cfg(not(feature = "async"))] pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { PDataWriter::new( &mut self.socket, @@ -1194,11 +1195,27 @@ impl ClientAssociation { ) } + /// Prepare a P-Data writer for sending + /// one or more data items. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + #[cfg(feature = "async")] + pub async fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + + PDataWriter::new( + &mut self.socket, + presentation_context_id, + self.acceptor_max_pdu_length, + ) + } + /// 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. + #[cfg(not(feature = "async"))] pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index 94642fb9c..287bb538f 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -22,6 +22,9 @@ mod uid; pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; +#[cfg(not(feature = "async"))] pub use pdata::{PDataReader, PDataWriter}; +#[cfg(feature = "async")] +pub use pdata::{AsyncPDataWriter as PDataWriter, AsyncPDataReader as PDataReader}; pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index aa450727b..6d8f08ae4 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -320,56 +320,64 @@ where } } -// TODO -// #[cfg(feature = "async")] -// impl AsyncWrite for AsyncPDataWriter -// where -// W: AsyncWrite + Unpin, -// { -// fn poll_write( -// mut self: Pin<&mut Self>, -// cx: &mut Context<'_>, -// buf: &[u8], -// ) -> Poll>{ -// let total_len = self.max_data_len as usize + 12; -// if self.buffer.len() + buf.len() <= total_len { -// // accumulate into buffer, do nothing -// self.buffer.extend(buf); -// Poll::Ready(Ok(buf.len())) -// } else { -// // fill in the rest of the buffer, send PDU, -// // and leave out the rest for subsequent writes -// let buf = &buf[..total_len - self.buffer.len()]; -// self.buffer.extend(buf); -// debug_assert_eq!(self.buffer.len(), total_len); -// setup_pdata_header(&mut self.buffer, false); -// let res = Pin::new(&mut self.stream).poll_write(cx, &mut self.buffer); -// match res { -// Poll::Ready(Ok(_)) => { -// self.buffer.truncate(12); -// Poll::Ready(Ok(buf.len())) -// }, -// Poll::Ready(Err(e)) => Poll::Ready(Err(e)), -// Poll::Pending => Poll::Pending -// } -// } +#[cfg(feature = "async")] +impl AsyncWrite for AsyncPDataWriter +where + W: AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll>{ + let total_len = self.max_data_len as usize + 12; + if self.buffer.len() + buf.len() <= total_len { + // accumulate into buffer, do nothing + self.buffer.extend(buf); + Poll::Ready(Ok(buf.len())) + } else { + // fill in the rest of the buffer, send PDU, + // and leave out the rest for subsequent writes + let buf = &buf[..total_len - self.buffer.len()]; + self.buffer.extend(buf); + debug_assert_eq!(self.buffer.len(), total_len); + setup_pdata_header(&mut self.buffer, false); + // Avoid multiple mutable borrows, take self.buffer then return it after poll_write + let data = std::mem::take(&mut self.buffer); + let data_len = data.len(); + let mut position = 0; + while position < data_len { + let res = Pin::new(&mut self.stream) + .poll_write(cx, &data[position..]); + match res { + Poll::Ready(Ok(n)) => { + // Update position + position += n; + }, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending + } + } + self.buffer = Vec::from(&data[..12]); + return Poll::Ready(Ok(position)) + } -// } + } -// fn poll_flush( -// mut self: Pin<&mut Self>, -// cx: &mut Context<'_>, -// ) -> Poll>{ -// Pin::new(&mut self.stream).poll_flush(cx) -// } + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>{ + Pin::new(&mut self.stream).poll_flush(cx) + } -// fn poll_shutdown( -// mut self: Pin<&mut Self>, -// cx: &mut Context<'_>, -// ) -> Poll>{ -// Pin::new(&mut self.stream).poll_shutdown(cx) -// } -// } + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>{ + Pin::new(&mut self.stream).poll_shutdown(cx) + } +} /// With the P-Data writer dropped, /// this `Drop` implementation @@ -519,6 +527,16 @@ where } } +#[cfg(feature = "async")] +#[must_use] +pub struct AsyncPDataReader { + buffer: VecDeque, + stream: R, + presentation_context_id: Option, + max_data_length: u32, + last_pdu: bool, + read_buffer: BytesMut, +} // TODO // #[cfg(feature = "async")] // impl AsyncRead for PDataReader where R: AsyncRead + Unpin { From 63e0736585afede87c6a36a8cb2e0fa20878a3b4 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 11:09:08 -0500 Subject: [PATCH 06/28] MAIN: Cleaning up * Remove unneeded trait bounds on TextCodec * Cleanup some imports * `context` errors instead of unwrapping --- encoding/src/text.rs | 2 +- ul/src/association/client.rs | 12 +++++++++--- ul/src/association/server.rs | 13 +++++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/encoding/src/text.rs b/encoding/src/text.rs index f6ae4840a..6d19cd844 100644 --- a/encoding/src/text.rs +++ b/encoding/src/text.rs @@ -65,7 +65,7 @@ type DecodeResult = Result; /// A holder of encoding and decoding mechanisms for text in DICOM content, /// which according to the standard, depends on the specific character set. -pub trait TextCodec: Send + Sync { +pub trait TextCodec { /// Obtain the defined term (unique name) of the text encoding, /// which may be used as the value of a /// Specific Character Set (0008, 0005) element to refer to this codec. diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 4d990e9e9..8d042c253 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -22,6 +22,7 @@ use crate::{ read_pdu, write_pdu, AbortRQSource, AssociationAC, AssociationRJ, AssociationRQ, Pdu, PresentationContextProposed, PresentationContextResult, PresentationContextResultReason, UserIdentity, UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, + ReadPduSnafu }, AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; @@ -829,7 +830,10 @@ impl<'a> ClientAssociationOptions<'a> { buf.set_position(0) } } - let recv = socket.read_buf(&mut read_buffer).await.unwrap(); + let recv = socket.read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } @@ -1074,7 +1078,6 @@ impl ClientAssociation { pub fn receive(&mut self) -> Result { use std::io::{BufRead, BufReader, Cursor}; - use crate::pdu::ReadPduSnafu; let mut reader = BufReader::new(&mut self.socket); loop { @@ -1119,7 +1122,10 @@ impl ClientAssociation { buf.set_position(0) } } - let recv = self.socket.read_buf(&mut self.read_buffer).await.unwrap(); + let recv = self.socket.read_buf(&mut self.read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index d6470078e..bb4cb2d55 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -22,6 +22,7 @@ use crate::{ AssociationRJ, AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ, Pdu, PresentationContextResult, PresentationContextResultReason, UserIdentity, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, + ReadPduSnafu }, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; @@ -365,7 +366,6 @@ where pub fn establish(&self, mut socket: TcpStream) -> Result { use std::io::{BufRead, BufReader}; - use crate::pdu::ReadPduSnafu; ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, @@ -587,7 +587,10 @@ where buf.set_position(0) } } - let recv = socket.read_buf(&mut read_buffer).await.unwrap(); + let recv = socket.read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } @@ -865,7 +868,6 @@ impl ServerAssociation { pub fn receive(&mut self) -> Result { use std::io::{BufRead, BufReader, Cursor}; - use crate::pdu::ReadPduSnafu; let mut reader = BufReader::new(&mut self.socket); loop { @@ -911,7 +913,10 @@ impl ServerAssociation { buf.set_position(0) } } - let recv = self.socket.read_buf(&mut self.read_buffer).await.unwrap(); + let recv = self.socket.read_buf(&mut self.read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; if recv == 0 { return OtherSnafu{msg: "Connection closed by peer"}.fail(); } From d53c0f2c3e145dccdf857951e705b1714ef7c76b Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 13:54:21 -0500 Subject: [PATCH 07/28] MAIN: Add implementation for AsyncRead to PDataReader --- ul/src/association/mod.rs | 5 +- ul/src/association/pdata.rs | 114 ++++++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index 287bb538f..f88fdcb8d 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -23,8 +23,9 @@ pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; #[cfg(not(feature = "async"))] -pub use pdata::{PDataReader, PDataWriter}; +pub use pdata::PDataWriter; #[cfg(feature = "async")] -pub use pdata::{AsyncPDataWriter as PDataWriter, AsyncPDataReader as PDataReader}; +pub use pdata::AsyncPDataWriter as PDataWriter; pub use server::{ServerAssociation, ServerAssociationOptions}; +pub use pdata::PDataReader; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 6d8f08ae4..f4ce53825 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -441,7 +441,6 @@ pub struct PDataReader { } impl PDataReader -where { pub fn new(stream: R, max_data_length: u32) -> Self { PDataReader { @@ -528,50 +527,77 @@ where } #[cfg(feature = "async")] -#[must_use] -pub struct AsyncPDataReader { - buffer: VecDeque, - stream: R, - presentation_context_id: Option, - max_data_length: u32, - last_pdu: bool, - read_buffer: BytesMut, +impl AsyncRead for PDataReader where R: AsyncRead + Unpin { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll>{ + if self.buffer.is_empty(){ + if self.last_pdu { + // reached the end of PData stream + return Poll::Ready(Ok(())); + } + let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); + let msg = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, self.max_data_length, false) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu + }, + None => { + // Reset position + buf.set_position(0) + } + } + // Do the actual read from the socket + let recv = Pin::new(&mut self.stream) + .poll_read(cx, &mut ReadBuf::new(&mut read_buffer)); + match recv { + Poll::Ready(Ok(())) => { + continue + }, + Poll::Pending => return Poll::Pending, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)) + } + }; + + match msg { + Pdu::PData { data } => { + for pdata_value in data { + self.presentation_context_id = match self.presentation_context_id { + None => Some(pdata_value.presentation_context_id), + Some(cid) if cid == pdata_value.presentation_context_id => Some(cid), + Some(cid) => { + warn!("Received PData value of presentation context {}, but should be {}", pdata_value.presentation_context_id, cid); + Some(cid) + } + }; + self.buffer.extend(pdata_value.data); + self.last_pdu = pdata_value.is_last; + } + } + _ => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Unexpected PDU type", + ))) + } + } + } + // Naive implementation of `Read::read` for VecDeque + while let Some(&byte) = self.buffer.front() { + if buf.remaining() == 0 { + return Poll::Ready(Ok(())); + } + buf.put_slice(&[byte]); + self.buffer.pop_front(); + } + Poll::Ready(Ok(())) + } } -// TODO -// #[cfg(feature = "async")] -// impl AsyncRead for PDataReader where R: AsyncRead + Unpin { -// fn poll_read( -// mut self: Pin<&mut Self>, -// cx: &mut Context<'_>, -// buf: &mut ReadBuf<'_>, -// ) -> Poll>{ -// let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); -// let msg = loop{ -// let mut buf = Cursor::new(&read_buffer[..]); -// match read_pdu(&mut buf, self.max_data_length, false) -// .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { -// Some(pdu) => { -// read_buffer.advance(buf.position() as usize); -// break pdu -// }, -// None => { -// // Reset position -// buf.set_position(0) -// } -// } -// match Pin::new(&mut self.stream).poll_read(cx, &mut ReadBuf::new(read_buffer.as_mut())){ -// Poll::Pending => return Poll::Pending, -// Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), -// Poll::Ready(Ok(_)) => return Poll::Ready(Err(std::io::Error::new( -// std::io::ErrorKind::Other, "Connection closed by peer" -// ))), -// Poll::Ready(Ok(_)) => {} -// } -// } -// Poll::Ready(Ok(())) -// } - -// } /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). From 876aac9f43927a1f4010a23e457c17d8a7ed0bf8 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 13:54:59 -0500 Subject: [PATCH 08/28] MAIN: Enumerate specific needed features --- ul/Cargo.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index d4fa30a28..6fae77455 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -16,9 +16,18 @@ bytes = "1.6.1" dicom-encoding = { path = "../encoding/", version = "0.7.0" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.0", default-features = false } snafu = "0.8" -tokio = { version = "1.38.0", features = ["full"], optional = true } tracing = "0.1.34" +[dependencies.tokio] +version = "1.38.0" +optional = true +features = [ + "rt", + "rt-multi-thread", + "net", + "io-util" +] + [dev-dependencies] matches = "0.1.8" From dfd0fada0ed7af13e3fd39e2b020814dcc06771d Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 14:14:37 -0500 Subject: [PATCH 09/28] MAIN: Resolve unused imports and format code --- ul/src/association/client.rs | 111 +++++++-------- ul/src/association/mod.rs | 7 +- ul/src/association/pdata.rs | 83 ++++++----- ul/src/association/server.rs | 138 +++++++++--------- ul/src/pdu/mod.rs | 4 +- ul/src/pdu/reader.rs | 268 ++++++++++++++++++++++++----------- ul/tests/pdu.rs | 11 +- 7 files changed, 358 insertions(+), 264 deletions(-) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 8d042c253..83f721886 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -4,25 +4,19 @@ //! in which this application entity is the one requesting the association. //! See [`ClientAssociationOptions`] //! for details and examples on how to create an association. -use std::{borrow::Cow, io::Cursor, convert::TryInto, net::ToSocketAddrs, time::Duration}; -#[cfg(not(feature = "async"))] -use std::{ - io::{Read, Write}, - net::TcpStream, -}; use bytes::BytesMut; +use std::{borrow::Cow, convert::TryInto, io::Cursor, net::ToSocketAddrs, time::Duration}; +#[cfg(not(feature = "async"))] +use std::{io::Write, net::TcpStream}; #[cfg(feature = "async")] -use tokio::{ - io::{AsyncRead, AsyncWriteExt}, - net::TcpStream, -}; +use tokio::{io::AsyncWriteExt, net::TcpStream}; use crate::{ pdu::{ read_pdu, write_pdu, AbortRQSource, AssociationAC, AssociationRJ, AssociationRQ, Pdu, PresentationContextProposed, PresentationContextResult, PresentationContextResultReason, - UserIdentity, UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, - ReadPduSnafu + ReadPduSnafu, UserIdentity, UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, + MAXIMUM_PDU_SIZE, }, AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; @@ -32,7 +26,9 @@ use bytes::Buf; use super::{ //pdata::{PDataReader, PDataWriter}, - uid::trim_uid, PDataReader, PDataWriter + uid::trim_uid, + PDataReader, + PDataWriter, }; #[derive(Debug, Snafu)] @@ -128,10 +124,8 @@ pub enum Error { #[snafu(backtrace)] source: crate::pdu::ReadError, }, - #[snafu(display("Other error: {}", msg))] - Other{ - msg: String - } + #[snafu(display("Connection closed by peer"))] + ConnectionClosed, } pub type Result = std::result::Result; @@ -516,8 +510,6 @@ impl<'a> ClientAssociationOptions<'a> { { use std::io::{BufRead, BufReader}; - use crate::pdu::ReadPduSnafu; - let ClientAssociationOptions { calling_ae_title, called_ae_title, @@ -617,20 +609,22 @@ impl<'a> ClientAssociationOptions<'a> { match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { Some(pdu) => { read_buffer.advance(buf.position() as usize); - break pdu - }, + break pdu; + } None => { // Reset position buf.set_position(0) } } // Use BufReader to get similar behavior to AsyncRead read_buf - let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + let recv = reader + .fill_buf() + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + .to_vec(); reader.consume(recv.len()); read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } + ensure!(recv.len() > 0, ConnectionClosedSnafu); }; match msg { @@ -689,7 +683,7 @@ impl<'a> ClientAssociationOptions<'a> { buffer, strict, read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), - timeout + timeout, }) } Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), @@ -810,8 +804,7 @@ impl<'a> ClientAssociationOptions<'a> { let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); // send request - write_pdu(&mut buffer, &msg) - .context(SendRequestSnafu)?; + write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; buffer.clear(); // receive response @@ -823,20 +816,19 @@ impl<'a> ClientAssociationOptions<'a> { match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { Some(pdu) => { read_buffer.advance(buf.position() as usize); - break pdu - }, + break pdu; + } None => { // Reset position buf.set_position(0) } } - let recv = socket.read_buf(&mut read_buffer) + let recv = socket + .read_buf(&mut read_buffer) .await .context(ReadPduSnafu) .context(ReceiveSnafu)?; - if recv == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } + ensure!(recv > 0, ConnectionClosedSnafu); }; match msg { @@ -1017,7 +1009,7 @@ pub struct ClientAssociation { /// Send/Receive operation timeout timeout: Option, /// Buffer to assemble PDU before parsing - read_buffer: BytesMut + read_buffer: BytesMut, } impl ClientAssociation { @@ -1082,24 +1074,27 @@ impl ClientAssociation { loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { + match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict) + .context(ReceiveResponseSnafu)? + { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); - return Ok(pdu) - }, + return Ok(pdu); + } None => { // Reset position buf.set_position(0) } } // Use BufReader to get similar behavior to AsyncRead read_buf - let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + let recv = reader + .fill_buf() + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + .to_vec(); reader.consume(recv.len()); self.read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } - + ensure!(recv.len() > 0, ConnectionClosedSnafu); } } #[cfg(feature = "async")] @@ -1107,28 +1102,29 @@ impl ClientAssociation { pub async fn receive(&mut self) -> Result { use std::io::Cursor; - use bytes::Buf; use tokio::io::AsyncReadExt; loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveResponseSnafu)? { + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) + .context(ReceiveResponseSnafu)? + { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); - return Ok(pdu) - }, + return Ok(pdu); + } None => { // Reset position buf.set_position(0) } } - let recv = self.socket.read_buf(&mut self.read_buffer) + let recv = self + .socket + .read_buf(&mut self.read_buffer) .await .context(ReadPduSnafu) .context(ReceiveSnafu)?; - if recv == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } + ensure!(recv > 0, ConnectionClosedSnafu); } } @@ -1208,7 +1204,6 @@ impl ClientAssociation { /// splits the inner data into separate PDUs if necessary. #[cfg(feature = "async")] pub async fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - PDataWriter::new( &mut self.socket, presentation_context_id, @@ -1264,11 +1259,15 @@ impl ClientAssociation { let pdu = loop { if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { - break pdu - } - if 0 == self.socket.read_buf(&mut read_buffer).await.unwrap() { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); + break pdu; } + let recv = self + .socket + .read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); }; match pdu { Pdu::ReleaseRP => {} diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index f88fdcb8d..c2f9d1516 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -22,10 +22,9 @@ mod uid; pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; -#[cfg(not(feature = "async"))] -pub use pdata::PDataWriter; #[cfg(feature = "async")] pub use pdata::AsyncPDataWriter as PDataWriter; -pub use server::{ServerAssociation, ServerAssociationOptions}; pub use pdata::PDataReader; - +#[cfg(not(feature = "async"))] +pub use pdata::PDataWriter; +pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index f4ce53825..7db199b27 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,16 +1,21 @@ +#[cfg(not(feature = "async"))] +use std::io::Write; use std::{ - collections::VecDeque, io::{BufRead, BufReader, Cursor, Read, Write} + collections::VecDeque, + io::{BufRead, BufReader, Cursor, Read}, }; use bytes::{Buf, BytesMut}; -use snafu::ResultExt; #[cfg(feature = "async")] -use tokio::io::ReadBuf; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; #[cfg(feature = "async")] -use std::{pin::Pin, task::{Context, Poll}}; +use tokio::io::ReadBuf; use tracing::warn; -use crate::{pdu::{ReadPduSnafu, PDU_HEADER_SIZE}, read_pdu, Pdu}; +use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; #[cfg(feature = "async")] use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; @@ -41,7 +46,6 @@ fn setup_pdata_header(buffer: &mut Vec, is_last: bool) { buffer[11] = if is_last { 0x02 } else { 0x00 }; } - /// A P-Data value writer. /// /// This exposes an API to iteratively construct and send Data messages @@ -85,6 +89,7 @@ fn setup_pdata_header(buffer: &mut Vec, is_last: bool) { /// let pdu_ac = association.receive()?; /// # Ok(()) /// # } +#[cfg(not(feature = "async"))] #[must_use] pub struct PDataWriter { buffer: Vec, @@ -92,6 +97,7 @@ pub struct PDataWriter { max_data_len: u32, } +#[cfg(not(feature = "async"))] impl PDataWriter where W: Write, @@ -168,6 +174,7 @@ where } } +#[cfg(not(feature = "async"))] impl Write for PDataWriter where W: Write, @@ -199,6 +206,7 @@ where /// this `Drop` implementation /// will construct and emit the last P-Data fragment PDU /// if there is any data left to send. +#[cfg(not(feature = "async"))] impl Drop for PDataWriter where W: Write, @@ -329,7 +337,7 @@ where mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], - ) -> Poll>{ + ) -> Poll> { let total_len = self.max_data_len as usize + 12; if self.buffer.len() + buf.len() <= total_len { // accumulate into buffer, do nothing @@ -347,34 +355,32 @@ where let data_len = data.len(); let mut position = 0; while position < data_len { - let res = Pin::new(&mut self.stream) - .poll_write(cx, &data[position..]); + let res = Pin::new(&mut self.stream).poll_write(cx, &data[position..]); match res { Poll::Ready(Ok(n)) => { // Update position position += n; - }, + } Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending + Poll::Pending => return Poll::Pending, } } self.buffer = Vec::from(&data[..12]); - return Poll::Ready(Ok(position)) + return Poll::Ready(Ok(position)); } - } fn poll_flush( mut self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>{ + ) -> Poll> { Pin::new(&mut self.stream).poll_flush(cx) } fn poll_shutdown( mut self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>{ + ) -> Poll> { Pin::new(&mut self.stream).poll_shutdown(cx) } } @@ -440,8 +446,7 @@ pub struct PDataReader { read_buffer: BytesMut, } -impl PDataReader -{ +impl PDataReader { pub fn new(stream: R, max_data_length: u32) -> Self { PDataReader { buffer: VecDeque::with_capacity(max_data_length as usize), @@ -476,14 +481,15 @@ where } let mut reader = BufReader::new(&mut self.stream); - let msg = loop{ + let msg = loop { let mut buf = Cursor::new(&self.read_buffer[..]); match read_pdu(&mut buf, self.max_data_length, false) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + { Some(pdu) => { - self.read_buffer.advance(buf.position() as usize); - break pdu - }, + self.read_buffer.advance(buf.position() as usize); + break pdu; + } None => { // Reset position buf.set_position(0) @@ -494,7 +500,8 @@ where self.read_buffer.extend_from_slice(&recv); if recv.len() == 0 { return Err(std::io::Error::new( - std::io::ErrorKind::Other, "Connection closed by peer" + std::io::ErrorKind::Other, + "Connection closed by peer", )); } }; @@ -527,13 +534,16 @@ where } #[cfg(feature = "async")] -impl AsyncRead for PDataReader where R: AsyncRead + Unpin { +impl AsyncRead for PDataReader +where + R: AsyncRead + Unpin, +{ fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, - ) -> Poll>{ - if self.buffer.is_empty(){ + ) -> Poll> { + if self.buffer.is_empty() { if self.last_pdu { // reached the end of PData stream return Poll::Ready(Ok(())); @@ -542,28 +552,27 @@ impl AsyncRead for PDataReader where R: AsyncRead + Unpin { let msg = loop { let mut buf = Cursor::new(&read_buffer[..]); match read_pdu(&mut buf, self.max_data_length, false) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + { Some(pdu) => { read_buffer.advance(buf.position() as usize); - break pdu - }, + break pdu; + } None => { // Reset position buf.set_position(0) } } // Do the actual read from the socket - let recv = Pin::new(&mut self.stream) - .poll_read(cx, &mut ReadBuf::new(&mut read_buffer)); + let recv = + Pin::new(&mut self.stream).poll_read(cx, &mut ReadBuf::new(&mut read_buffer)); match recv { - Poll::Ready(Ok(())) => { - continue - }, + Poll::Ready(Ok(())) => continue, Poll::Pending => return Poll::Pending, - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)) + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), } }; - + match msg { Pdu::PData { data } => { for pdata_value in data { @@ -613,7 +622,7 @@ mod tests { use std::collections::VecDeque; use std::io::{Read, Write}; - use crate::pdu::{read_pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE, Pdu}; + use crate::pdu::{read_pdu, Pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE}; use crate::pdu::{PDataValue, PDataValueType}; use crate::write_pdu; diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index bb4cb2d55..b2db6ac2e 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -4,14 +4,13 @@ //! in which this application entity listens to incoming association requests. //! See [`ServerAssociationOptions`] //! for details and examples on how to create an association. -use bytes::{BytesMut, Buf}; +use bytes::{Buf, BytesMut}; use std::{borrow::Cow, io::Cursor}; #[cfg(not(feature = "async"))] use std::{io::Write, net::TcpStream}; #[cfg(feature = "async")] use tokio::{io::AsyncWriteExt, net::TcpStream}; - use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use snafu::{ensure, Backtrace, ResultExt, Snafu}; @@ -21,16 +20,12 @@ use crate::{ read_pdu, write_pdu, AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ, AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ, Pdu, PresentationContextResult, PresentationContextResultReason, - UserIdentity, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, - ReadPduSnafu + ReadPduSnafu, UserIdentity, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, }, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; -use super::{ - pdata::{PDataReader, PDataWriter}, - uid::trim_uid, -}; +use super::{uid::trim_uid, PDataReader, PDataWriter}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -94,9 +89,8 @@ pub enum Error { ))] #[non_exhaustive] SendTooLongPdu { length: usize, backtrace: Backtrace }, - Other{ - msg: String - } + #[snafu(display("Connection closed by peer"))] + ConnectionClosed, } pub type Result = std::result::Result; @@ -366,7 +360,6 @@ where pub fn establish(&self, mut socket: TcpStream) -> Result { use std::io::{BufRead, BufReader}; - ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, MissingAbstractSyntaxSnafu @@ -382,21 +375,22 @@ where match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { Some(pdu) => { read_buffer.advance(buf.position() as usize); - break pdu - }, + break pdu; + } None => { // Reset position buf.set_position(0) } } // Use BufReader to get similar behavior to AsyncRead read_buf - let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + let recv = reader + .fill_buf() + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + .to_vec(); reader.consume(recv.len()); read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } - + ensure!(recv.len() > 0, ConnectionClosedSnafu); }; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); match msg { @@ -571,7 +565,6 @@ where ); let max_pdu_length = self.max_pdu_length; - use bytes::BytesMut; use tokio::io::AsyncReadExt; let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); @@ -580,20 +573,19 @@ where match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { Some(pdu) => { read_buffer.advance(buf.position() as usize); - break pdu - }, + break pdu; + } None => { // Reset position buf.set_position(0) } } - let recv = socket.read_buf(&mut read_buffer) + let recv = socket + .read_buf(&mut read_buffer) .await .context(ReadPduSnafu) .context(ReceiveSnafu)?; - if recv == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } + ensure!(recv > 0, ConnectionClosedSnafu); }; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); @@ -636,35 +628,33 @@ where return RejectedSnafu.fail(); } - match self.ae_access_control - .check_access( - &self.ae_title, - &calling_ae_title, - &called_ae_title, - user_variables - .iter() - .find_map(|user_variable| match user_variable { - UserVariableItem::UserIdentityItem(user_identity) => { - Some(user_identity) - } - _ => None, + match self.ae_access_control.check_access( + &self.ae_title, + &calling_ae_title, + &called_ae_title, + user_variables + .iter() + .find_map(|user_variable| match user_variable { + UserVariableItem::UserIdentityItem(user_identity) => { + Some(user_identity) + } + _ => None, + }), + ) { + Ok(()) => {} + Err(reason) => { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser(reason), }), - ){ - Ok(()) => {}, - Err(reason) => { - write_pdu( - &mut buffer, - &Pdu::AssociationRJ(AssociationRJ { - result: AssociationRJResult::Permanent, - source: AssociationRJSource::ServiceUser(reason), - }), - ) - .context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - return Err(RejectedSnafu.build()); - - } + ) + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return Err(RejectedSnafu.build()); } + } // fetch requested maximum PDU length let requestor_max_pdu_length = user_variables @@ -745,12 +735,11 @@ where client_ae_title: calling_ae_title, buffer, strict: self.strict, - read_buffer: bytes::BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize) + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), }) } Pdu::ReleaseRQ => { - write_pdu(&mut buffer, &Pdu::ReleaseRP) - .context(SendResponseSnafu)?; + write_pdu(&mut buffer, &Pdu::ReleaseRP).context(SendResponseSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; AbortedSnafu.fail() } @@ -818,7 +807,7 @@ pub struct ServerAssociation { /// whether to receive PDUs in strict mode strict: bool, /// Read buffer from the socket - read_buffer: bytes::BytesMut + read_buffer: bytes::BytesMut, } impl ServerAssociation { @@ -872,24 +861,27 @@ impl ServerAssociation { loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + match read_pdu(&mut buf, self.acceptor_max_pdu_length, self.strict) + .context(ReceiveRequestSnafu)? + { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); - return Ok(pdu) - }, + return Ok(pdu); + } None => { // Reset position buf.set_position(0) } } // Use BufReader to get similar behavior to AsyncRead read_buf - let recv = reader.fill_buf().context(ReadPduSnafu).context(ReceiveSnafu)?.to_vec(); + let recv = reader + .fill_buf() + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + .to_vec(); reader.consume(recv.len()); self.read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } - + ensure!(recv.len() > 0, ConnectionClosedSnafu); } } @@ -903,23 +895,25 @@ impl ServerAssociation { loop { let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict).context(ReceiveRequestSnafu)? { + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) + .context(ReceiveRequestSnafu)? + { Some(pdu) => { self.read_buffer.advance(buf.position() as usize); - return Ok(pdu) - }, + return Ok(pdu); + } None => { // Reset position buf.set_position(0) } } - let recv = self.socket.read_buf(&mut self.read_buffer) + let recv = self + .socket + .read_buf(&mut self.read_buffer) .await .context(ReadPduSnafu) .context(ReceiveSnafu)?; - if recv == 0 { - return OtherSnafu{msg: "Connection closed by peer"}.fail(); - } + ensure!(recv > 0, ConnectionClosedSnafu); } } diff --git a/ul/src/pdu/mod.rs b/ul/src/pdu/mod.rs index 00ba5a2d4..0deebe251 100644 --- a/ul/src/pdu/mod.rs +++ b/ul/src/pdu/mod.rs @@ -10,8 +10,8 @@ pub mod writer; use std::fmt::Display; pub use reader::read_pdu; -pub use writer::{write_pdu, WriteChunkError}; use snafu::{Backtrace, Snafu}; +pub use writer::{write_pdu, WriteChunkError}; /// The default maximum PDU size pub const DEFAULT_MAX_PDU: u32 = 16_384; @@ -74,7 +74,7 @@ pub enum ReadError { #[snafu(display("No PDU available"))] NoPduAvailable { backtrace: Backtrace }, - #[snafu(display("Could not read PDU"),visibility(pub(crate)))] + #[snafu(display("Could not read PDU"), visibility(pub(crate)))] ReadPdu { source: std::io::Error, backtrace: Backtrace, diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index b8a8354da..e65ba1159 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -1,14 +1,13 @@ /// PDU reader module use crate::pdu::*; +use bytes::Buf; use dicom_encoding::text::{DefaultCharacterSetCodec, TextCodec}; use snafu::{ensure, OptionExt, ResultExt}; use tracing::warn; -use bytes::Buf; pub type Result = std::result::Result; -pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result> -{ +pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result> { ensure!( (MINIMUM_PDU_SIZE..=MAXIMUM_PDU_SIZE).contains(&max_pdu_length), InvalidMaxPduSnafu { max_pdu_length } @@ -52,7 +51,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< max_pdu_length ); } - if buf.remaining() < pdu_length as usize { return Ok(None); } + if buf.remaining() < pdu_length as usize { + return Ok(None); + } let mut bytes = buf.copy_to_bytes(pdu_length as usize); let codec = DefaultCharacterSetCodec; @@ -69,12 +70,16 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // Version 1 and shall be identified with bit 0 set. A receiver of this PDU // implementing only this version of the DICOM UL protocol shall only test that bit // 0 is set. - if bytes.remaining() < 2 { return Ok(None) } + if bytes.remaining() < 2 { + return Ok(None); + } let protocol_version = bytes.get_u16(); // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not // tested to this value when received. - if bytes.remaining() < 2 { return Ok(None) } + if bytes.remaining() < 2 { + return Ok(None); + } bytes.get_u16(); // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be encoded @@ -126,10 +131,10 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< } Some(var_item) => { return InvalidPduVariableSnafu { var_item }.fail(); - }, + } None => { println!("PDU variable none"); - return Ok(None) + return Ok(None); } } } @@ -156,12 +161,16 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // Version 1 and shall be identified with bit 0 set. A receiver of this PDU // implementing only this version of the DICOM UL protocol shall only test that bit // 0 is set. - if bytes.remaining() < 2 { return Ok(None) } + if bytes.remaining() < 2 { + return Ok(None); + } let protocol_version = bytes.get_u16(); // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not // tested to this value when received. - if bytes.remaining() < 2 { return Ok(None) } + if bytes.remaining() < 2 { + return Ok(None); + } bytes.get_u16(); // 11-26 - Reserved - This reserved field shall be sent with a value identical to @@ -210,8 +219,8 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< } Some(var_item) => { return InvalidPduVariableSnafu { var_item }.fail(); - }, - None => return Ok(None) + } + None => return Ok(None), } } @@ -230,14 +239,18 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - if bytes.remaining() < 1 { return Ok(None) } + if bytes.remaining() < 1 { + return Ok(None); + } bytes.get_u8(); // 8 - Result - This Result field shall contain an integer value encoded as an unsigned // binary number. One of the following values shall be used: // 1 - rejected-permanent // 2 - rejected-transient - if bytes.remaining() < 1 { return Ok(None) } + if bytes.remaining() < 1 { + return Ok(None); + } let result = AssociationRJResult::from(bytes.get_u8()) .context(InvalidRejectSourceOrReasonSnafu)?; @@ -262,12 +275,11 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 1 - temporary-congestio // 2 - local-limit-exceeded // 3-7 - reserved - if bytes.remaining() < 2 { return Ok(None) } - let source = AssociationRJSource::from( - bytes.get_u8(), - bytes.get_u8() - ) - .context(InvalidRejectSourceOrReasonSnafu)?; + if bytes.remaining() < 2 { + return Ok(None); + } + let source = AssociationRJSource::from(bytes.get_u8(), bytes.get_u8()) + .context(InvalidRejectSourceOrReasonSnafu)?; Ok(Some(Pdu::AssociationRJ(AssociationRJ { result, source }))) } @@ -284,7 +296,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 1-4 - Item-length - This Item-length shall be the number of bytes from the first // byte of the following field to the last byte of the Presentation-data-value // field. It shall be encoded as an unsigned binary number. - if bytes.remaining() < 4 { return Ok(None) } + if bytes.remaining() < 4 { + return Ok(None); + } let item_length = bytes.get_u32(); ensure!( @@ -297,7 +311,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd // integers between 1 and 255, encoded as an unsigned binary number. For a complete // description of the use of this field see Section 7.1.1.13. - if bytes.remaining() < 1 { return Ok(None) } + if bytes.remaining() < 1 { + return Ok(None); + } let presentation_context_id = bytes.get_u8(); // 6-xxx - Presentation-data-value - This Presentation-data-value field shall @@ -313,7 +329,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // following fragment shall contain the last fragment of a Message Data Set or of a // Message Command. If bit 1 is set to 0, the following fragment // does not contain the last fragment of a Message Data Set or of a Message Command. - if bytes.remaining() < 1 { return Ok(None) } + if bytes.remaining() < 1 { + return Ok(None); + } let header = bytes.get_u8(); let value_type = if header & 0x01 > 0 { @@ -322,7 +340,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< PDataValueType::Data }; let is_last = (header & 0x02) > 0; - if bytes.remaining() < (item_length - 2) as usize { return Ok(None) } + if bytes.remaining() < (item_length - 2) as usize { + return Ok(None); + } values.push(PDataValue { presentation_context_id, value_type, @@ -347,7 +367,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // 7-10 - Reserved - This reserved field shall be sent with a value 00000000H but not // tested to this value when received. - if bytes.remaining() < 4 { return Ok(None) } + if bytes.remaining() < 4 { + return Ok(None); + } bytes.advance(4); Ok(Some(Pdu::ReleaseRP)) @@ -359,7 +381,9 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // this value when received. // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to // this value when received. - if bytes.remaining() < 2 { return Ok(None) } + if bytes.remaining() < 2 { + return Ok(None); + } let _ = bytes.copy_to_bytes(2); // 9 - Source - This Source field shall contain an integer value encoded as an unsigned @@ -376,40 +400,48 @@ pub fn read_pdu(mut buf: impl Buf, max_pdu_length: u32, strict: bool) -> Result< // - 4 - unrecognized-PDU parameter // - 5 - unexpected-PDU parameter // - 6 - invalid-PDU-parameter value - if bytes.remaining() < 2 { return Ok(None) } - let source = AbortRQSource::from( - bytes.get_u8(), - bytes.get_u8() - ) - .context(InvalidAbortSourceOrReasonSnafu)?; + if bytes.remaining() < 2 { + return Ok(None); + } + let source = AbortRQSource::from(bytes.get_u8(), bytes.get_u8()) + .context(InvalidAbortSourceOrReasonSnafu)?; Ok(Some(Pdu::AbortRQ { source })) } _ => { - if bytes.remaining() < pdu_length as usize {return Ok(None);} - Ok(Some(Pdu::Unknown { - pdu_type, - data: bytes.copy_to_bytes(pdu_length as usize).to_vec() + if bytes.remaining() < pdu_length as usize { + return Ok(None); + } + Ok(Some(Pdu::Unknown { + pdu_type, + data: bytes.copy_to_bytes(pdu_length as usize).to_vec(), })) } } } -fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result> -{ +fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result> { // 1 - Item-type - XXH - if buf.remaining() < 1 { return Ok(None); } + if buf.remaining() < 1 { + return Ok(None); + } let item_type = buf.get_u8(); // 2 - Reserved - if buf.remaining() < 1 { return Ok(None); } + if buf.remaining() < 1 { + return Ok(None); + } buf.get_u8(); // 3-4 - Item-length - if buf.remaining() < 2 { return Ok(None); } + if buf.remaining() < 2 { + return Ok(None); + } let item_length = buf.get_u16(); - if buf.remaining() < item_length as usize { return Ok(None); } + if buf.remaining() < item_length as usize { + return Ok(None); + } let mut bytes = buf.copy_to_bytes(item_length as usize); match item_type { 0x10 => { @@ -420,11 +452,9 @@ fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result { @@ -436,22 +466,30 @@ fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result Result Result Result Result Result Result Result { - if bytes.remaining() < item_length as usize { return Ok(None); } + if bytes.remaining() < item_length as usize { + return Ok(None); + } transfer_syntax = Some( codec .decode(bytes.copy_to_bytes(item_length as usize).as_ref()) @@ -632,17 +696,23 @@ fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result Result { // Implementation Class UID Sub-Item Structure @@ -670,7 +740,9 @@ fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result Result Result Result Result Result { - if bytes.remaining() < item_length as usize { return Ok(None); } + if bytes.remaining() < item_length as usize { + return Ok(None); + } user_variables.push(UserVariableItem::Unknown( item_type, - bytes.copy_to_bytes(item_length as usize).to_vec() + bytes.copy_to_bytes(item_length as usize).to_vec(), )); } } diff --git a/ul/tests/pdu.rs b/ul/tests/pdu.rs index 037436fb0..a28121cb6 100644 --- a/ul/tests/pdu.rs +++ b/ul/tests/pdu.rs @@ -2,7 +2,7 @@ use dicom_ul::pdu::reader::read_pdu; use dicom_ul::pdu::writer::write_pdu; use dicom_ul::pdu::{ AssociationRQ, PDataValue, PDataValueType, Pdu, PresentationContextProposed, UserIdentity, - UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU + UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, }; use matches::matches; use std::io::Cursor; @@ -46,8 +46,7 @@ fn can_read_write_associate_rq() -> Result<(), Box> { let mut bytes = vec![0u8; 0]; write_pdu(&mut bytes, &association_rq.into())?; - let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)? - .unwrap(); + let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)?.unwrap(); if let Pdu::AssociationRQ(AssociationRQ { protocol_version, @@ -135,8 +134,7 @@ fn can_read_write_primary_field_only_user_identity() -> Result<(), Box Result<(), Box> { let mut bytes = Vec::new(); write_pdu(&mut bytes, &pdata_rq)?; - let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)? - .unwrap(); + let result = read_pdu(&mut Cursor::new(&bytes), DEFAULT_MAX_PDU, true)?.unwrap(); if let Pdu::PData { data } = result { assert_eq!(data.len(), 1); From 5334e2df8b238fe0712feb2a8a1e20cdb55f3932 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 16:06:27 -0500 Subject: [PATCH 10/28] MAIN: Fix implementation of poll_read and poll_write --- ul/Cargo.toml | 1 + ul/src/association/pdata.rs | 287 +++++++++++++++++++++++++++++------- 2 files changed, 234 insertions(+), 54 deletions(-) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index 6fae77455..c76646589 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -30,6 +30,7 @@ features = [ [dev-dependencies] matches = "0.1.8" +tokio = { version = "1.38.0", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } [features] async = ["dep:tokio"] diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 7db199b27..5a50fac35 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,18 +1,17 @@ #[cfg(not(feature = "async"))] use std::io::Write; use std::{ - collections::VecDeque, - io::{BufRead, BufReader, Cursor, Read}, + collections::VecDeque, future::Future, io::{BufRead, BufReader, Cursor, Read}, task::ready }; -use bytes::{Buf, BytesMut}; +use bytes::{Buf, BufMut, BytesMut}; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; #[cfg(feature = "async")] -use tokio::io::ReadBuf; +use tokio::io::{ReadBuf, AsyncReadExt}; use tracing::warn; use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; @@ -326,6 +325,22 @@ where } Ok(()) } + + /// Use the current state of the buffer to send new PDUs + /// + /// Pre-condition: + /// buffer must have enough data for one P-Data-tf PDU + async fn dispatch_pdu(&mut self) -> std::io::Result<()> { + debug_assert!(self.buffer.len() >= 12); + // send PDU now + setup_pdata_header(&mut self.buffer, false); + self.stream.write_all(&self.buffer).await?; + + // back to just the header + self.buffer.truncate(12); + + Ok(()) + } } #[cfg(feature = "async")] @@ -349,24 +364,13 @@ where let buf = &buf[..total_len - self.buffer.len()]; self.buffer.extend(buf); debug_assert_eq!(self.buffer.len(), total_len); - setup_pdata_header(&mut self.buffer, false); - // Avoid multiple mutable borrows, take self.buffer then return it after poll_write - let data = std::mem::take(&mut self.buffer); - let data_len = data.len(); - let mut position = 0; - while position < data_len { - let res = Pin::new(&mut self.stream).poll_write(cx, &data[position..]); - match res { - Poll::Ready(Ok(n)) => { - // Update position - position += n; - } - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending, - } + let dispatch = self.dispatch_pdu(); + tokio::pin!(dispatch); + match dispatch.poll(cx){ + Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, } - self.buffer = Vec::from(&data[..12]); - return Poll::Ready(Ok(position)); } } @@ -534,42 +538,36 @@ where } #[cfg(feature = "async")] -impl AsyncRead for PDataReader +impl PDataReader where R: AsyncRead + Unpin, { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if self.buffer.is_empty() { - if self.last_pdu { - // reached the end of PData stream - return Poll::Ready(Ok(())); - } - let mut read_buffer = BytesMut::with_capacity(self.max_data_length as usize); + fn poll_fill_buf(&mut self, cx: &mut Context<'_>) -> Poll> { + use std::task::ready; + use tokio::io::{AsyncBufRead, AsyncBufReadExt}; + if self.buffer.is_empty() && !self.last_pdu { + let mut reader = tokio::io::BufReader::new(&mut self.stream); let msg = loop { - let mut buf = Cursor::new(&read_buffer[..]); + let mut buf = std::io::Cursor::new(&self.read_buffer[..]); match read_pdu(&mut buf, self.max_data_length, false) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { Some(pdu) => { - read_buffer.advance(buf.position() as usize); + self.read_buffer.advance(buf.position() as usize); break pdu; } None => { - // Reset position - buf.set_position(0) + buf.set_position(0); } } - // Do the actual read from the socket - let recv = - Pin::new(&mut self.stream).poll_read(cx, &mut ReadBuf::new(&mut read_buffer)); - match recv { - Poll::Ready(Ok(())) => continue, - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + let recv = ready!(Pin::new(&mut reader).poll_fill_buf(cx))?.to_vec(); + reader.consume(recv.len()); + self.read_buffer.extend_from_slice(&recv); + if recv.len() == 0 { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Connection closed by peer", + ))); } }; @@ -592,21 +590,39 @@ where return Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "Unexpected PDU type", - ))) + ))); } } } - // Naive implementation of `Read::read` for VecDeque - while let Some(&byte) = self.buffer.front() { - if buf.remaining() == 0 { - return Poll::Ready(Ok(())); - } - buf.put_slice(&[byte]); - self.buffer.pop_front(); + Poll::Ready(Ok(())) + } + + fn read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> usize { + let len = std::cmp::min(self.buffer.len(), buf.remaining()); + for _ in 0..len { + buf.put_u8(self.buffer.pop_front().unwrap()); } + len + } +} + +#[cfg(feature = "async")] +impl AsyncRead for PDataReader +where + R: AsyncRead + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf + ) -> Poll> { + use std::task::ready; + ready!(self.poll_fill_buf(cx))?; + let _ = self.read_buffer(buf); Poll::Ready(Ok(())) } } + /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). @@ -619,15 +635,19 @@ fn calculate_max_data_len_single(pdu_len: u32) -> u32 { #[cfg(test)] mod tests { - use std::collections::VecDeque; + #[cfg(feature = "async")] + use tokio::{self, io::{AsyncWriteExt, AsyncReadExt}}; + #[cfg(not(feature = "async"))] use std::io::{Read, Write}; use crate::pdu::{read_pdu, Pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE}; use crate::pdu::{PDataValue, PDataValueType}; use crate::write_pdu; + use crate::association::PDataWriter; - use super::{PDataReader, PDataWriter}; + use super::PDataReader; + #[cfg(not(feature = "async"))] #[test] fn test_write_pdata_and_finish() { let presentation_context_id = 12; @@ -660,6 +680,42 @@ mod tests { assert_eq!(cursor.len(), 0); } + #[cfg(feature = "async")] + #[tokio::test(flavor = "multi_thread")] + async fn test_async_write_pdata_and_finish() { + use tokio::io::AsyncWriteExt; + let presentation_context_id = 12; + + let mut buf = Vec::new(); + { + let mut writer = PDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); + writer.write_all(&(0..64).collect::>()).await.unwrap(); + writer.finish().await.unwrap(); + } + + let mut cursor = &buf[..]; + let same_pdu = read_pdu(&mut cursor, MINIMUM_PDU_SIZE, true).unwrap(); + + // concatenate data chunks, compare with all data + + match same_pdu.unwrap() { + Pdu::PData { data: data_1 } => { + let data_1 = &data_1[0]; + + // check that this PDU is consistent + assert_eq!(data_1.value_type, PDataValueType::Data); + assert_eq!(data_1.presentation_context_id, presentation_context_id); + assert_eq!(data_1.data.len(), 64); + assert_eq!(data_1.data, (0..64).collect::>()); + } + pdu => panic!("Expected PData, got {:?}", pdu), + } + + assert_eq!(cursor.len(), 0); + } + + + #[cfg(not(feature = "async"))] #[test] fn test_write_large_pdata_and_finish() { let presentation_context_id = 32; @@ -738,6 +794,86 @@ mod tests { assert_eq!(cursor.len(), 0); } + #[cfg(feature = "async")] + #[tokio::test(flavor = "multi_thread")] + async fn test_async_write_large_pdata_and_finish() { + let presentation_context_id = 32; + + let my_data: Vec<_> = (0..9000).map(|x: u32| x as u8).collect(); + + let mut buf = Vec::new(); + { + let mut writer = PDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); + writer.write_all(&my_data).await.unwrap(); + writer.finish().await.unwrap(); + } + + let mut cursor = &buf[..]; + let pdu_1 = read_pdu(&mut cursor, MINIMUM_PDU_SIZE, true).unwrap(); + let pdu_2 = read_pdu(&mut cursor, MINIMUM_PDU_SIZE, true).unwrap(); + let pdu_3 = read_pdu(&mut cursor, MINIMUM_PDU_SIZE, true).unwrap(); + + // concatenate data chunks, compare with all data + + match (pdu_1.unwrap(), pdu_2.unwrap(), pdu_3.unwrap()) { + ( + Pdu::PData { data: data_1 }, + Pdu::PData { data: data_2 }, + Pdu::PData { data: data_3 }, + ) => { + assert_eq!(data_1.len(), 1); + let data_1 = &data_1[0]; + assert_eq!(data_2.len(), 1); + let data_2 = &data_2[0]; + assert_eq!(data_3.len(), 1); + let data_3 = &data_3[0]; + + // check that these two PDUs are consistent + assert_eq!(data_1.value_type, PDataValueType::Data); + assert_eq!(data_2.value_type, PDataValueType::Data); + assert_eq!(data_1.presentation_context_id, presentation_context_id); + assert_eq!(data_2.presentation_context_id, presentation_context_id); + + // check expected lengths + assert_eq!( + data_1.data.len(), + (MINIMUM_PDU_SIZE - PDU_HEADER_SIZE) as usize + ); + assert_eq!( + data_2.data.len(), + (MINIMUM_PDU_SIZE - PDU_HEADER_SIZE) as usize + ); + assert_eq!(data_3.data.len(), 820); + + // check data consistency + assert_eq!( + &data_1.data[..], + (0..MINIMUM_PDU_SIZE - PDU_HEADER_SIZE) + .map(|x| x as u8) + .collect::>() + ); + assert_eq!( + data_1.data.len() + data_2.data.len() + data_3.data.len(), + 9000 + ); + + let data_1 = &data_1.data; + let data_2 = &data_2.data; + let data_3 = &data_3.data; + + let mut all_data: Vec = Vec::new(); + all_data.extend(data_1); + all_data.extend(data_2); + all_data.extend(data_3); + assert_eq!(all_data, my_data); + } + x => panic!("Expected 3 PDatas, got {:?}", x), + } + + assert_eq!(cursor.len(), 0); + } + + #[cfg(not(feature = "async"))] #[test] fn test_read_large_pdata_and_finish() { let presentation_context_id = 32; @@ -776,4 +912,47 @@ mod tests { } assert_eq!(buf, my_data); } + + #[cfg(feature = "async")] + #[tokio::test] + async fn test_async_read_large_pdata_and_finish() { + + let presentation_context_id = 32; + + let my_data: Vec<_> = (0..9000).map(|x: u32| x as u8).collect(); + let pdata_1 = vec![PDataValue { + value_type: PDataValueType::Data, + data: my_data[0..3000].to_owned(), + presentation_context_id, + is_last: false, + }]; + let pdata_2 = vec![PDataValue { + value_type: PDataValueType::Data, + data: my_data[3000..6000].to_owned(), + presentation_context_id, + is_last: false, + }]; + let pdata_3 = vec![PDataValue { + value_type: PDataValueType::Data, + data: my_data[6000..].to_owned(), + presentation_context_id, + is_last: true, + }]; + + let mut pdu_stream = std::io::Cursor::new(Vec::new()); + + // write some PDUs + write_pdu(&mut pdu_stream, &Pdu::PData { data: pdata_1 }).unwrap(); + write_pdu(&mut pdu_stream, &Pdu::PData { data: pdata_2 }).unwrap(); + write_pdu(&mut pdu_stream, &Pdu::PData { data: pdata_3 }).unwrap(); + + let mut buf = Vec::new(); + let inner = pdu_stream.into_inner(); + let mut stream = tokio::io::BufReader::new(inner.as_slice()); + { + let mut reader = PDataReader::new(&mut stream, MINIMUM_PDU_SIZE); + reader.read_to_end(&mut buf).await.unwrap(); + } + assert_eq!(buf, my_data); + } } From 250578c510ed5b2c8feca66929bb44d16423cb05 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Wed, 7 Aug 2024 16:26:51 -0500 Subject: [PATCH 11/28] MAIN: Formatting and fixing compilation warnings --- storescp/Cargo.toml | 2 +- storescu/src/main.rs | 55 +++++++++++++++++++++++------------- storescu/src/store_async.rs | 24 +++++++++++----- storescu/src/store_sync.rs | 25 ++++++++++------ ul/src/association/client.rs | 11 ++++++++ ul/src/association/pdata.rs | 35 ++++++++++++++--------- ul/src/association/server.rs | 16 +++++------ 7 files changed, 111 insertions(+), 57 deletions(-) diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index e9d8df1b0..8fc05cd07 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -24,5 +24,5 @@ tracing-subscriber = "0.3.15" tokio = { version = "1.38.0", features = ["full"], optional = true } [features] -deafult = ["async"] +default = ["async"] async = ["dicom-ul/async", "dep:tokio"] diff --git a/storescu/src/main.rs b/storescu/src/main.rs index d4b03cba1..3df905a6e 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -16,10 +16,10 @@ use tracing::{debug, error, info, warn, Level}; use transfer_syntax::TransferSyntaxIndex; use walkdir::WalkDir; -#[cfg(not(feature = "async"))] -mod store_sync; #[cfg(feature = "async")] mod store_async; +#[cfg(not(feature = "async"))] +mod store_sync; /// DICOM C-STORE SCU #[derive(Debug, Parser)] @@ -149,8 +149,10 @@ async fn main() { } fn check_files( - files: Vec, verbose: bool, never_transcode: bool -) -> (Vec, HashSet<(String, String)>){ + files: Vec, + verbose: bool, + never_transcode: bool, +) -> (Vec, HashSet<(String, String)>) { let mut checked_files: Vec = vec![]; let mut dicom_files: Vec = vec![]; let mut presentation_contexts = HashSet::new(); @@ -207,13 +209,12 @@ fn check_files( eprintln!("No supported files to transfer"); std::process::exit(-1); } - return (dicom_files, presentation_contexts) - - + return (dicom_files, presentation_contexts); } #[cfg(not(feature = "async"))] fn run() -> Result<(), Error> { + use crate::store_sync::{get_scu, send_file}; let App { addr, files, @@ -251,7 +252,6 @@ fn run() -> Result<(), Error> { } let (mut dicom_files, presentation_contexts) = check_files(files, verbose, never_transcode); - let mut scu = get_scu( addr, calling_ae_title, @@ -262,7 +262,7 @@ fn run() -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, - presentation_contexts + presentation_contexts, )?; if verbose { @@ -313,7 +313,14 @@ fn run() -> Result<(), Error> { for file in dicom_files { // TODO - scu = store_sync::send_file(scu, file, message_id, progress_bar.as_ref(), verbose, fail_first)?; + scu = store_sync::send_file( + scu, + file, + message_id, + progress_bar.as_ref(), + verbose, + fail_first, + )?; } if let Some(pb) = progress_bar { @@ -363,10 +370,10 @@ async fn run() -> Result<(), Error> { if verbose { info!("Establishing association with '{}'...", &addr); } - let (mut dicom_files, presentation_contexts) = tokio::task::spawn_blocking( - move || check_files(files, verbose, never_transcode) - ).await.unwrap(); - + let (mut dicom_files, presentation_contexts) = + tokio::task::spawn_blocking(move || check_files(files, verbose, never_transcode)) + .await + .unwrap(); let mut scu = get_scu( addr, @@ -378,8 +385,9 @@ async fn run() -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, - presentation_contexts - ).await?; + presentation_contexts, + ) + .await?; if verbose { info!("Association established"); @@ -428,15 +436,24 @@ async fn run() -> Result<(), Error> { } for file in dicom_files { - // TODO - scu = send_file(scu, file, message_id, progress_bar.as_ref(), verbose, fail_first).await?; + // TODO: Eventually expose concurrency option, for now, just run sequentially + scu = send_file( + scu, + file, + message_id, + progress_bar.as_ref(), + verbose, + fail_first, + ) + .await?; } if let Some(pb) = progress_bar { pb.finish_with_message("done") }; - scu.release().await + scu.release() + .await .whatever_context("Failed to release SCU association")?; Ok(()) } diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs index d0f2bead7..2ecddc3f4 100644 --- a/storescu/src/store_async.rs +++ b/storescu/src/store_async.rs @@ -4,13 +4,19 @@ use dicom_dictionary_std::tags; use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{pdu::{PDataValue, PDataValueType}, ClientAssociation, ClientAssociationOptions, Pdu}; +use dicom_ul::{ + pdu::{PDataValue, PDataValueType}, + ClientAssociation, ClientAssociationOptions, Pdu, +}; use indicatif::ProgressBar; use snafu::{OptionExt, ResultExt}; use tokio::io::AsyncWriteExt; use tracing::{debug, error, info, warn}; -use crate::{into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, UnsupportedFileTransferSyntaxSnafu}; +use crate::{ + into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, + UnsupportedFileTransferSyntaxSnafu, +}; pub async fn get_scu( addr: String, @@ -22,7 +28,7 @@ pub async fn get_scu( kerberos_service_ticket: Option, saml_assertion: Option, jwt: Option, - presentation_contexts: HashSet<(String, String)> + presentation_contexts: HashSet<(String, String)>, ) -> Result { let mut scu_init = ClientAssociationOptions::new() .calling_ae_title(calling_ae_title) @@ -60,7 +66,12 @@ pub async fn get_scu( } pub async fn send_file( - mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool + mut scu: ClientAssociation, + file: DicomFile, + message_id: u16, + progress_bar: Option<&ProgressBar>, + verbose: bool, + fail_first: bool, ) -> Result { if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { if let Some(pb) = &progress_bar { @@ -164,8 +175,7 @@ pub async fn send_file( let cmd_obj = InMemDicomObject::read_dataset_with_ts( &data_value.data[..], - &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN - .erased(), + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), ) .whatever_context("Could not read response from SCP")?; if verbose { @@ -240,4 +250,4 @@ pub async fn send_file( pb.inc(1) }; Ok(scu) -} \ No newline at end of file +} diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs index ae0470f69..5cdfe2a93 100644 --- a/storescu/src/store_sync.rs +++ b/storescu/src/store_sync.rs @@ -4,12 +4,18 @@ use dicom_dictionary_std::tags; use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{pdu::{PDataValue, PDataValueType}, ClientAssociation, ClientAssociationOptions, Pdu}; +use dicom_ul::{ + pdu::{PDataValue, PDataValueType}, + ClientAssociation, ClientAssociationOptions, Pdu, +}; use indicatif::ProgressBar; use snafu::{OptionExt, ResultExt}; use tracing::{debug, error, info, warn}; -use crate::{into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, UnsupportedFileTransferSyntaxSnafu}; +use crate::{ + into_ts, store_req_command, CreateCommandSnafu, DicomFile, Error, InitScuSnafu, + UnsupportedFileTransferSyntaxSnafu, +}; pub fn get_scu( addr: String, @@ -21,7 +27,7 @@ pub fn get_scu( kerberos_service_ticket: Option, saml_assertion: Option, jwt: Option, - presentation_contexts: HashSet<(String, String)> + presentation_contexts: HashSet<(String, String)>, ) -> Result { let mut scu_init = ClientAssociationOptions::new() .calling_ae_title(calling_ae_title) @@ -59,7 +65,12 @@ pub fn get_scu( } pub fn send_file( - mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool + mut scu: ClientAssociation, + file: DicomFile, + message_id: u16, + progress_bar: Option<&ProgressBar>, + verbose: bool, + fail_first: bool, ) -> Result { if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { if let Some(pb) = &progress_bar { @@ -159,8 +170,7 @@ pub fn send_file( let cmd_obj = InMemDicomObject::read_dataset_with_ts( &data_value.data[..], - &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN - .erased(), + &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), ) .whatever_context("Could not read response from SCP")?; if verbose { @@ -235,5 +245,4 @@ pub fn send_file( pb.inc(1) }; Ok(scu) - -} \ No newline at end of file +} diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 83f721886..4e329efab 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -798,6 +798,7 @@ impl<'a> ClientAssociationOptions<'a> { }); let socket_addrs: Vec<_> = ae_address.to_socket_addrs().unwrap().collect(); + // TODO: add tokio-time flag and set timeouts for this and send/receive let mut socket = TcpStream::connect(socket_addrs.as_slice()) .await .context(ConnectSnafu)?; @@ -1221,6 +1222,16 @@ impl ClientAssociation { PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } + /// 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. + #[cfg(feature = "async")] + pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) + } + #[cfg(not(feature = "async"))] /// Release implementation function, /// which tries to send a release request and receive a release response. diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 5a50fac35..b672e295c 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,17 +1,18 @@ #[cfg(not(feature = "async"))] use std::io::Write; use std::{ - collections::VecDeque, future::Future, io::{BufRead, BufReader, Cursor, Read}, task::ready + collections::VecDeque, + io::{BufRead, BufReader, Cursor, Read}, }; -use bytes::{Buf, BufMut, BytesMut}; +use bytes::{Buf, BytesMut}; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; #[cfg(feature = "async")] -use tokio::io::{ReadBuf, AsyncReadExt}; +use tokio::io::ReadBuf; use tracing::warn; use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; @@ -353,6 +354,7 @@ where cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { + use std::future::Future; let total_len = self.max_data_len as usize + 12; if self.buffer.len() + buf.len() <= total_len { // accumulate into buffer, do nothing @@ -366,7 +368,7 @@ where debug_assert_eq!(self.buffer.len(), total_len); let dispatch = self.dispatch_pdu(); tokio::pin!(dispatch); - match dispatch.poll(cx){ + match dispatch.poll(cx) { Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), Poll::Ready(Err(e)) => Poll::Ready(Err(e)), Poll::Pending => Poll::Pending, @@ -538,7 +540,7 @@ where } #[cfg(feature = "async")] -impl PDataReader +impl PDataReader where R: AsyncRead + Unpin, { @@ -598,6 +600,7 @@ where } fn read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> usize { + use bytes::BufMut; let len = std::cmp::min(self.buffer.len(), buf.remaining()); for _ in 0..len { buf.put_u8(self.buffer.pop_front().unwrap()); @@ -612,9 +615,9 @@ where R: AsyncRead + Unpin, { fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf, ) -> Poll> { use std::task::ready; ready!(self.poll_fill_buf(cx))?; @@ -635,15 +638,18 @@ fn calculate_max_data_len_single(pdu_len: u32) -> u32 { #[cfg(test)] mod tests { - #[cfg(feature = "async")] - use tokio::{self, io::{AsyncWriteExt, AsyncReadExt}}; #[cfg(not(feature = "async"))] use std::io::{Read, Write}; + #[cfg(feature = "async")] + use tokio::{ + self, + io::{AsyncReadExt, AsyncWriteExt}, + }; + use crate::association::PDataWriter; use crate::pdu::{read_pdu, Pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE}; use crate::pdu::{PDataValue, PDataValueType}; use crate::write_pdu; - use crate::association::PDataWriter; use super::PDataReader; @@ -689,7 +695,10 @@ mod tests { let mut buf = Vec::new(); { let mut writer = PDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); - writer.write_all(&(0..64).collect::>()).await.unwrap(); + writer + .write_all(&(0..64).collect::>()) + .await + .unwrap(); writer.finish().await.unwrap(); } @@ -714,7 +723,6 @@ mod tests { assert_eq!(cursor.len(), 0); } - #[cfg(not(feature = "async"))] #[test] fn test_write_large_pdata_and_finish() { @@ -916,7 +924,6 @@ mod tests { #[cfg(feature = "async")] #[tokio::test] async fn test_async_read_large_pdata_and_finish() { - let presentation_context_id = 32; let my_data: Vec<_> = (0..9000).map(|x: u32| x as u8).collect(); diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index b2db6ac2e..1c930b98b 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -966,14 +966,14 @@ impl ServerAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - // #[cfg(feature = "async")] - // pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - // PDataWriter::new( - // &mut self.socket, - // presentation_context_id, - // self.requestor_max_pdu_length, - // ) - // } + #[cfg(feature = "async")] + pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + PDataWriter::new( + &mut self.socket, + presentation_context_id, + self.requestor_max_pdu_length, + ) + } /// Prepare a P-Data reader for receiving /// one or more data item PDUs. From b863798a84d3e3adacdbe76c3c05dcde51c0f602 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Thu, 8 Aug 2024 12:32:06 -0500 Subject: [PATCH 12/28] MAIN: Simplify implementation of poll_read --- ul/src/association/pdata.rs | 65 +++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index b672e295c..1b027497d 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -2,7 +2,7 @@ use std::io::Write; use std::{ collections::VecDeque, - io::{BufRead, BufReader, Cursor, Read}, + io::{BufRead, BufReader, Cursor, Read} }; use bytes::{Buf, BytesMut}; @@ -540,31 +540,46 @@ where } #[cfg(feature = "async")] -impl PDataReader +impl AsyncRead for PDataReader where R: AsyncRead + Unpin, { - fn poll_fill_buf(&mut self, cx: &mut Context<'_>) -> Poll> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf, + ) -> Poll> { + use tokio::io::{BufReader, AsyncBufRead, AsyncBufReadExt}; + use bytes::BufMut; use std::task::ready; - use tokio::io::{AsyncBufRead, AsyncBufReadExt}; - if self.buffer.is_empty() && !self.last_pdu { - let mut reader = tokio::io::BufReader::new(&mut self.stream); + if self.buffer.is_empty(){ + if self.last_pdu { + return Poll::Ready(Ok(())); + } + let Self { + ref mut stream, + ref mut read_buffer, + ref max_data_length, + .. + } = &mut *self; + let mut reader = BufReader::new(stream); let msg = loop { - let mut buf = std::io::Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.max_data_length, false) + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, *max_data_length, false) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { Some(pdu) => { - self.read_buffer.advance(buf.position() as usize); + read_buffer.advance(buf.position() as usize); break pdu; } None => { - buf.set_position(0); + // Reset position + buf.set_position(0) } } let recv = ready!(Pin::new(&mut reader).poll_fill_buf(cx))?.to_vec(); reader.consume(recv.len()); - self.read_buffer.extend_from_slice(&recv); + read_buffer.extend_from_slice(&recv); if recv.len() == 0 { return Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::Other, @@ -572,7 +587,6 @@ where ))); } }; - match msg { Pdu::PData { data } => { for pdata_value in data { @@ -592,40 +606,20 @@ where return Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "Unexpected PDU type", - ))); + ))) } + } } - Poll::Ready(Ok(())) - } - - fn read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> usize { - use bytes::BufMut; let len = std::cmp::min(self.buffer.len(), buf.remaining()); for _ in 0..len { buf.put_u8(self.buffer.pop_front().unwrap()); } - len - } -} - -#[cfg(feature = "async")] -impl AsyncRead for PDataReader -where - R: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf, - ) -> Poll> { - use std::task::ready; - ready!(self.poll_fill_buf(cx))?; - let _ = self.read_buffer(buf); Poll::Ready(Ok(())) } } + /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). @@ -884,6 +878,7 @@ mod tests { #[cfg(not(feature = "async"))] #[test] fn test_read_large_pdata_and_finish() { + use std::collections::VecDeque; let presentation_context_id = 32; let my_data: Vec<_> = (0..9000).map(|x: u32| x as u8).collect(); From fdccf13012496f28a1625a56b9dc1ca3ae35d29a Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 9 Aug 2024 09:39:45 -0500 Subject: [PATCH 13/28] MAIN: Review comments and implement async versions of tests --- Cargo.toml | 4 + ul/src/association/client.rs | 10 +- ul/src/association/pdata.rs | 45 +++++---- ul/src/association/server.rs | 2 + ul/tests/association_echo.rs | 82 ++++++++++++++-- ul/tests/association_promiscuous.rs | 95 ++++++++++++++++++- ul/tests/association_store.rs | 92 ++++++++++++++++-- ul/tests/association_store_uncompressed.rs | 103 +++++++++++++++++++-- 8 files changed, 383 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa7291330..153aecf5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,7 @@ opt-level = 2 # optimize JPEG 2000 decoder to run tests faster [profile.dev.package.jpeg2k] opt-level = 2 + +# optimize flate2 to run tests faster +[profile.dev.package."flate2"] +opt-level = 2 diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 4e329efab..42e38f0e4 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -139,6 +139,7 @@ pub type Result = std::result::Result; /// /// # Example /// +#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; /// # fn run() -> Result<(), Box> { @@ -148,6 +149,7 @@ pub type Result = std::result::Result; /// # Ok(()) /// # } /// ``` +"##)] /// /// At least one presentation context must be specified, /// using the method [`with_presentation_context`](Self::with_presentation_context) @@ -159,6 +161,7 @@ pub type Result = std::result::Result; /// in the resulting presentation context. /// # Example /// +#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; /// # fn run() -> Result<(), Box> { @@ -168,6 +171,7 @@ pub type Result = std::result::Result; /// # Ok(()) /// # } /// ``` +"##)] #[derive(Debug, Clone)] pub struct ClientAssociationOptions<'a> { /// the calling AE title @@ -477,11 +481,13 @@ impl<'a> ClientAssociationOptions<'a> { /// /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; - /// # fn run() -> Result<(), Box> { + /// #[tokio::main] + /// # async fn run() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_abstract_syntax("1.2.840.10008.1.1") /// // called AE title in address - /// .establish_with("MY-STORAGE@10.0.0.100:104")?; + /// .establish_with("MY-STORAGE@10.0.0.100:104") + /// .await?; /// # Ok(()) /// # } /// ``` diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 1b027497d..cd8b3f569 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -231,13 +231,16 @@ where /// /// ```no_run /// # use std::io::Write; +/// use tokio::io::AsyncWriteExt; /// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } -/// # fn main() -> Result<(), Box> { +/// #[tokio::main] +/// # async fn main() -> Result<(), Box> { /// let mut association = ClientAssociationOptions::new() -/// .establish("129.168.0.5:104")?; +/// .establish("129.168.0.5:104") +/// .await?; /// /// let presentation_context_id = association.presentation_contexts()[0].id; /// @@ -249,16 +252,17 @@ where /// is_last: true, /// data: command_data(), /// }], -/// }); +/// }).await; /// /// // then send a DICOM object which may be split into multiple PDUs -/// let mut pdata = association.send_pdata(presentation_context_id); -/// pdata.write_all(dicom_data())?; -/// pdata.finish()?; +/// let mut pdata = association.send_pdata(presentation_context_id).await; +/// pdata.write_all(dicom_data()).await?; +/// pdata.finish().await?; /// -/// let pdu_ac = association.receive()?; +/// let pdu_ac = association.receive().await?; /// # Ok(()) /// # } +/// ``` #[cfg(feature = "async")] #[must_use] pub struct AsyncPDataWriter { @@ -423,6 +427,7 @@ where /// Use an association's `receive_pdata` method /// to create a new P-Data value reader. /// +#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use std::io::Read; /// # use dicom_ul::association::{ClientAssociationOptions, PDataReader}; @@ -442,6 +447,8 @@ where /// }; /// # Ok(()) /// # } +/// ``` +"##)] #[must_use] pub struct PDataReader { buffer: VecDeque, @@ -664,8 +671,8 @@ mod tests { // concatenate data chunks, compare with all data - match same_pdu.unwrap() { - Pdu::PData { data: data_1 } => { + match same_pdu { + Some(Pdu::PData { data: data_1 }) => { let data_1 = &data_1[0]; // check that this PDU is consistent @@ -701,8 +708,8 @@ mod tests { // concatenate data chunks, compare with all data - match same_pdu.unwrap() { - Pdu::PData { data: data_1 } => { + match same_pdu { + Some(Pdu::PData { data: data_1 }) => { let data_1 = &data_1[0]; // check that this PDU is consistent @@ -738,11 +745,11 @@ mod tests { // concatenate data chunks, compare with all data - match (pdu_1.unwrap(), pdu_2.unwrap(), pdu_3.unwrap()) { + match (pdu_1, pdu_2, pdu_3) { ( - Pdu::PData { data: data_1 }, - Pdu::PData { data: data_2 }, - Pdu::PData { data: data_3 }, + Some(Pdu::PData { data: data_1 }), + Some(Pdu::PData { data: data_2 }), + Some(Pdu::PData { data: data_3 }), ) => { assert_eq!(data_1.len(), 1); let data_1 = &data_1[0]; @@ -817,11 +824,11 @@ mod tests { // concatenate data chunks, compare with all data - match (pdu_1.unwrap(), pdu_2.unwrap(), pdu_3.unwrap()) { + match (pdu_1, pdu_2, pdu_3) { ( - Pdu::PData { data: data_1 }, - Pdu::PData { data: data_2 }, - Pdu::PData { data: data_3 }, + Some(Pdu::PData { data: data_1 }), + Some(Pdu::PData { data: data_2 }), + Some(Pdu::PData { data: data_3 }), ) => { assert_eq!(data_1.len(), 1); let data_1 = &data_1[0]; diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 1c930b98b..084f13be6 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -164,6 +164,7 @@ impl AccessControl for AcceptCalledAeTitle { /// /// # Example /// +#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use std::net::TcpListener; /// # use dicom_ul::association::server::ServerAssociationOptions; @@ -178,6 +179,7 @@ impl AccessControl for AcceptCalledAeTitle { /// # Ok(()) /// # } /// ``` +"##)] /// /// The SCP will by default accept all transfer syntaxes /// supported by the main [transfer syntax registry][1], diff --git a/ul/tests/association_echo.rs b/ul/tests/association_echo.rs index 99784e74a..f5dd04b3e 100644 --- a/ul/tests/association_echo.rs +++ b/ul/tests/association_echo.rs @@ -2,11 +2,8 @@ use dicom_ul::{ association::client::ClientAssociationOptions, pdu::{Pdu, PresentationContextResult, PresentationContextResultReason}, }; -use std::net::TcpListener; -use std::{ - net::SocketAddr, - thread::{spawn, JoinHandle}, -}; + +use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; @@ -21,15 +18,16 @@ static JPEG_BASELINE: &str = "1.2.840.10008.1.2.4.50"; static VERIFICATION_SOP_CLASS: &str = "1.2.840.10008.1.1"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { - let listener = TcpListener::bind("localhost:0")?; +#[cfg(not(feature = "async"))] +fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { + let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() .accept_called_ae_title() .ae_title(SCP_AE_TITLE) .with_abstract_syntax(VERIFICATION_SOP_CLASS); - let h = spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<()> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -59,7 +57,47 @@ fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { Ok((h, addr)) } +#[cfg(feature = "async")] +async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { + let listener = tokio::net::TcpListener::bind("localhost:0").await?; + let addr = listener.local_addr()?; + let scp = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title(SCP_AE_TITLE) + .with_abstract_syntax(VERIFICATION_SOP_CLASS); + + let h = tokio::spawn(async move { + let (stream, _addr) = listener.accept().await?; + let mut association = scp.establish(stream).await?; + + assert_eq!( + association.presentation_contexts(), + &[ + PresentationContextResult { + id: 1, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: IMPLICIT_VR_LE.to_string(), + }, + PresentationContextResult { + id: 2, + reason: PresentationContextResultReason::AbstractSyntaxNotSupported, + transfer_syntax: IMPLICIT_VR_LE.to_string(), + } + ], + ); + + // handle one release request + let pdu = association.receive().await?; + assert_eq!(pdu, Pdu::ReleaseRQ); + association.send(&Pdu::ReleaseRP).await?; + + Ok(()) + }); + Ok((h, addr)) +} + /// Run an SCP and an SCU concurrently, negotiate an association and release it. +#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_test() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -84,3 +122,31 @@ fn scu_scp_association_test() { .expect("SCP panicked") .expect("Error at the SCP"); } + +#[cfg(feature = "async")] +#[tokio::test(flavor = "multi_thread")] +async fn scu_scp_asociation_test(){ + let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); + + let association = ClientAssociationOptions::new() + .calling_ae_title(SCU_AE_TITLE) + .called_ae_title(SCP_AE_TITLE) + .with_presentation_context(VERIFICATION_SOP_CLASS, vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE]) + .with_presentation_context( + DIGITAL_MG_STORAGE_SOP_CLASS, + vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE, JPEG_BASELINE], + ) + .establish(scp_addr) + .await + .unwrap(); + + association + .release() + .await + .expect("did not have a peaceful release"); + + scp_handle + .await + .expect("SCP panicked") + .expect("Error at the SCP"); +} diff --git a/ul/tests/association_promiscuous.rs b/ul/tests/association_promiscuous.rs index 9419e6182..17e3edc9f 100644 --- a/ul/tests/association_promiscuous.rs +++ b/ul/tests/association_promiscuous.rs @@ -1,8 +1,7 @@ use dicom_ul::association::client::Error::NoAcceptedPresentationContexts; use dicom_ul::pdu::{PresentationContextResult, PresentationContextResultReason}; use dicom_ul::{ClientAssociationOptions, Pdu, ServerAssociationOptions}; -use std::net::{SocketAddr, TcpListener}; -use std::thread::{spawn, JoinHandle}; +use std::net::SocketAddr; type Result = std::result::Result>; @@ -13,11 +12,12 @@ const IMPLICIT_VR_LE: &str = "1.2.840.10008.1.2"; const MR_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.4\0"; const ULTRASOUND_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.6.1\0"; +#[cfg(not(feature = "async"))] fn spawn_scp( abstract_syntax_uids: &'static [&str], promiscuous: bool, -) -> Result<(JoinHandle>, SocketAddr)> { - let listener = TcpListener::bind("localhost:0")?; +) -> Result<(std::thread::JoinHandle>, SocketAddr)> { + let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let mut options = ServerAssociationOptions::new() .accept_called_ae_title() @@ -28,7 +28,7 @@ fn spawn_scp( options = options.with_abstract_syntax(*abstract_syntax_uid); } - let handle = spawn(move || { + let handle = std::thread::spawn(move || { let (stream, _addr) = listener.accept()?; let mut association = options.establish(stream)?; assert_eq!( @@ -50,6 +50,45 @@ fn spawn_scp( Ok((handle, addr)) } +#[cfg(feature = "async")] +async fn spawn_scp( + abstract_syntax_uids: &'static [&str], + promiscuous: bool, +) -> Result<(tokio::task::JoinHandle>, SocketAddr)> { + let listener = tokio::net::TcpListener::bind("localhost:0").await?; + let addr = listener.local_addr()?; + let mut options = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title(SCP_AE_TITLE) + .promiscuous(promiscuous); + + for abstract_syntax_uid in abstract_syntax_uids { + options = options.with_abstract_syntax(*abstract_syntax_uid); + } + + let handle = tokio::spawn(async move { + let (stream, _addr) = listener.accept().await?; + let mut association = options.establish(stream).await?; + assert_eq!( + association.presentation_contexts(), + &[PresentationContextResult { + id: 1, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: IMPLICIT_VR_LE.to_string(), + }] + ); + + let pdu = association.receive().await?; + assert_eq!(pdu, Pdu::ReleaseRQ); + association.send(&Pdu::ReleaseRP).await?; + + Ok(()) + }); + + Ok((handle, addr)) +} + +#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_promiscuous_enabled() { // SCP is set to promiscuous mode - all abstract syntaxes are accepted @@ -72,6 +111,32 @@ fn scu_scp_association_promiscuous_enabled() { .expect("Error at the SCP"); } +#[cfg(feature = "async")] +#[tokio::test(flavor = "multi_thread")] +async fn scu_scp_association_promiscuous_enabled() { + // SCP is set to promiscuous mode - all abstract syntaxes are accepted + let (scp_handle, scp_addr) = spawn_scp(&[], true).await.unwrap(); + + let association = ClientAssociationOptions::new() + .calling_ae_title(SCU_AE_TITLE) + .called_ae_title(SCP_AE_TITLE) + .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) + .establish(scp_addr) + .await + .unwrap(); + + association + .release() + .await + .expect("did not have a peaceful release"); + + scp_handle + .await + .expect("SCP panicked") + .expect("Error at the SCP"); +} + +#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_promiscuous_disabled() { // SCP only accepts Ultrasound Image Storage @@ -89,3 +154,23 @@ fn scu_scp_association_promiscuous_disabled() { Err(NoAcceptedPresentationContexts { .. }) )); } + +#[cfg(feature = "async")] +#[tokio::test(flavor = "multi_thread")] +async fn scu_scp_association_promiscuous_disabled() { + // SCP only accepts Ultrasound Image Storage + let (_scu_handle, scp_addr) = spawn_scp(&[ULTRASOUND_IMAGE_STORAGE_RAW], false).await.unwrap(); + + let association = ClientAssociationOptions::new() + .calling_ae_title(SCU_AE_TITLE) + .called_ae_title(SCP_AE_TITLE) + .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) + .establish(scp_addr) + .await; + + // Assert that no presentation context was accepted + assert!(matches!( + association, + Err(NoAcceptedPresentationContexts { .. }) + )); +} \ No newline at end of file diff --git a/ul/tests/association_store.rs b/ul/tests/association_store.rs index 1a9da9ad9..7ec75ae65 100644 --- a/ul/tests/association_store.rs +++ b/ul/tests/association_store.rs @@ -2,11 +2,7 @@ use dicom_ul::{ association::client::ClientAssociationOptions, pdu::{Pdu, PresentationContextResult, PresentationContextResultReason}, }; -use std::net::TcpListener; -use std::{ - net::SocketAddr, - thread::{spawn, JoinHandle}, -}; +use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; @@ -24,8 +20,9 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { - let listener = TcpListener::bind("localhost:0")?; +#[cfg(not(feature = "async"))] +fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { + let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() .accept_called_ae_title() @@ -33,7 +30,7 @@ fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { .with_abstract_syntax(MR_IMAGE_STORAGE) .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS); - let h = spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<()> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -63,9 +60,50 @@ fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { Ok((h, addr)) } +#[cfg(feature = "async")] +async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { + let listener = tokio::net::TcpListener::bind("localhost:0").await?; + let addr = listener.local_addr()?; + let scp = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title(SCP_AE_TITLE) + .with_abstract_syntax(MR_IMAGE_STORAGE) + .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS); + + let h = tokio::task::spawn(async move { + let (stream, _addr) = listener.accept().await?; + let mut association = scp.establish(stream).await?; + + assert_eq!( + association.presentation_contexts(), + &[ + PresentationContextResult { + id: 1, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: IMPLICIT_VR_LE.to_string(), + }, + PresentationContextResult { + id: 2, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: JPEG_BASELINE.to_string(), + } + ], + ); + + // handle one release request + let pdu = association.receive().await?; + assert_eq!(pdu, Pdu::ReleaseRQ); + association.send(&Pdu::ReleaseRP).await?; + + Ok(()) + }); + Ok((h, addr)) +} + /// Run an SCP and an SCU concurrently, /// negotiate an association with distinct transfer syntaxes /// and release it. +#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_test() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -102,3 +140,41 @@ fn scu_scp_association_test() { .expect("SCP panicked") .expect("Error at the SCP"); } + +#[cfg(feature = "async")] +#[tokio::test(flavor = "multi_thread")] +async fn scu_scp_association_test() { + let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); + + let association = ClientAssociationOptions::new() + .calling_ae_title(SCU_AE_TITLE) + .called_ae_title(SCP_AE_TITLE) + .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) + // MG storage, JPEG baseline + .with_presentation_context(DIGITAL_MG_STORAGE_SOP_CLASS_RAW, vec![JPEG_BASELINE]) + .establish(scp_addr).await + .unwrap(); + + for pc in association.presentation_contexts() { + match pc.id { + 1 => { + // guaranteed to be MR image storage + assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); + } + 2 => { + // guaranteed to be MG image storage + assert_eq!(pc.transfer_syntax, JPEG_BASELINE); + } + id => panic!("unexpected presentation context ID {}", id), + } + } + + association + .release().await + .expect("did not have a peaceful release"); + + scp_handle + .await + .expect("SCP panicked") + .expect("Error at the SCP"); +} diff --git a/ul/tests/association_store_uncompressed.rs b/ul/tests/association_store_uncompressed.rs index 23241cd51..51b6357eb 100644 --- a/ul/tests/association_store_uncompressed.rs +++ b/ul/tests/association_store_uncompressed.rs @@ -5,11 +5,7 @@ use dicom_ul::{ association::client::ClientAssociationOptions, pdu::{Pdu, PresentationContextResult, PresentationContextResultReason}, }; -use std::net::TcpListener; -use std::{ - net::SocketAddr, - thread::{spawn, JoinHandle}, -}; +use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; @@ -28,8 +24,9 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { - let listener = TcpListener::bind("localhost:0")?; +#[cfg(not(feature = "async"))] +fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { + let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() .accept_called_ae_title() @@ -39,7 +36,7 @@ fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { .with_transfer_syntax(EXPLICIT_VR_LE) .with_transfer_syntax(IMPLICIT_VR_LE); - let h = spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<()> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -71,9 +68,55 @@ fn spawn_scp() -> Result<(JoinHandle>, SocketAddr)> { Ok((h, addr)) } +#[cfg(feature = "async")] +async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { + let listener = tokio::net::TcpListener::bind("localhost:0").await?; + let addr = listener.local_addr()?; + let scp = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title(SCP_AE_TITLE) + .with_abstract_syntax(MR_IMAGE_STORAGE) + .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS) + .with_transfer_syntax(EXPLICIT_VR_LE) + .with_transfer_syntax(IMPLICIT_VR_LE); + + let h = tokio::task::spawn(async move { + let (stream, _addr) = listener.accept().await?; + let mut association = scp.establish(stream).await?; + + assert_eq!( + association.presentation_contexts(), + &[ + PresentationContextResult { + id: 1, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: IMPLICIT_VR_LE.to_string(), + }, + // should always pick Explicit VR LE + // because JPEG baseline was not explicitly enabled in SCP + PresentationContextResult { + id: 2, + reason: PresentationContextResultReason::Acceptance, + transfer_syntax: EXPLICIT_VR_LE.to_string(), + } + ], + ); + + // handle one release request + let pdu = association.receive().await?; + assert_eq!(pdu, Pdu::ReleaseRQ); + association.send(&Pdu::ReleaseRP).await?; + + Ok(()) + }); + Ok((h, addr)) +} + + /// Run an SCP and an SCU concurrently, /// negotiate an association with distinct transfer syntaxes /// and release it. +#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_uncompressed() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -115,3 +158,47 @@ fn scu_scp_association_uncompressed() { .expect("SCP panicked") .expect("Error at the SCP"); } + +#[cfg(feature = "async")] +#[tokio::test(flavor = "multi_thread")] +async fn scu_scp_association_uncompressed() { + let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); + + let association = ClientAssociationOptions::new() + .calling_ae_title(SCU_AE_TITLE) + .called_ae_title(SCP_AE_TITLE) + .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) + // MG storage, JPEG baseline + .with_presentation_context( + DIGITAL_MG_STORAGE_SOP_CLASS_RAW, + vec![JPEG_BASELINE, EXPLICIT_VR_LE, IMPLICIT_VR_LE], + ) + .establish(scp_addr).await + .unwrap(); + + for pc in association.presentation_contexts() { + match pc.id { + // guaranteed to be MR image storage + 1 => { + // only one option provided + assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); + } + // guaranteed to be MG image storage + 2 => { + // server picked this one because it did not accept JPEG baseline + assert_eq!(pc.transfer_syntax, EXPLICIT_VR_LE); + } + id => panic!("unexpected presentation context ID {}", id), + } + } + + association + .release().await + .expect("did not have a peaceful release"); + + scp_handle + .await + .expect("SCP panicked") + .expect("Error at the SCP"); +} + From b73bb0d06eb6507b78dbe99202813a5991220909 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 9 Aug 2024 09:40:52 -0500 Subject: [PATCH 14/28] MAIN: Add github workflow for async feature flag --- .github/workflows/rust.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a29bd6712..b42aa3c6d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -33,6 +33,7 @@ jobs: - run: cargo test -p dicom-pixeldata --features gdcm # test dicom-pixeldata without default features - run: cargo test -p dicom-pixeldata --no-default-features + - run: cargo test -p dicom-ul --features async # run Clippy with stable toolchain - if: matrix.rust == 'stable' run: cargo clippy @@ -60,4 +61,4 @@ jobs: toolchain: stable cache: true - run: cargo check - \ No newline at end of file + From 113b7f7189fdc047040551e8e68a84305b809ae4 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 9 Aug 2024 09:42:39 -0500 Subject: [PATCH 15/28] MAIN: Enumerate needed tokio features --- storescu/Cargo.toml | 6 +++++- storescu/src/main.rs | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/storescu/Cargo.toml b/storescu/Cargo.toml index d17cd2d1b..97ecc2c9f 100644 --- a/storescu/Cargo.toml +++ b/storescu/Cargo.toml @@ -30,4 +30,8 @@ indicatif = "0.17.0" tracing = "0.1.34" tracing-subscriber = "0.3.11" snafu = "0.8" -tokio = { version = "1.38.0", features = ["full"], optional = true } \ No newline at end of file + +[dependencies.tokio] +version = "1.38.0" +features = ["rt", "rt-multi-thread", "macros"] +optional = true diff --git a/storescu/src/main.rs b/storescu/src/main.rs index 3df905a6e..a0411512e 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -313,7 +313,7 @@ fn run() -> Result<(), Error> { for file in dicom_files { // TODO - scu = store_sync::send_file( + scu = send_file( scu, file, message_id, @@ -436,7 +436,8 @@ async fn run() -> Result<(), Error> { } for file in dicom_files { - // TODO: Eventually expose concurrency option, for now, just run sequentially + // TODO: Eventually expose concurrency option to sping up multiple + // worker tasks to send files in parallel scu = send_file( scu, file, From 0b625864a9416ef155a54e5f8af1660a59a40b59 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 9 Aug 2024 12:01:38 -0500 Subject: [PATCH 16/28] MAIN: Fix warnings and compilation errors --- scpproxy/src/main.rs | 14 ++-- storescp/src/main.rs | 6 +- storescp/src/store_async.rs | 4 +- ul/src/association/client.rs | 123 ++++++++++++++++++++--------------- 4 files changed, 82 insertions(+), 65 deletions(-) diff --git a/scpproxy/src/main.rs b/scpproxy/src/main.rs index 307cdd11c..2696b96c9 100644 --- a/scpproxy/src/main.rs +++ b/scpproxy/src/main.rs @@ -1,7 +1,7 @@ use clap::{crate_version, value_parser, Arg, ArgAction, Command}; -use dicom_ul::pdu::reader::read_pdu; use dicom_ul::pdu::writer::write_pdu; use dicom_ul::pdu::Pdu; +use dicom_ul::association::client::get_client_pdu; use snafu::{Backtrace, OptionExt, Report, ResultExt, Snafu, Whatever}; use std::io::Write; use std::net::{Shutdown, TcpListener, TcpStream}; @@ -63,11 +63,11 @@ pub enum ThreadMessage { }, ReadErr { from: ProviderType, - err: dicom_ul::pdu::reader::Error, + err: dicom_ul::association::client::Error, }, WriteErr { from: ProviderType, - err: dicom_ul::pdu::writer::Error, + err: dicom_ul::pdu::WriteError, }, Shutdown { initiator: ProviderType, @@ -96,7 +96,7 @@ fn run( let message_tx = message_tx.clone(); scu_reader_thread = thread::spawn(move || { loop { - match read_pdu(&mut reader, max_pdu_length, strict) { + match get_client_pdu(&mut reader, max_pdu_length, strict) { Ok(pdu) => { message_tx .send(ThreadMessage::SendPdu { @@ -105,7 +105,7 @@ fn run( }) .context(SendMessageSnafu)?; } - Err(dicom_ul::pdu::reader::Error::NoPduAvailable { .. }) => { + Err(dicom_ul::association::client::Error::ReceiveResponse{ .. }) => { message_tx .send(ThreadMessage::Shutdown { initiator: ProviderType::Scu, @@ -133,7 +133,7 @@ fn run( let mut reader = scp_stream.try_clone().context(CloneSocketSnafu)?; scp_reader_thread = thread::spawn(move || { loop { - match read_pdu(&mut reader, max_pdu_length, strict) { + match get_client_pdu(&mut reader, max_pdu_length, strict) { Ok(pdu) => { message_tx .send(ThreadMessage::SendPdu { @@ -142,7 +142,7 @@ fn run( }) .context(SendMessageSnafu)?; } - Err(dicom_ul::pdu::reader::Error::NoPduAvailable { .. }) => { + Err(dicom_ul::association::client::Error::ReceiveResponse{ .. }) => { message_tx .send(ThreadMessage::Shutdown { initiator: ProviderType::Scp, diff --git a/storescp/src/main.rs b/storescp/src/main.rs index ced0f6c35..04c84af6d 100644 --- a/storescp/src/main.rs +++ b/storescp/src/main.rs @@ -1,5 +1,5 @@ use std::{ - net::{Ipv4Addr, SocketAddrV4, TcpListener}, + net::{Ipv4Addr, SocketAddrV4}, path::PathBuf, }; @@ -143,8 +143,6 @@ async fn main() -> Result<(), Box> { } }); } - - Ok(()) } #[cfg(not(feature = "async"))] @@ -175,7 +173,7 @@ fn main() -> Result<(), Box> { }); let listen_addr = SocketAddrV4::new(Ipv4Addr::from(0), args.port); - let listener = TcpListener::bind(listen_addr)?; + let listener = std::net::TcpListener::bind(listen_addr)?; info!( "{} listening on: tcp://{}", &args.calling_ae_title, listen_addr diff --git a/storescp/src/store_async.rs b/storescp/src/store_async.rs index 36352488b..703eeb0cc 100644 --- a/storescp/src/store_async.rs +++ b/storescp/src/store_async.rs @@ -1,10 +1,10 @@ use dicom_dictionary_std::tags; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; -use dicom_object::{FileMetaTableBuilder, InMemDicomObject, StandardDataDictionary}; +use dicom_object::{FileMetaTableBuilder, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use dicom_ul::{pdu::PDataValueType, Pdu}; use snafu::{OptionExt, Report, ResultExt, Whatever}; -use tracing::{debug, error, info, warn, Level}; +use tracing::{debug, info, warn}; use crate::{transfer::ABSTRACT_SYNTAXES, App, create_cecho_response, create_cstore_response}; pub async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 42e38f0e4..6a920e0aa 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -7,9 +7,9 @@ use bytes::BytesMut; use std::{borrow::Cow, convert::TryInto, io::Cursor, net::ToSocketAddrs, time::Duration}; #[cfg(not(feature = "async"))] -use std::{io::Write, net::TcpStream}; +use std::{io::{Write, Read, BufReader, BufRead}, net::TcpStream}; #[cfg(feature = "async")] -use tokio::{io::AsyncWriteExt, net::TcpStream}; +use tokio::{io::{AsyncRead, AsyncWriteExt}, net::TcpStream}; use crate::{ pdu::{ @@ -130,6 +130,67 @@ pub enum Error { pub type Result = std::result::Result; + +#[cfg(not(feature = "async"))] +pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result{ + // Receive response + + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + let mut reader = BufReader::new(reader); + + let msg = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, max_pdu_length, strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu; + } + None => { + // Reset position + buf.set_position(0) + } + } + // Use BufReader to get similar behavior to AsyncRead read_buf + let recv = reader + .fill_buf() + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + .to_vec(); + reader.consume(recv.len()); + read_buffer.extend_from_slice(&recv); + ensure!(recv.len() > 0, ConnectionClosedSnafu); + }; + Ok(msg) +} + +#[cfg(feature = "async")] +pub async fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result{ + // receive response + use tokio::io::AsyncReadExt; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + + let msg = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, max_pdu_length, strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu; + } + None => { + // Reset position + buf.set_position(0) + } + } + let recv = reader + .read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); + }; + Ok(msg) +} + /// A DICOM association builder for a client node. /// The final outcome is a [`ClientAssociation`]. /// @@ -514,8 +575,6 @@ impl<'a> ClientAssociationOptions<'a> { where T: ToSocketAddrs, { - use std::io::{BufRead, BufReader}; - let ClientAssociationOptions { calling_ae_title, called_ae_title, @@ -606,32 +665,7 @@ impl<'a> ClientAssociationOptions<'a> { socket.write_all(&buffer).context(WireSendSnafu)?; buffer.clear(); - // Receive response - let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - let mut reader = BufReader::new(&mut socket); - - let msg = loop { - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu; - } - None => { - // Reset position - buf.set_position(0) - } - } - // Use BufReader to get similar behavior to AsyncRead read_buf - let recv = reader - .fill_buf() - .context(ReadPduSnafu) - .context(ReceiveSnafu)? - .to_vec(); - reader.consume(recv.len()); - read_buffer.extend_from_slice(&recv); - ensure!(recv.len() > 0, ConnectionClosedSnafu); - }; + let msg = get_client_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict)?; match msg { Pdu::AssociationAC(AssociationAC { @@ -814,29 +848,10 @@ impl<'a> ClientAssociationOptions<'a> { write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; socket.write_all(&buffer).await.context(WireSendSnafu)?; buffer.clear(); - // receive response - use tokio::io::AsyncReadExt; - let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - let msg = loop { - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveResponseSnafu)? { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu; - } - None => { - // Reset position - buf.set_position(0) - } - } - let recv = socket - .read_buf(&mut read_buffer) - .await - .context(ReadPduSnafu) - .context(ReceiveSnafu)?; - ensure!(recv > 0, ConnectionClosedSnafu); - }; + // receive response + let msg = get_client_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict) + .await?; match msg { Pdu::AssociationAC(AssociationAC { @@ -1020,6 +1035,10 @@ pub struct ClientAssociation { } impl ClientAssociation { + /// Retrieve timeout for the association + pub fn timeout(&self) -> Option { + self.timeout + } /// Retrieve the list of negotiated presentation contexts. pub fn presentation_contexts(&self) -> &[PresentationContextResult] { &self.presentation_contexts From 4830f2f11a0aec49277a3741214d93686fb9f123 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Sun, 11 Aug 2024 09:02:19 -0500 Subject: [PATCH 17/28] MAIN: Fix CI issues * Turn off async as default for storescp as it sets it to be a default feature across the workspace and breaks other builds * Fix clippy errors --- storescp/Cargo.toml | 2 +- storescp/src/main.rs | 2 -- storescu/out.json | 0 storescu/src/main.rs | 4 ++-- storescu/src/store_async.rs | 9 +++++---- storescu/src/store_sync.rs | 3 ++- ul/src/association/client.rs | 12 ++++++------ ul/src/association/pdata.rs | 6 +++--- ul/src/association/server.rs | 4 ++-- ul/src/pdu/reader.rs | 2 +- 10 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 storescu/out.json diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index 8fc05cd07..bbdfbb2fc 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -24,5 +24,5 @@ tracing-subscriber = "0.3.15" tokio = { version = "1.38.0", features = ["full"], optional = true } [features] -default = ["async"] +default = [] async = ["dicom-ul/async", "dep:tokio"] diff --git a/storescp/src/main.rs b/storescp/src/main.rs index 04c84af6d..dd6f3c1b7 100644 --- a/storescp/src/main.rs +++ b/storescp/src/main.rs @@ -147,8 +147,6 @@ async fn main() -> Result<(), Box> { #[cfg(not(feature = "async"))] fn main() -> Result<(), Box> { - use std::io::Read; - let args = App::parse(); tracing::subscriber::set_global_default( diff --git a/storescu/out.json b/storescu/out.json new file mode 100644 index 000000000..e69de29bb diff --git a/storescu/src/main.rs b/storescu/src/main.rs index a0411512e..881bf1e3a 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -209,7 +209,7 @@ fn check_files( eprintln!("No supported files to transfer"); std::process::exit(-1); } - return (dicom_files, presentation_contexts); + (dicom_files, presentation_contexts) } #[cfg(not(feature = "async"))] @@ -413,7 +413,7 @@ async fn run() -> Result<(), Error> { Err(e) => { error!("{}", Report::from_error(e)); if fail_first { - let _ = scu.abort(); + let _ = scu.abort().await; std::process::exit(-2); } } diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs index 2ecddc3f4..3be699fda 100644 --- a/storescu/src/store_async.rs +++ b/storescu/src/store_async.rs @@ -18,6 +18,7 @@ use crate::{ UnsupportedFileTransferSyntaxSnafu, }; +#[allow(clippy::too_many_arguments)] pub async fn get_scu( addr: String, calling_ae_title: String, @@ -62,7 +63,7 @@ pub async fn get_scu( scu_init = scu_init.jwt(jwt); } - Ok(scu_init.establish_with(&addr).await.context(InitScuSnafu)?) + scu_init.establish_with(&addr).await.context(InitScuSnafu) } pub async fn send_file( @@ -216,7 +217,7 @@ pub async fn send_file( storage_sop_instance_uid ); if fail_first { - let _ = scu.abort(); + let _ = scu.abort().await; std::process::exit(-2); } } @@ -226,7 +227,7 @@ pub async fn send_file( storage_sop_instance_uid, status ); if fail_first { - let _ = scu.abort(); + let _ = scu.abort().await; std::process::exit(-2); } } @@ -241,7 +242,7 @@ pub async fn send_file( | pdu @ Pdu::ReleaseRP | pdu @ Pdu::AbortRQ { .. } => { error!("Unexpected SCP response: {:?}", pdu); - let _ = scu.abort(); + let _ = scu.abort().await; std::process::exit(-2); } } diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs index 5cdfe2a93..5e8e02736 100644 --- a/storescu/src/store_sync.rs +++ b/storescu/src/store_sync.rs @@ -17,6 +17,7 @@ use crate::{ UnsupportedFileTransferSyntaxSnafu, }; +#[allow(clippy::too_many_arguments)] pub fn get_scu( addr: String, calling_ae_title: String, @@ -61,7 +62,7 @@ pub fn get_scu( scu_init = scu_init.jwt(jwt); } - Ok(scu_init.establish_with(&addr).context(InitScuSnafu)?) + scu_init.establish_with(&addr).context(InitScuSnafu) } pub fn send_file( diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 6a920e0aa..2869b3cfa 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -158,7 +158,7 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool .to_vec(); reader.consume(recv.len()); read_buffer.extend_from_slice(&recv); - ensure!(recv.len() > 0, ConnectionClosedSnafu); + ensure!(!recv.is_empty(), ConnectionClosedSnafu); }; Ok(msg) } @@ -653,7 +653,7 @@ impl<'a> ClientAssociationOptions<'a> { let mut socket = std::net::TcpStream::connect(ae_address).context(ConnectSnafu)?; socket - .set_read_timeout(timeout.clone()) + .set_read_timeout(timeout) .context(SetReadTimeoutSnafu)?; socket .set_write_timeout(timeout) @@ -897,7 +897,7 @@ impl<'a> ClientAssociationOptions<'a> { source: AbortRQSource::ServiceUser, }, ); - let _ = socket.write_all(&buffer); + let _ = socket.write_all(&buffer).await; buffer.clear(); return NoAcceptedPresentationContextsSnafu.fail(); } @@ -925,7 +925,7 @@ impl<'a> ClientAssociationOptions<'a> { source: AbortRQSource::ServiceUser, }, ); - let _ = socket.write_all(&buffer); + let _ = socket.write_all(&buffer).await; UnexpectedResponseSnafu { pdu }.fail() } pdu @ Pdu::Unknown { .. } => { @@ -936,7 +936,7 @@ impl<'a> ClientAssociationOptions<'a> { source: AbortRQSource::ServiceUser, }, ); - let _ = socket.write_all(&buffer); + let _ = socket.write_all(&buffer).await; UnknownResponseSnafu { pdu }.fail() } } @@ -1120,7 +1120,7 @@ impl ClientAssociation { .to_vec(); reader.consume(recv.len()); self.read_buffer.extend_from_slice(&recv); - ensure!(recv.len() > 0, ConnectionClosedSnafu); + ensure!(!recv.is_empty(), ConnectionClosedSnafu); } } #[cfg(feature = "async")] diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index cd8b3f569..7011a9749 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -21,7 +21,7 @@ use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; /// Set up the P-Data PDU header for sending. -fn setup_pdata_header(buffer: &mut Vec, is_last: bool) { +fn setup_pdata_header(buffer: &mut [u8], is_last: bool) { let data_len = (buffer.len() - 12) as u32; // full PDU length (minus PDU type and reserved byte) @@ -511,7 +511,7 @@ where let recv = reader.fill_buf()?.to_vec(); reader.consume(recv.len()); self.read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { + if recv.is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "Connection closed by peer", @@ -587,7 +587,7 @@ where let recv = ready!(Pin::new(&mut reader).poll_fill_buf(cx))?.to_vec(); reader.consume(recv.len()); read_buffer.extend_from_slice(&recv); - if recv.len() == 0 { + if recv.is_empty() { return Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::Other, "Connection closed by peer", diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 084f13be6..82d3f891f 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -392,7 +392,7 @@ where .to_vec(); reader.consume(recv.len()); read_buffer.extend_from_slice(&recv); - ensure!(recv.len() > 0, ConnectionClosedSnafu); + ensure!(!recv.is_empty(), ConnectionClosedSnafu); }; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); match msg { @@ -883,7 +883,7 @@ impl ServerAssociation { .to_vec(); reader.consume(recv.len()); self.read_buffer.extend_from_slice(&recv); - ensure!(recv.len() > 0, ConnectionClosedSnafu); + ensure!(!recv.is_empty(), ConnectionClosedSnafu); } } diff --git a/ul/src/pdu/reader.rs b/ul/src/pdu/reader.rs index e65ba1159..fabc55370 100644 --- a/ul/src/pdu/reader.rs +++ b/ul/src/pdu/reader.rs @@ -452,7 +452,7 @@ fn read_pdu_variable(mut buf: impl Buf, codec: &dyn TextCodec) -> Result Date: Tue, 27 Aug 2024 08:14:13 -0500 Subject: [PATCH 18/28] MAIN: Update some locks --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b5192af7..bb69e63e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,7 +1408,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1541,7 +1541,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] From 8c5ac5b2ca4897f8bc8217b05a0cc026bc7b0e10 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Tue, 27 Aug 2024 08:28:27 -0500 Subject: [PATCH 19/28] MAIN: Relax requirements for bytes and tokio --- ul/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index d36f2e3eb..423e92db9 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -12,14 +12,14 @@ readme = "README.md" [dependencies] byteordered = "0.6" -bytes = "1.6.1" +bytes = "^1.6" dicom-encoding = { path = "../encoding/", version = "0.7.1" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.1", default-features = false } snafu = "0.8" tracing = "0.1.34" [dependencies.tokio] -version = "1.38.0" +version = "^1.38" optional = true features = [ "rt", @@ -30,7 +30,7 @@ features = [ [dev-dependencies] matches = "0.1.8" -tokio = { version = "1.38.0", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } +tokio = { version = "^1.38", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } [features] async = ["dep:tokio"] From 5a4c2740f7fb01966440c16c0c4f5b63f9ff894e Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 30 Aug 2024 10:31:38 -0500 Subject: [PATCH 20/28] MAIN: Fix !552 in async code as well :) --- ul/src/association/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index ccd230a5d..5c763869a 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -802,7 +802,7 @@ impl<'a> ClientAssociationOptions<'a> { .into_iter() .enumerate() .map(|(i, presentation_context)| PresentationContextProposed { - id: (i + 1) as u8, + id: (2 * i + 1) as u8, abstract_syntax: presentation_context.0.to_string(), transfer_syntaxes: presentation_context .1 From 484225dcefaa53246fd122e28845f74858a1514d Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Thu, 5 Sep 2024 11:53:51 -0500 Subject: [PATCH 21/28] ENH: Move async to separate modules * Allows async and sync to be used in same program WIP [skip ci] --- ul/Cargo.toml | 5 +- ul/src/association/client.rs | 1014 ++++++++++++++++++++-------------- ul/src/association/mod.rs | 6 +- ul/src/association/pdata.rs | 617 ++++++++++----------- ul/src/association/server.rs | 668 ++++++++++++---------- 5 files changed, 1273 insertions(+), 1037 deletions(-) diff --git a/ul/Cargo.toml b/ul/Cargo.toml index 423e92db9..47d6a6e92 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -25,7 +25,8 @@ features = [ "rt", "rt-multi-thread", "net", - "io-util" + "io-util", + "time" ] [dev-dependencies] @@ -34,4 +35,4 @@ tokio = { version = "^1.38", features = ["io-util", "macros", "net", "rt", "rt-m [features] async = ["dep:tokio"] -default = [] +default = ["async"] diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 5c763869a..d77d794f9 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -6,10 +6,10 @@ //! for details and examples on how to create an association. use bytes::BytesMut; use std::{borrow::Cow, convert::TryInto, io::Cursor, net::ToSocketAddrs, time::Duration}; -#[cfg(not(feature = "async"))] -use std::{io::{Write, Read, BufReader, BufRead}, net::TcpStream}; -#[cfg(feature = "async")] -use tokio::{io::{AsyncRead, AsyncWriteExt}, net::TcpStream}; +use std::{ + io::{BufRead, BufReader, Read, Write}, + net::TcpStream, +}; use crate::{ pdu::{ @@ -25,10 +25,8 @@ use snafu::{ensure, Backtrace, ResultExt, Snafu}; use bytes::Buf; use super::{ - //pdata::{PDataReader, PDataWriter}, + pdata::{PDataReader, PDataWriter}, uid::trim_uid, - PDataReader, - PDataWriter, }; #[derive(Debug, Snafu)] @@ -124,15 +122,14 @@ pub enum Error { #[snafu(backtrace)] source: crate::pdu::ReadError, }, + #[snafu(display("Connection closed by peer"))] ConnectionClosed, } pub type Result = std::result::Result; - -#[cfg(not(feature = "async"))] -pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result{ +pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result { // Receive response let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); @@ -163,54 +160,51 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool Ok(msg) } -#[cfg(feature = "async")] -pub async fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result{ - // receive response - use tokio::io::AsyncReadExt; - let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - - let msg = loop { - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, max_pdu_length, strict).context(ReceiveResponseSnafu)? { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu; - } - None => { - // Reset position - buf.set_position(0) - } - } - let recv = reader - .read_buf(&mut read_buffer) - .await - .context(ReadPduSnafu) - .context(ReceiveSnafu)?; - ensure!(recv > 0, ConnectionClosedSnafu); - }; - Ok(msg) -} - /// A DICOM association builder for a client node. /// The final outcome is a [`ClientAssociation`]. /// /// This is the standard way of requesting and establishing /// an association with another DICOM node, /// that one usually taking the role of a service class provider (SCP). +/// +/// You can create either a blocking or non-blocking client by calling either +/// `establish` or `establish_async` respectively. /// -/// # Example +/// > **⚠️ Warning:** It is highly recommended to set `timeout` to a reasonable value for the +/// > async client since there is _no_ default timeout on +/// > [`tokio::net::TcpStream`] +/// +/// ## Basic usage +/// +/// ### Sync /// -#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; +/// # use std::time::Duration; /// # fn run() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_presentation_context("1.2.840.10008.1.1", vec!["1.2.840.10008.1.2.1", "1.2.840.10008.1.2"]) +/// .timeout(Duration::from_secs(60)) /// .establish("129.168.0.5:104")?; /// # Ok(()) /// # } /// ``` -"##)] +/// +/// ### Async +/// +/// ```no_run +/// # use dicom_ul::association::client::ClientAssociationOptions; +/// # use std::time::Duration; +/// # fn run() -> Result<(), Box> { +/// let association = ClientAssociationOptions::new() +/// .with_presentation_context("1.2.840.10008.1.1", vec!["1.2.840.10008.1.2.1", "1.2.840.10008.1.2"]) +/// .timeout(Duration::from_secs(60)) +/// .establish_async("129.168.0.5:104") +/// .await?; +/// # Ok(()) +/// # } +/// ``` +/// /// /// At least one presentation context must be specified, /// using the method [`with_presentation_context`](Self::with_presentation_context) @@ -220,9 +214,7 @@ pub async fn get_client_pdu(reader: &mut R, max_pdu_length /// include by default the transfer syntaxes /// _Implicit VR Little Endian_ and _Explicit VR Little Endian_ /// in the resulting presentation context. -/// # Example /// -#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; /// # fn run() -> Result<(), Box> { @@ -232,7 +224,6 @@ pub async fn get_client_pdu(reader: &mut R, max_pdu_length /// # Ok(()) /// # } /// ``` -"##)] #[derive(Debug, Clone)] pub struct ClientAssociationOptions<'a> { /// the calling AE title @@ -479,23 +470,13 @@ impl<'a> ClientAssociationOptions<'a> { self } - #[cfg(not(feature = "async"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. - pub fn establish(self, address: A) -> Result { + pub fn establish(self, address: A) -> Result> { self.establish_impl(AeAddr::new_socket_addr(address)) } - #[cfg(feature = "async")] - /// Initiate the TCP connection to the given address - /// and request a new DICOM association, - /// negotiating the presentation contexts in the process. - pub async fn establish(self, address: A) -> Result { - self.establish_impl(AeAddr::new_socket_addr(address)).await - } - - #[cfg(not(feature = "async"))] /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. @@ -519,49 +500,13 @@ impl<'a> ClientAssociationOptions<'a> { /// # Ok(()) /// # } /// ``` - pub fn establish_with(self, ae_address: &str) -> Result { + pub fn establish_with(self, ae_address: &str) -> Result> { match ae_address.try_into() { Ok(ae_address) => self.establish_impl(ae_address), Err(_) => self.establish_impl(AeAddr::new_socket_addr(ae_address)), } } - #[cfg(feature = "async")] - /// Initiate the TCP connection to the given address - /// and request a new DICOM association, - /// negotiating the presentation contexts in the process. - /// - /// This method allows you to specify the called AE title - /// alongside with the socket address. - /// See [AeAddr](`crate::AeAddr`) for more details. - /// However, the AE title in this parameter - /// is overridden by any `called_ae_title` option - /// previously received. - /// - /// # Example - /// - /// ```no_run - /// # use dicom_ul::association::client::ClientAssociationOptions; - /// #[tokio::main] - /// # async fn run() -> Result<(), Box> { - /// let association = ClientAssociationOptions::new() - /// .with_abstract_syntax("1.2.840.10008.1.1") - /// // called AE title in address - /// .establish_with("MY-STORAGE@10.0.0.100:104") - /// .await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn establish_with(self, ae_address: &str) -> Result { - match ae_address.try_into() { - Ok(ae_address) => self.establish_impl(ae_address).await, - Err(_) => { - self.establish_impl(AeAddr::new_socket_addr(ae_address)) - .await - } - } - } - /// Set the read timeout for the underlying TCP socket pub fn timeout(self, timeout: Duration) -> Self { Self { @@ -570,8 +515,7 @@ impl<'a> ClientAssociationOptions<'a> { } } - #[cfg(not(feature = "async"))] - fn establish_impl(self, ae_address: AeAddr) -> Result + fn establish_impl(self, ae_address: AeAddr) -> Result> where T: ToSocketAddrs, { @@ -756,191 +700,6 @@ impl<'a> ClientAssociationOptions<'a> { } } - #[cfg(feature = "async")] - async fn establish_impl(self, ae_address: AeAddr) -> Result - where - T: ToSocketAddrs, - { - let ClientAssociationOptions { - calling_ae_title, - called_ae_title, - application_context_name, - presentation_contexts, - protocol_version, - max_pdu_length, - strict, - username, - password, - kerberos_service_ticket, - saml_assertion, - jwt, - timeout, - } = self; - - // fail if no presentation contexts were provided: they represent intent, - // should not be omitted by the user - ensure!( - !presentation_contexts.is_empty(), - MissingAbstractSyntaxSnafu - ); - - // choose called AE title - let called_ae_title: &str = match (&called_ae_title, ae_address.ae_title()) { - (Some(aec), Some(_)) => { - tracing::warn!( - "Option `called_ae_title` overrides the AE title to `{}`", - aec - ); - aec - } - (Some(aec), None) => aec, - (None, Some(aec)) => aec, - (None, None) => "ANY-SCP", - }; - - let presentation_contexts: Vec<_> = presentation_contexts - .into_iter() - .enumerate() - .map(|(i, presentation_context)| PresentationContextProposed { - id: (2 * i + 1) as u8, - abstract_syntax: presentation_context.0.to_string(), - transfer_syntaxes: presentation_context - .1 - .iter() - .map(|uid| uid.to_string()) - .collect(), - }) - .collect(); - - let mut user_variables = vec![ - UserVariableItem::MaxLength(max_pdu_length), - UserVariableItem::ImplementationClassUID(IMPLEMENTATION_CLASS_UID.to_string()), - UserVariableItem::ImplementationVersionName(IMPLEMENTATION_VERSION_NAME.to_string()), - ]; - - if let Some(user_identity) = Self::determine_user_identity( - username, - password, - kerberos_service_ticket, - saml_assertion, - jwt, - ) { - user_variables.push(UserVariableItem::UserIdentityItem(user_identity)); - } - - let msg = Pdu::AssociationRQ(AssociationRQ { - protocol_version, - calling_ae_title: calling_ae_title.to_string(), - called_ae_title: called_ae_title.to_string(), - application_context_name: application_context_name.to_string(), - presentation_contexts, - user_variables, - }); - let socket_addrs: Vec<_> = ae_address.to_socket_addrs().unwrap().collect(); - - // TODO: add tokio-time flag and set timeouts for this and send/receive - let mut socket = TcpStream::connect(socket_addrs.as_slice()) - .await - .context(ConnectSnafu)?; - let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); - // send request - - write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - buffer.clear(); - - // receive response - let msg = get_client_pdu(&mut socket, MAXIMUM_PDU_SIZE, self.strict) - .await?; - - match msg { - Pdu::AssociationAC(AssociationAC { - protocol_version: protocol_version_scp, - application_context_name: _, - presentation_contexts: presentation_contexts_scp, - calling_ae_title: _, - called_ae_title: _, - user_variables, - }) => { - ensure!( - protocol_version == protocol_version_scp, - ProtocolVersionMismatchSnafu { - expected: protocol_version, - got: protocol_version_scp, - } - ); - - let acceptor_max_pdu_length = user_variables - .iter() - .find_map(|item| match item { - UserVariableItem::MaxLength(len) => Some(*len), - _ => None, - }) - .unwrap_or(DEFAULT_MAX_PDU); - - // treat 0 as the maximum size admitted by the standard - let acceptor_max_pdu_length = if acceptor_max_pdu_length == 0 { - MAXIMUM_PDU_SIZE - } else { - acceptor_max_pdu_length - }; - - let presentation_contexts: Vec<_> = presentation_contexts_scp - .into_iter() - .filter(|c| c.reason == PresentationContextResultReason::Acceptance) - .collect(); - if presentation_contexts.is_empty() { - // abort connection - let _ = write_pdu( - &mut buffer, - &Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }, - ); - let _ = socket.write_all(&buffer).await; - buffer.clear(); - return NoAcceptedPresentationContextsSnafu.fail(); - } - Ok(ClientAssociation { - presentation_contexts, - requestor_max_pdu_length: max_pdu_length, - acceptor_max_pdu_length, - socket, - buffer, - strict, - timeout, - read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), - }) - } - Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), - pdu @ Pdu::AbortRQ { .. } - | pdu @ Pdu::ReleaseRQ { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRP { .. } => { - // abort connection - let _ = write_pdu( - &mut buffer, - &Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }, - ); - let _ = socket.write_all(&buffer).await; - UnexpectedResponseSnafu { pdu }.fail() - } - pdu @ Pdu::Unknown { .. } => { - // abort connection - let _ = write_pdu( - &mut buffer, - &Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }, - ); - let _ = socket.write_all(&buffer).await; - UnknownResponseSnafu { pdu }.fail() - } - } - } fn determine_user_identity( username: Option, password: Option, @@ -1000,6 +759,25 @@ impl<'a> ClientAssociationOptions<'a> { } } +pub trait CloseSocket { + fn close(&mut self) -> std::io::Result<()>; +} + +impl CloseSocket for TcpStream { + fn close(&mut self) -> std::io::Result<()> { + self.shutdown(std::net::Shutdown::Both) + } +} +pub trait Release { + fn release(&mut self) -> Result<()>; +} + +impl Release for ClientAssociation { + fn release(&mut self) -> Result<()> { + self.release_impl() + } +} + /// A DICOM upper level association from the perspective /// of a requesting application entity. /// @@ -1014,7 +792,11 @@ impl<'a> ClientAssociationOptions<'a> { /// through a standard C-RELEASE message exchange, /// then shut down the underlying TCP connection. #[derive(Debug)] -pub struct ClientAssociation { +pub struct ClientAssociation +where + S: CloseSocket, + ClientAssociation: Release, +{ /// The presentation contexts accorded with the acceptor application entity, /// without the rejected ones. presentation_contexts: Vec, @@ -1023,18 +805,21 @@ pub struct ClientAssociation { /// The maximum PDU length that the remote application entity accepts acceptor_max_pdu_length: u32, /// The TCP stream to the other DICOM node - socket: TcpStream, + socket: S, /// Buffer to assemble PDU before sending it on wire buffer: Vec, /// whether to receive PDUs in strict mode strict: bool, - /// Send/Receive operation timeout + /// Timeout for individual Send/Receive operations timeout: Option, /// Buffer to assemble PDU before parsing read_buffer: BytesMut, } -impl ClientAssociation { +impl ClientAssociation +where + ClientAssociation: Release, +{ /// Retrieve timeout for the association pub fn timeout(&self) -> Option { self.timeout @@ -1059,8 +844,12 @@ impl ClientAssociation { pub fn requestor_max_pdu_length(&self) -> u32 { self.requestor_max_pdu_length } +} - #[cfg(not(feature = "async"))] +impl ClientAssociation +where + ClientAssociation: Release, +{ /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -1074,24 +863,6 @@ impl ClientAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } - #[cfg(feature = "async")] - /// Send a PDU message to the other intervenient. - pub async fn send(&mut self, msg: &Pdu) -> Result<()> { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendSnafu)?; - if self.buffer.len() > self.acceptor_max_pdu_length as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), - } - .fail(); - } - self.socket - .write_all(&self.buffer) - .await - .context(WireSendSnafu) - } - - #[cfg(not(feature = "async"))] /// Read a PDU message from the other intervenient. pub fn receive(&mut self) -> Result { use std::io::{BufRead, BufReader, Cursor}; @@ -1123,38 +894,7 @@ impl ClientAssociation { ensure!(!recv.is_empty(), ConnectionClosedSnafu); } } - #[cfg(feature = "async")] - /// Read a PDU message from the other intervenient. - pub async fn receive(&mut self) -> Result { - use std::io::Cursor; - use tokio::io::AsyncReadExt; - - loop { - let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) - .context(ReceiveResponseSnafu)? - { - Some(pdu) => { - self.read_buffer.advance(buf.position() as usize); - return Ok(pdu); - } - None => { - // Reset position - buf.set_position(0) - } - } - let recv = self - .socket - .read_buf(&mut self.read_buffer) - .await - .context(ReadPduSnafu) - .context(ReceiveSnafu)?; - ensure!(recv > 0, ConnectionClosedSnafu); - } - } - - #[cfg(not(feature = "async"))] /// Gracefully terminate the association by exchanging release messages /// and then shutting down the TCP connection. pub fn release(mut self) -> Result<()> { @@ -1163,16 +903,6 @@ impl ClientAssociation { out } - #[cfg(feature = "async")] - /// Gracefully terminate the association by exchanging release messages - /// and then shutting down the TCP connection. - pub async fn release(mut self) -> Result<()> { - let out = self.release_impl().await; - let _ = self.socket.shutdown().await; - out - } - - #[cfg(not(feature = "async"))] /// Send an abort message and shut down the TCP connection, /// terminating the association. pub fn abort(mut self) -> Result<()> { @@ -1184,18 +914,6 @@ impl ClientAssociation { out } - #[cfg(feature = "async")] - /// Send an abort message and shut down the TCP connection, - /// terminating the association. - pub async fn abort(mut self) -> Result<()> { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }; - let out = self.send(&pdu).await; - let _ = self.socket.shutdown().await; - out - } - /// Obtain access to the inner TCP stream /// connected to the association acceptor. /// @@ -1214,7 +932,6 @@ impl ClientAssociation { /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - #[cfg(not(feature = "async"))] pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { PDataWriter::new( &mut self.socket, @@ -1223,41 +940,15 @@ impl ClientAssociation { ) } - /// Prepare a P-Data writer for sending - /// one or more data items. - /// - /// Returns a writer which automatically - /// splits the inner data into separate PDUs if necessary. - #[cfg(feature = "async")] - pub async fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - PDataWriter::new( - &mut self.socket, - presentation_context_id, - self.acceptor_max_pdu_length, - ) - } - - /// 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. - #[cfg(not(feature = "async"))] - pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) - } - /// 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. - #[cfg(feature = "async")] pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length, self.timeout) } - #[cfg(not(feature = "async"))] /// Release implementation function, /// which tries to send a release request and receive a release response. /// This is in a separate private function because @@ -1280,63 +971,538 @@ impl ClientAssociation { } Ok(()) } +} - #[cfg(feature = "async")] - /// Release implementation function, - /// which tries to send a release request and receive a release response. - /// This is in a separate private function because - /// terminating a connection should still close the connection - /// if the exchange fails. - async fn release_impl(&mut self) -> Result<()> { - let pdu = Pdu::ReleaseRQ; - self.send(&pdu).await?; +/// Automatically release the association and shut down the connection. +impl Drop for ClientAssociation +where + T: CloseSocket, + ClientAssociation: Release, +{ + fn drop(&mut self) { + let _ = self.release(); + let _ = self.socket.close(); + } +} + +#[cfg(feature = "async")] +pub mod non_blocking { + use std::{convert::TryInto, io::Cursor, net::ToSocketAddrs}; + + use crate::{ + association::{ + client::{ + ConnectSnafu, ConnectionClosedSnafu, MissingAbstractSyntaxSnafu, + NoAcceptedPresentationContextsSnafu, ProtocolVersionMismatchSnafu, + ReceiveResponseSnafu, ReceiveSnafu, RejectedSnafu, SendRequestSnafu, + UnexpectedResponseSnafu, UnknownResponseSnafu, + }, + pdata::non_blocking::{PDataReader, AsyncPDataWriter} + }, + pdu::{ + AbortRQSource, AssociationAC, AssociationRQ, PresentationContextProposed, + PresentationContextResultReason, ReadPduSnafu, UserVariableItem, DEFAULT_MAX_PDU, + MAXIMUM_PDU_SIZE, + }, + read_pdu, write_pdu, AeAddr, Pdu, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, + }; + + use super::{ + ClientAssociation, ClientAssociationOptions, CloseSocket, Release, Result, SendSnafu, + SendTooLongPduSnafu, WireSendSnafu, + }; + use bytes::{Buf, BytesMut}; + use snafu::{ensure, ResultExt}; + use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + use tracing::warn; + + pub async fn get_client_pdu_async( + reader: &mut R, + max_pdu_length: u32, + strict: bool, + ) -> Result { + // receive response use tokio::io::AsyncReadExt; let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - let pdu = loop { - if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { - break pdu; + let msg = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, max_pdu_length, strict).context(ReceiveResponseSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu; + } + None => { + // Reset position + buf.set_position(0) + } } - let recv = self - .socket + let recv = reader .read_buf(&mut read_buffer) .await .context(ReadPduSnafu) .context(ReceiveSnafu)?; ensure!(recv > 0, ConnectionClosedSnafu); }; - match pdu { - Pdu::ReleaseRP => {} - pdu @ Pdu::AbortRQ { .. } - | pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRQ { .. } => return UnexpectedResponseSnafu { pdu }.fail(), - pdu @ Pdu::Unknown { .. } => return UnknownResponseSnafu { pdu }.fail(), - } - Ok(()) + Ok(msg) } -} -#[cfg(not(feature = "async"))] -/// Automatically release the association and shut down the connection. -impl Drop for ClientAssociation { - fn drop(&mut self) { - let _ = self.release_impl(); - let _ = self.socket.shutdown(std::net::Shutdown::Both); + impl<'a> ClientAssociationOptions<'a> { + async fn establish_impl_async( + self, + ae_address: AeAddr, + ) -> Result> + where + T: ToSocketAddrs, + { + let timeout = self.timeout; + let task = async { + let ClientAssociationOptions { + calling_ae_title, + called_ae_title, + application_context_name, + presentation_contexts, + protocol_version, + max_pdu_length, + strict, + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + timeout, + } = self; + + // fail if no presentation contexts were provided: they represent intent, + // should not be omitted by the user + ensure!( + !presentation_contexts.is_empty(), + MissingAbstractSyntaxSnafu + ); + + // choose called AE title + let called_ae_title: &str = match (&called_ae_title, ae_address.ae_title()) { + (Some(aec), Some(_)) => { + tracing::warn!( + "Option `called_ae_title` overrides the AE title to `{}`", + aec + ); + aec + } + (Some(aec), None) => aec, + (None, Some(aec)) => aec, + (None, None) => "ANY-SCP", + }; + + let presentation_contexts: Vec<_> = presentation_contexts + .into_iter() + .enumerate() + .map(|(i, presentation_context)| PresentationContextProposed { + id: (2 * i + 1) as u8, + abstract_syntax: presentation_context.0.to_string(), + transfer_syntaxes: presentation_context + .1 + .iter() + .map(|uid| uid.to_string()) + .collect(), + }) + .collect(); + + let mut user_variables = vec![ + UserVariableItem::MaxLength(max_pdu_length), + UserVariableItem::ImplementationClassUID(IMPLEMENTATION_CLASS_UID.to_string()), + UserVariableItem::ImplementationVersionName( + IMPLEMENTATION_VERSION_NAME.to_string(), + ), + ]; + + if let Some(user_identity) = Self::determine_user_identity( + username, + password, + kerberos_service_ticket, + saml_assertion, + jwt, + ) { + user_variables.push(UserVariableItem::UserIdentityItem(user_identity)); + } + + let msg = Pdu::AssociationRQ(AssociationRQ { + protocol_version, + calling_ae_title: calling_ae_title.to_string(), + called_ae_title: called_ae_title.to_string(), + application_context_name: application_context_name.to_string(), + presentation_contexts, + user_variables, + }); + let socket_addrs: Vec<_> = ae_address.to_socket_addrs().unwrap().collect(); + + let mut socket = TcpStream::connect(socket_addrs.as_slice()) + .await + .context(ConnectSnafu)?; + let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); + // send request + + write_pdu(&mut buffer, &msg).context(SendRequestSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + buffer.clear(); + + // receive response + let msg = get_client_pdu_async(&mut socket, MAXIMUM_PDU_SIZE, self.strict).await?; + + match msg { + Pdu::AssociationAC(AssociationAC { + protocol_version: protocol_version_scp, + application_context_name: _, + presentation_contexts: presentation_contexts_scp, + calling_ae_title: _, + called_ae_title: _, + user_variables, + }) => { + ensure!( + protocol_version == protocol_version_scp, + ProtocolVersionMismatchSnafu { + expected: protocol_version, + got: protocol_version_scp, + } + ); + + let acceptor_max_pdu_length = user_variables + .iter() + .find_map(|item| match item { + UserVariableItem::MaxLength(len) => Some(*len), + _ => None, + }) + .unwrap_or(DEFAULT_MAX_PDU); + + // treat 0 as the maximum size admitted by the standard + let acceptor_max_pdu_length = if acceptor_max_pdu_length == 0 { + MAXIMUM_PDU_SIZE + } else { + acceptor_max_pdu_length + }; + + let presentation_contexts: Vec<_> = presentation_contexts_scp + .into_iter() + .filter(|c| c.reason == PresentationContextResultReason::Acceptance) + .collect(); + if presentation_contexts.is_empty() { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer).await; + buffer.clear(); + return NoAcceptedPresentationContextsSnafu.fail(); + } + Ok(ClientAssociation { + presentation_contexts, + requestor_max_pdu_length: max_pdu_length, + acceptor_max_pdu_length, + socket, + buffer, + strict, + timeout, + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), + }) + } + Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::ReleaseRQ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRP { .. } => { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer).await; + UnexpectedResponseSnafu { pdu }.fail() + } + pdu @ Pdu::Unknown { .. } => { + // abort connection + let _ = write_pdu( + &mut buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + let _ = socket.write_all(&buffer).await; + UnknownResponseSnafu { pdu }.fail() + } + } + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(ConnectSnafu)? + } else { + warn!("No timeout set. It is highly recommended to set a timeout."); + task.await + } + + } + + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + pub async fn establish_async( + self, + address: A, + ) -> Result> { + self.establish_impl_async(AeAddr::new_socket_addr(address)) + .await + } + + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + /// + /// This method allows you to specify the called AE title + /// alongside with the socket address. + /// See [AeAddr](`crate::AeAddr`) for more details. + /// However, the AE title in this parameter + /// is overridden by any `called_ae_title` option + /// previously received. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_ul::association::client::ClientAssociationOptions; + /// #[tokio::main] + /// # async fn run() -> Result<(), Box> { + /// let association = ClientAssociationOptions::new() + /// .with_abstract_syntax("1.2.840.10008.1.1") + /// // called AE title in address + /// .establish_with_async("MY-STORAGE@10.0.0.100:104") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn establish_with_async( + self, + ae_address: &str, + ) -> Result> { + match ae_address.try_into() { + Ok(ae_address) => self.establish_impl_async(ae_address).await, + Err(_) => { + self.establish_impl_async(AeAddr::new_socket_addr(ae_address)) + .await + } + } + } } -} -#[cfg(feature = "async")] -/// Automatically release the association and shut down the connection. -impl Drop for ClientAssociation { - fn drop(&mut self) { - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - let _ = self.release_impl().await; + impl ClientAssociation + where + ClientAssociation: Release, + { + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + let timeout = self.timeout; + let task = async { + self.buffer.clear(); + write_pdu(&mut self.buffer, msg).context(SendSnafu)?; + if self.buffer.len() > self.acceptor_max_pdu_length as usize { + return SendTooLongPduSnafu { + length: self.buffer.len(), + } + .fail(); + } + self.socket + .write_all(&self.buffer) + .await + .context(WireSendSnafu) + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireSendSnafu)? + + } else { + task.await + } + } + + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + let timeout = self.timeout; + let task = async { + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) + .context(ReceiveResponseSnafu)? + { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu); + } + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self + .socket + .read_buf(&mut self.read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); + } + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + + } else { + task.await + } + } + + /// Gracefully terminate the association by exchanging release messages + /// and then shutting down the TCP connection. + pub async fn release(mut self) -> Result<()> { + let timeout = self.timeout; + let task = async { + let out = self.release_impl().await; let _ = self.socket.shutdown().await; + out + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireSendSnafu)? + } else { + task.await + } + } + + /// Send an abort message and shut down the TCP connection, + /// terminating the association. + pub async fn abort(mut self) -> Result<()> { + let timeout = self.timeout; + let task = async { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }; + let out = self.send(&pdu).await; + let _ = self.socket.shutdown().await; + out + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireSendSnafu)? + } else { + task.await + } + } + + /// Prepare a P-Data writer for sending + /// one or more data items. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + pub async fn send_pdata( + &mut self, + presentation_context_id: u8, + ) -> AsyncPDataWriter<&mut TcpStream> { + AsyncPDataWriter::new( + &mut self.socket, + presentation_context_id, + self.acceptor_max_pdu_length, + self.timeout + ) + } + + /// 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. + #[cfg(feature = "async")] + pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length, self.timeout) + } + + /// Release implementation function, + /// which tries to send a release request and receive a release response. + /// This is in a separate private function because + /// terminating a connection should still close the connection + /// if the exchange fails. + async fn release_impl(&mut self) -> Result<()> { + let pdu = Pdu::ReleaseRQ; + self.send(&pdu).await?; + use tokio::io::AsyncReadExt; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + + let pdu = loop { + if let Ok(Some(pdu)) = read_pdu(&mut read_buffer, MAXIMUM_PDU_SIZE, self.strict) { + break pdu; + } + let recv = self + .socket + .read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); + }; + match pdu { + Pdu::ReleaseRP => {} + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRQ { .. } => return UnexpectedResponseSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => return UnknownResponseSnafu { pdu }.fail(), + } + Ok(()) + } + /// Obtain access to the inner TCP 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 TcpStream { + &mut self.socket + } + } + + impl Release for ClientAssociation { + fn release(&mut self) -> super::Result<()> { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { self.release_impl().await }) + }) + } + } + /// Automatically release the association and shut down the connection. + impl CloseSocket for TcpStream { + fn close(&mut self) -> std::io::Result<()> { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { self.shutdown().await }) }) - }) + } } } diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index c2f9d1516..dd510bd1a 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -17,14 +17,10 @@ //! [1]: std::net::TcpStream pub mod client; pub mod server; + mod uid; pub(crate) mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; -#[cfg(feature = "async")] -pub use pdata::AsyncPDataWriter as PDataWriter; -pub use pdata::PDataReader; -#[cfg(not(feature = "async"))] -pub use pdata::PDataWriter; pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 7011a9749..f3b86e34b 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,25 +1,13 @@ -#[cfg(not(feature = "async"))] -use std::io::Write; use std::{ collections::VecDeque, - io::{BufRead, BufReader, Cursor, Read} + io::{BufRead, BufReader, Cursor, Read, Write}, time::Duration }; use bytes::{Buf, BytesMut}; -#[cfg(feature = "async")] -use std::{ - pin::Pin, - task::{Context, Poll}, -}; -#[cfg(feature = "async")] -use tokio::io::ReadBuf; use tracing::warn; use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; -#[cfg(feature = "async")] -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; - /// Set up the P-Data PDU header for sending. fn setup_pdata_header(buffer: &mut [u8], is_last: bool) { let data_len = (buffer.len() - 12) as u32; @@ -89,7 +77,6 @@ fn setup_pdata_header(buffer: &mut [u8], is_last: bool) { /// let pdu_ac = association.receive()?; /// # Ok(()) /// # } -#[cfg(not(feature = "async"))] #[must_use] pub struct PDataWriter { buffer: Vec, @@ -97,7 +84,6 @@ pub struct PDataWriter { max_data_len: u32, } -#[cfg(not(feature = "async"))] impl PDataWriter where W: Write, @@ -174,7 +160,6 @@ where } } -#[cfg(not(feature = "async"))] impl Write for PDataWriter where W: Write, @@ -206,7 +191,6 @@ where /// this `Drop` implementation /// will construct and emit the last P-Data fragment PDU /// if there is any data left to send. -#[cfg(not(feature = "async"))] impl Drop for PDataWriter where W: Write, @@ -216,203 +200,6 @@ where } } -/// A P-Data async value writer. -/// -/// This exposes an API to iteratively construct and send Data messages -/// to another node. -/// Using this as a [standard writer](std::io::Write) -/// will automatically split the incoming bytes -/// into separate PDUs if they do not fit in a single one. -/// -/// # Example -/// -/// Use an association's `send_pdata` method -/// to create a new P-Data value writer. -/// -/// ```no_run -/// # use std::io::Write; -/// use tokio::io::AsyncWriteExt; -/// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; -/// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; -/// # fn command_data() -> Vec { unimplemented!() } -/// # fn dicom_data() -> &'static [u8] { unimplemented!() } -/// #[tokio::main] -/// # async fn main() -> Result<(), Box> { -/// let mut association = ClientAssociationOptions::new() -/// .establish("129.168.0.5:104") -/// .await?; -/// -/// let presentation_context_id = association.presentation_contexts()[0].id; -/// -/// // send a command first -/// association.send(&Pdu::PData { -/// data: vec![PDataValue { -/// presentation_context_id, -/// value_type: PDataValueType::Command, -/// is_last: true, -/// data: command_data(), -/// }], -/// }).await; -/// -/// // then send a DICOM object which may be split into multiple PDUs -/// let mut pdata = association.send_pdata(presentation_context_id).await; -/// pdata.write_all(dicom_data()).await?; -/// pdata.finish().await?; -/// -/// let pdu_ac = association.receive().await?; -/// # Ok(()) -/// # } -/// ``` -#[cfg(feature = "async")] -#[must_use] -pub struct AsyncPDataWriter { - buffer: Vec, - stream: W, - max_data_len: u32, -} - -#[cfg(feature = "async")] -impl AsyncPDataWriter -where - W: AsyncWrite + Unpin, -{ - /// Construct a new P-Data value writer. - /// - /// `max_pdu_length` is the maximum value of the PDU-length property. - pub(crate) fn new(stream: W, presentation_context_id: u8, max_pdu_length: u32) -> Self { - let max_data_length = calculate_max_data_len_single(max_pdu_length); - let mut buffer = Vec::with_capacity((max_data_length + PDU_HEADER_SIZE) as usize); - // initial buffer set up - buffer.extend([ - // PDU-type + reserved byte - 0x04, - 0x00, - // full PDU length, unknown at this point - 0xFF, - 0xFF, - 0xFF, - 0xFF, - // presentation data length, unknown at this point - 0xFF, - 0xFF, - 0xFF, - 0xFF, - // presentation context id - presentation_context_id, - // message control header, unknown at this point - 0xFF, - ]); - - AsyncPDataWriter { - stream, - max_data_len: max_data_length, - buffer, - } - } - - /// Declare to have finished sending P-Data fragments, - /// thus emitting the last P-Data fragment PDU. - /// - /// This is also done automatically once the P-Data writer is dropped. - pub async fn finish(mut self) -> std::io::Result<()> { - self.finish_impl().await?; - Ok(()) - } - - async fn finish_impl(&mut self) -> std::io::Result<()> { - if !self.buffer.is_empty() { - // send last PDU - setup_pdata_header(&mut self.buffer, true); - self.stream.write_all(&self.buffer[..]).await?; - // clear buffer so that subsequent calls to `finish_impl` - // do not send any more PDUs - self.buffer.clear(); - } - Ok(()) - } - - /// Use the current state of the buffer to send new PDUs - /// - /// Pre-condition: - /// buffer must have enough data for one P-Data-tf PDU - async fn dispatch_pdu(&mut self) -> std::io::Result<()> { - debug_assert!(self.buffer.len() >= 12); - // send PDU now - setup_pdata_header(&mut self.buffer, false); - self.stream.write_all(&self.buffer).await?; - - // back to just the header - self.buffer.truncate(12); - - Ok(()) - } -} - -#[cfg(feature = "async")] -impl AsyncWrite for AsyncPDataWriter -where - W: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - use std::future::Future; - let total_len = self.max_data_len as usize + 12; - if self.buffer.len() + buf.len() <= total_len { - // accumulate into buffer, do nothing - self.buffer.extend(buf); - Poll::Ready(Ok(buf.len())) - } else { - // fill in the rest of the buffer, send PDU, - // and leave out the rest for subsequent writes - let buf = &buf[..total_len - self.buffer.len()]; - self.buffer.extend(buf); - debug_assert_eq!(self.buffer.len(), total_len); - let dispatch = self.dispatch_pdu(); - tokio::pin!(dispatch); - match dispatch.poll(cx) { - Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - } - } - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.stream).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.stream).poll_shutdown(cx) - } -} - -/// With the P-Data writer dropped, -/// this `Drop` implementation -/// will construct and emit the last P-Data fragment PDU -/// if there is any data left to send. -#[cfg(feature = "async")] -impl Drop for AsyncPDataWriter -where - W: AsyncWrite + Unpin, -{ - fn drop(&mut self) { - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - let _ = self.finish_impl().await; - }) - }) - } -} - /// A P-Data value reader. /// /// This exposes an API which provides a byte stream of data @@ -427,7 +214,6 @@ where /// Use an association's `receive_pdata` method /// to create a new P-Data value reader. /// -#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use std::io::Read; /// # use dicom_ul::association::{ClientAssociationOptions, PDataReader}; @@ -448,7 +234,6 @@ where /// # Ok(()) /// # } /// ``` -"##)] #[must_use] pub struct PDataReader { buffer: VecDeque, @@ -457,10 +242,11 @@ pub struct PDataReader { max_data_length: u32, last_pdu: bool, read_buffer: BytesMut, + timeout: Option } impl PDataReader { - pub fn new(stream: R, max_data_length: u32) -> Self { + pub fn new(stream: R, max_data_length: u32, timeout: Option) -> Self { PDataReader { buffer: VecDeque::with_capacity(max_data_length as usize), stream, @@ -468,6 +254,7 @@ impl PDataReader { max_data_length, last_pdu: false, read_buffer: BytesMut::with_capacity(max_data_length as usize), + timeout } } @@ -546,115 +333,321 @@ where } } + +/// Determine the maximum length of actual PDV data +/// when encapsulated in a PDU with the given length property. +/// Does not account for the first 2 bytes (type + reserved). +#[inline] +fn calculate_max_data_len_single(pdu_len: u32) -> u32 { + // data length: 4 bytes + // control header: 2 bytes + pdu_len - 4 - 2 +} + #[cfg(feature = "async")] -impl AsyncRead for PDataReader -where - R: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf, - ) -> Poll> { - use tokio::io::{BufReader, AsyncBufRead, AsyncBufReadExt}; - use bytes::BufMut; - use std::task::ready; - if self.buffer.is_empty(){ - if self.last_pdu { - return Poll::Ready(Ok(())); +pub mod non_blocking { + use std::{io::Cursor, pin::Pin, task::{ready, Context, Poll}, time::Duration}; + + use bytes::{Buf, BufMut}; + use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf }; + use tracing::warn; + + use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; + + use super::{calculate_max_data_len_single, setup_pdata_header}; + pub use super::PDataReader; + + /// A P-Data async value writer. + /// + /// This exposes an API to iteratively construct and send Data messages + /// to another node. + /// Using this as a [standard writer](std::io::Write) + /// will automatically split the incoming bytes + /// into separate PDUs if they do not fit in a single one. + /// + /// # Example + /// + /// Use an association's `send_pdata` method + /// to create a new P-Data value writer. + /// + /// ```no_run + /// # use std::io::Write; + /// use tokio::io::AsyncWriteExt; + /// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; + /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; + /// # fn command_data() -> Vec { unimplemented!() } + /// # fn dicom_data() -> &'static [u8] { unimplemented!() } + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let mut association = ClientAssociationOptions::new() + /// .establish("129.168.0.5:104") + /// .await?; + /// + /// let presentation_context_id = association.presentation_contexts()[0].id; + /// + /// // send a command first + /// association.send(&Pdu::PData { + /// data: vec![PDataValue { + /// presentation_context_id, + /// value_type: PDataValueType::Command, + /// is_last: true, + /// data: command_data(), + /// }], + /// }).await; + /// + /// // then send a DICOM object which may be split into multiple PDUs + /// let mut pdata = association.send_pdata(presentation_context_id).await; + /// pdata.write_all(dicom_data()).await?; + /// pdata.finish().await?; + /// + /// let pdu_ac = association.receive().await?; + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub struct AsyncPDataWriter { + buffer: Vec, + stream: W, + max_data_len: u32, + timeout: Option + } + + #[cfg(feature = "async")] + impl AsyncPDataWriter + where + W: AsyncWrite + Unpin, + { + /// Construct a new P-Data value writer. + /// + /// `max_pdu_length` is the maximum value of the PDU-length property. + pub(crate) fn new(stream: W, presentation_context_id: u8, max_pdu_length: u32, timeout: Option) -> Self { + let max_data_length = calculate_max_data_len_single(max_pdu_length); + let mut buffer = Vec::with_capacity((max_data_length + PDU_HEADER_SIZE) as usize); + // initial buffer set up + buffer.extend([ + // PDU-type + reserved byte + 0x04, + 0x00, + // full PDU length, unknown at this point + 0xFF, + 0xFF, + 0xFF, + 0xFF, + // presentation data length, unknown at this point + 0xFF, + 0xFF, + 0xFF, + 0xFF, + // presentation context id + presentation_context_id, + // message control header, unknown at this point + 0xFF, + ]); + + AsyncPDataWriter { + stream, + max_data_len: max_data_length, + buffer, + timeout } - let Self { - ref mut stream, - ref mut read_buffer, - ref max_data_length, - .. - } = &mut *self; - let mut reader = BufReader::new(stream); - let msg = loop { - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, *max_data_length, false) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? - { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu; - } - None => { - // Reset position - buf.set_position(0) - } + } + + /// Declare to have finished sending P-Data fragments, + /// thus emitting the last P-Data fragment PDU. + /// + /// This is also done automatically once the P-Data writer is dropped. + pub async fn finish(mut self) -> std::io::Result<()> { + self.finish_impl().await?; + Ok(()) + } + + async fn finish_impl(&mut self) -> std::io::Result<()> { + if !self.buffer.is_empty() { + // send last PDU + setup_pdata_header(&mut self.buffer, true); + self.stream.write_all(&self.buffer[..]).await?; + // clear buffer so that subsequent calls to `finish_impl` + // do not send any more PDUs + self.buffer.clear(); + } + Ok(()) + } + + /// Use the current state of the buffer to send new PDUs + /// + /// Pre-condition: + /// buffer must have enough data for one P-Data-tf PDU + async fn dispatch_pdu(&mut self) -> std::io::Result<()> { + debug_assert!(self.buffer.len() >= 12); + // send PDU now + setup_pdata_header(&mut self.buffer, false); + self.stream.write_all(&self.buffer).await?; + + // back to just the header + self.buffer.truncate(12); + + Ok(()) + } + } + + #[cfg(feature = "async")] + impl AsyncWrite for AsyncPDataWriter + where + W: AsyncWrite + Unpin, + { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + use std::future::Future; + let total_len = self.max_data_len as usize + 12; + if self.buffer.len() + buf.len() <= total_len { + // accumulate into buffer, do nothing + self.buffer.extend(buf); + Poll::Ready(Ok(buf.len())) + } else { + // fill in the rest of the buffer, send PDU, + // and leave out the rest for subsequent writes + let buf = &buf[..total_len - self.buffer.len()]; + self.buffer.extend(buf); + debug_assert_eq!(self.buffer.len(), total_len); + let dispatch = self.dispatch_pdu(); + tokio::pin!(dispatch); + match dispatch.poll(cx) { + Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, } - let recv = ready!(Pin::new(&mut reader).poll_fill_buf(cx))?.to_vec(); - reader.consume(recv.len()); - read_buffer.extend_from_slice(&recv); - if recv.is_empty() { - return Poll::Ready(Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Connection closed by peer", - ))); + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.stream).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.stream).poll_shutdown(cx) + } + } + + /// With the P-Data writer dropped, + /// this `Drop` implementation + /// will construct and emit the last P-Data fragment PDU + /// if there is any data left to send. + impl Drop for AsyncPDataWriter + where + W: AsyncWrite + Unpin, + { + fn drop(&mut self) { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + let _ = self.finish_impl().await; + }) + }) + } + } + + impl AsyncRead for PDataReader + where + R: AsyncRead + Unpin, + { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf, + ) -> Poll> { + if self.buffer.is_empty(){ + if self.last_pdu { + return Poll::Ready(Ok(())); } - }; - match msg { - Pdu::PData { data } => { - for pdata_value in data { - self.presentation_context_id = match self.presentation_context_id { - None => Some(pdata_value.presentation_context_id), - Some(cid) if cid == pdata_value.presentation_context_id => Some(cid), - Some(cid) => { - warn!("Received PData value of presentation context {}, but should be {}", pdata_value.presentation_context_id, cid); - Some(cid) - } - }; - self.buffer.extend(pdata_value.data); - self.last_pdu = pdata_value.is_last; + let Self { + ref mut stream, + ref mut read_buffer, + ref max_data_length, + .. + } = &mut *self; + let mut reader = BufReader::new(stream); + let msg = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, *max_data_length, false) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu; + } + None => { + // Reset position + buf.set_position(0) + } + } + let recv = ready!(Pin::new(&mut reader).poll_fill_buf(cx))?.to_vec(); + reader.consume(recv.len()); + read_buffer.extend_from_slice(&recv); + if recv.is_empty() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Connection closed by peer", + ))); + } + }; + match msg { + Pdu::PData { data } => { + for pdata_value in data { + self.presentation_context_id = match self.presentation_context_id { + None => Some(pdata_value.presentation_context_id), + Some(cid) if cid == pdata_value.presentation_context_id => Some(cid), + Some(cid) => { + warn!("Received PData value of presentation context {}, but should be {}", pdata_value.presentation_context_id, cid); + Some(cid) + } + }; + self.buffer.extend(pdata_value.data); + self.last_pdu = pdata_value.is_last; + } + } + _ => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Unexpected PDU type", + ))) } - } - _ => { - return Poll::Ready(Err(std::io::Error::new( - std::io::ErrorKind::UnexpectedEof, - "Unexpected PDU type", - ))) - } + } } + let len = std::cmp::min(self.buffer.len(), buf.remaining()); + for _ in 0..len { + buf.put_u8(self.buffer.pop_front().unwrap()); + } + Poll::Ready(Ok(())) } - let len = std::cmp::min(self.buffer.len(), buf.remaining()); - for _ in 0..len { - buf.put_u8(self.buffer.pop_front().unwrap()); - } - Poll::Ready(Ok(())) } -} - -/// Determine the maximum length of actual PDV data -/// when encapsulated in a PDU with the given length property. -/// Does not account for the first 2 bytes (type + reserved). -#[inline] -fn calculate_max_data_len_single(pdu_len: u32) -> u32 { - // data length: 4 bytes - // control header: 2 bytes - pdu_len - 4 - 2 } + #[cfg(test)] mod tests { - #[cfg(not(feature = "async"))] use std::io::{Read, Write}; - #[cfg(feature = "async")] - use tokio::{ - self, - io::{AsyncReadExt, AsyncWriteExt}, - }; - use crate::association::PDataWriter; + use crate::association::pdata::PDataWriter; use crate::pdu::{read_pdu, Pdu, MINIMUM_PDU_SIZE, PDU_HEADER_SIZE}; use crate::pdu::{PDataValue, PDataValueType}; use crate::write_pdu; use super::PDataReader; - #[cfg(not(feature = "async"))] + use tokio::io::AsyncWriteExt; + + use crate::association::pdata::non_blocking::AsyncPDataWriter; + #[test] fn test_write_pdata_and_finish() { let presentation_context_id = 12; @@ -687,15 +680,13 @@ mod tests { assert_eq!(cursor.len(), 0); } - #[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] async fn test_async_write_pdata_and_finish() { - use tokio::io::AsyncWriteExt; let presentation_context_id = 12; let mut buf = Vec::new(); { - let mut writer = PDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); + let mut writer = AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); writer .write_all(&(0..64).collect::>()) .await @@ -724,7 +715,6 @@ mod tests { assert_eq!(cursor.len(), 0); } - #[cfg(not(feature = "async"))] #[test] fn test_write_large_pdata_and_finish() { let presentation_context_id = 32; @@ -803,7 +793,6 @@ mod tests { assert_eq!(cursor.len(), 0); } - #[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] async fn test_async_write_large_pdata_and_finish() { let presentation_context_id = 32; @@ -812,7 +801,7 @@ mod tests { let mut buf = Vec::new(); { - let mut writer = PDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); + let mut writer = AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); writer.write_all(&my_data).await.unwrap(); writer.finish().await.unwrap(); } @@ -882,7 +871,6 @@ mod tests { assert_eq!(cursor.len(), 0); } - #[cfg(not(feature = "async"))] #[test] fn test_read_large_pdata_and_finish() { use std::collections::VecDeque; @@ -917,15 +905,16 @@ mod tests { let mut buf = Vec::new(); { - let mut reader = PDataReader::new(&mut pdu_stream, MINIMUM_PDU_SIZE); + let mut reader = PDataReader::new(&mut pdu_stream, MINIMUM_PDU_SIZE, None); reader.read_to_end(&mut buf).unwrap(); } assert_eq!(buf, my_data); } - #[cfg(feature = "async")] #[tokio::test] async fn test_async_read_large_pdata_and_finish() { + use tokio::io::AsyncReadExt; + let presentation_context_id = 32; let my_data: Vec<_> = (0..9000).map(|x: u32| x as u8).collect(); @@ -959,7 +948,7 @@ mod tests { let inner = pdu_stream.into_inner(); let mut stream = tokio::io::BufReader::new(inner.as_slice()); { - let mut reader = PDataReader::new(&mut stream, MINIMUM_PDU_SIZE); + let mut reader = PDataReader::new(&mut stream, MINIMUM_PDU_SIZE, None); reader.read_to_end(&mut buf).await.unwrap(); } assert_eq!(buf, my_data); diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 82d3f891f..0f9a48bd6 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -5,11 +5,10 @@ //! See [`ServerAssociationOptions`] //! for details and examples on how to create an association. use bytes::{Buf, BytesMut}; +use std::io::{BufRead, BufReader}; +use std::time::Duration; use std::{borrow::Cow, io::Cursor}; -#[cfg(not(feature = "async"))] use std::{io::Write, net::TcpStream}; -#[cfg(feature = "async")] -use tokio::{io::AsyncWriteExt, net::TcpStream}; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; @@ -25,7 +24,7 @@ use crate::{ IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; -use super::{uid::trim_uid, PDataReader, PDataWriter}; +use super::{pdata::{PDataReader, PDataWriter}, uid::trim_uid}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -50,7 +49,11 @@ pub enum Error { #[snafu(backtrace)] source: crate::pdu::WriteError, }, - + /// Failed to read from the wire + WireRead{ + source: std::io::Error, + backtrace: Backtrace, + }, /// failed to send PDU over the wire WireSend { source: std::io::Error, @@ -91,6 +94,18 @@ pub enum Error { SendTooLongPdu { length: usize, backtrace: Backtrace }, #[snafu(display("Connection closed by peer"))] ConnectionClosed, + + /// Could not set tcp read timeout + SetReadTimeout { + source: std::io::Error, + backtrace: Backtrace, + }, + + /// Could not set tcp write timeout + SetWriteTimeout { + source: std::io::Error, + backtrace: Backtrace, + }, } pub type Result = std::result::Result; @@ -164,7 +179,6 @@ impl AccessControl for AcceptCalledAeTitle { /// /// # Example /// -#[cfg_attr(not(feature = "async"),doc=r##" /// ```no_run /// # use std::net::TcpListener; /// # use dicom_ul::association::server::ServerAssociationOptions; @@ -179,7 +193,6 @@ impl AccessControl for AcceptCalledAeTitle { /// # Ok(()) /// # } /// ``` -"##)] /// /// The SCP will by default accept all transfer syntaxes /// supported by the main [transfer syntax registry][1], @@ -225,6 +238,8 @@ pub struct ServerAssociationOptions<'a, A> { strict: bool, /// whether to accept unknown abstract syntaxes promiscuous: bool, + /// Timeout for individual send/receive operations + timeout: Option, } impl<'a> Default for ServerAssociationOptions<'a, AcceptAny> { @@ -239,6 +254,7 @@ impl<'a> Default for ServerAssociationOptions<'a, AcceptAny> { max_pdu_length: DEFAULT_MAX_PDU, strict: true, promiscuous: false, + timeout: None } } } @@ -289,6 +305,7 @@ where strict, promiscuous, ae_access_control: _, + timeout, } = self; ServerAssociationOptions { @@ -301,6 +318,7 @@ where max_pdu_length, strict, promiscuous, + timeout } } @@ -357,10 +375,16 @@ where self } - #[cfg(not(feature = "async"))] + /// Set the timeout for the underlying TCP socket + pub fn timeout(self, timeout: Duration) -> Self { + Self { + timeout: Some(timeout), + ..self + } + } + /// Negotiate an association with the given TCP stream. - pub fn establish(&self, mut socket: TcpStream) -> Result { - use std::io::{BufRead, BufReader}; + pub fn establish(&self, mut socket: TcpStream) -> Result> { ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, @@ -368,6 +392,12 @@ where ); let max_pdu_length = self.max_pdu_length; + socket + .set_read_timeout(self.timeout) + .context(SetReadTimeoutSnafu)?; + socket + .set_write_timeout(self.timeout) + .context(SetWriteTimeoutSnafu)?; let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); let mut reader = BufReader::new(&mut socket); @@ -542,6 +572,7 @@ where buffer, strict: self.strict, read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), + timeout: self.timeout }) } Pdu::ReleaseRQ => { @@ -558,202 +589,6 @@ where } } - #[cfg(feature = "async")] - /// Negotiate an association with the given TCP stream. - pub async fn establish(&self, mut socket: TcpStream) -> Result { - ensure!( - !self.abstract_syntax_uids.is_empty() || self.promiscuous, - MissingAbstractSyntaxSnafu - ); - - let max_pdu_length = self.max_pdu_length; - use tokio::io::AsyncReadExt; - let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); - - let pdu = loop { - let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { - Some(pdu) => { - read_buffer.advance(buf.position() as usize); - break pdu; - } - None => { - // Reset position - buf.set_position(0) - } - } - let recv = socket - .read_buf(&mut read_buffer) - .await - .context(ReadPduSnafu) - .context(ReceiveSnafu)?; - ensure!(recv > 0, ConnectionClosedSnafu); - }; - - let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); - match pdu { - Pdu::AssociationRQ(AssociationRQ { - protocol_version, - calling_ae_title, - called_ae_title, - application_context_name, - presentation_contexts, - user_variables, - }) => { - if protocol_version != self.protocol_version { - write_pdu( - &mut buffer, - &Pdu::AssociationRJ(AssociationRJ { - result: AssociationRJResult::Permanent, - source: AssociationRJSource::ServiceUser( - AssociationRJServiceUserReason::NoReasonGiven, - ), - }), - ) - .context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - return RejectedSnafu.fail(); - } - - if application_context_name != self.application_context_name { - write_pdu( - &mut buffer, - &Pdu::AssociationRJ(AssociationRJ { - result: AssociationRJResult::Permanent, - source: AssociationRJSource::ServiceUser( - AssociationRJServiceUserReason::ApplicationContextNameNotSupported, - ), - }), - ) - .context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - return RejectedSnafu.fail(); - } - - match self.ae_access_control.check_access( - &self.ae_title, - &calling_ae_title, - &called_ae_title, - user_variables - .iter() - .find_map(|user_variable| match user_variable { - UserVariableItem::UserIdentityItem(user_identity) => { - Some(user_identity) - } - _ => None, - }), - ) { - Ok(()) => {} - Err(reason) => { - write_pdu( - &mut buffer, - &Pdu::AssociationRJ(AssociationRJ { - result: AssociationRJResult::Permanent, - source: AssociationRJSource::ServiceUser(reason), - }), - ) - .context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - return Err(RejectedSnafu.build()); - } - } - - // fetch requested maximum PDU length - let requestor_max_pdu_length = user_variables - .iter() - .find_map(|item| match item { - UserVariableItem::MaxLength(len) => Some(*len), - _ => None, - }) - .unwrap_or(DEFAULT_MAX_PDU); - - // treat 0 as the maximum size admitted by the standard - let requestor_max_pdu_length = if requestor_max_pdu_length == 0 { - MAXIMUM_PDU_SIZE - } else { - requestor_max_pdu_length - }; - - let presentation_contexts: Vec<_> = presentation_contexts - .into_iter() - .map(|pc| { - if !self - .abstract_syntax_uids - .contains(&trim_uid(Cow::from(pc.abstract_syntax))) - && !self.promiscuous - { - return PresentationContextResult { - id: pc.id, - reason: PresentationContextResultReason::AbstractSyntaxNotSupported, - transfer_syntax: "1.2.840.10008.1.2".to_string(), - }; - } - - let (transfer_syntax, reason) = self - .choose_ts(pc.transfer_syntaxes) - .map(|ts| (ts, PresentationContextResultReason::Acceptance)) - .unwrap_or_else(|| { - ( - "1.2.840.10008.1.2".to_string(), - PresentationContextResultReason::TransferSyntaxesNotSupported, - ) - }); - - PresentationContextResult { - id: pc.id, - reason, - transfer_syntax, - } - }) - .collect(); - - write_pdu( - &mut buffer, - &Pdu::AssociationAC(AssociationAC { - protocol_version: self.protocol_version, - application_context_name, - presentation_contexts: presentation_contexts.clone(), - calling_ae_title: calling_ae_title.clone(), - called_ae_title, - user_variables: vec![ - UserVariableItem::MaxLength(max_pdu_length), - UserVariableItem::ImplementationClassUID( - IMPLEMENTATION_CLASS_UID.to_string(), - ), - UserVariableItem::ImplementationVersionName( - IMPLEMENTATION_VERSION_NAME.to_string(), - ), - ], - }), - ) - .context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - - Ok(ServerAssociation { - presentation_contexts, - requestor_max_pdu_length, - acceptor_max_pdu_length: max_pdu_length, - socket, - client_ae_title: calling_ae_title, - buffer, - strict: self.strict, - read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), - }) - } - Pdu::ReleaseRQ => { - write_pdu(&mut buffer, &Pdu::ReleaseRP).context(SendResponseSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; - AbortedSnafu.fail() - } - pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRP - | pdu @ Pdu::AbortRQ { .. } => UnexpectedRequestSnafu { pdu }.fail(), - pdu @ Pdu::Unknown { .. } => UnknownRequestSnafu { pdu }.fail(), - } - } - /// From a sequence of transfer syntaxes, /// choose the first transfer syntax to /// - be on the options' list of transfer syntaxes, and @@ -793,7 +628,7 @@ where /// When the value falls out of scope, /// the program will shut down the underlying TCP connection. #[derive(Debug)] -pub struct ServerAssociation { +pub struct ServerAssociation{ /// The accorded presentation contexts presentation_contexts: Vec, /// The maximum PDU length that the remote application entity accepts @@ -801,7 +636,7 @@ pub struct ServerAssociation { /// The maximum PDU length that this application entity is expecting to receive acceptor_max_pdu_length: u32, /// The TCP stream to the other DICOM node - socket: TcpStream, + socket: S, /// The application entity title of the other DICOM node client_ae_title: String, /// write buffer to send fully assembled PDUs on wire @@ -810,9 +645,11 @@ pub struct ServerAssociation { strict: bool, /// Read buffer from the socket read_buffer: bytes::BytesMut, + /// Timeout for individual send/receive operations + timeout: Option, } -impl ServerAssociation { +impl ServerAssociation { /// Obtain a view of the negotiated presentation contexts. pub fn presentation_contexts(&self) -> &[PresentationContextResult] { &self.presentation_contexts @@ -822,8 +659,10 @@ impl ServerAssociation { pub fn client_ae_title(&self) -> &str { &self.client_ae_title } +} + +impl ServerAssociation{ - #[cfg(not(feature = "async"))] /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -837,25 +676,7 @@ impl ServerAssociation { self.socket.write_all(&self.buffer).context(WireSendSnafu) } - #[cfg(feature = "async")] - /// Send a PDU message to the other intervenient. - pub async fn send(&mut self, msg: &Pdu) -> Result<()> { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendSnafu)?; - if self.buffer.len() > self.requestor_max_pdu_length as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), - } - .fail(); - } - self.socket - .write_all(&self.buffer) - .await - .context(WireSendSnafu) - } - /// Read a PDU message from the other intervenient. - #[cfg(not(feature = "async"))] pub fn receive(&mut self) -> Result { use std::io::{BufRead, BufReader, Cursor}; @@ -887,42 +708,9 @@ impl ServerAssociation { } } - #[cfg(feature = "async")] - /// Read a PDU message from the other intervenient. - pub async fn receive(&mut self) -> Result { - use std::io::Cursor; - - use bytes::Buf; - use tokio::io::AsyncReadExt; - - loop { - let mut buf = Cursor::new(&self.read_buffer[..]); - match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) - .context(ReceiveRequestSnafu)? - { - Some(pdu) => { - self.read_buffer.advance(buf.position() as usize); - return Ok(pdu); - } - None => { - // Reset position - buf.set_position(0) - } - } - let recv = self - .socket - .read_buf(&mut self.read_buffer) - .await - .context(ReadPduSnafu) - .context(ReceiveSnafu)?; - ensure!(recv > 0, ConnectionClosedSnafu); - } - } - /// Send a provider initiated abort message /// and shut down the TCP connection, /// terminating the association. - #[cfg(not(feature = "async"))] pub fn abort(mut self) -> Result<()> { let pdu = Pdu::AbortRQ { source: AbortRQSource::ServiceProvider( @@ -934,41 +722,12 @@ impl ServerAssociation { out } - /// Send a provider initiated abort message - /// and shut down the TCP connection, - /// terminating the association. - #[cfg(feature = "async")] - pub async fn abort(mut self) -> Result<()> { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceProvider( - AbortRQServiceProviderReason::ReasonNotSpecified, - ), - }; - let out = self.send(&pdu).await; - let _ = self.socket.shutdown().await; - out - } /// 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. - #[cfg(not(feature = "async"))] - pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - PDataWriter::new( - &mut self.socket, - presentation_context_id, - self.requestor_max_pdu_length, - ) - } - - /// 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. - #[cfg(feature = "async")] pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { PDataWriter::new( &mut self.socket, @@ -983,7 +742,7 @@ impl ServerAssociation { /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) + PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length, self.timeout) } /// Obtain access to the inner TCP stream @@ -1057,6 +816,331 @@ where it.into_iter().find(|ts| is_supported(ts.as_ref())) } + +#[cfg(feature = "async")] +pub mod non_blocking{ + use std::{borrow::Cow, io::Cursor}; + + use bytes::{Buf, BytesMut}; + use snafu::{ensure, ResultExt}; + use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; + + use crate::{association::{server::{AbortedSnafu, ConnectionClosedSnafu, MissingAbstractSyntaxSnafu, ReceiveRequestSnafu, ReceiveSnafu, RejectedSnafu, SendResponseSnafu, UnexpectedRequestSnafu, UnknownRequestSnafu, WireReadSnafu}, uid::trim_uid}, pdu::{AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ, AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ, PresentationContextResult, PresentationContextResultReason, ReadPduSnafu, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE}, read_pdu, write_pdu, Pdu, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME}; + use super::{AccessControl, Result, SendSnafu, SendTooLongPduSnafu, ServerAssociation, ServerAssociationOptions, WireSendSnafu}; + + impl<'a, A> ServerAssociationOptions<'a, A> + where + A: AccessControl, + { + /// Negotiate an association with the given TCP stream. + pub async fn establish_async(&self, mut socket: TcpStream) -> Result> { + ensure!( + !self.abstract_syntax_uids.is_empty() || self.promiscuous, + MissingAbstractSyntaxSnafu + ); + let timeout = self.timeout; + let task = async { + + let max_pdu_length = self.max_pdu_length; + let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); + + let pdu = loop { + let mut buf = Cursor::new(&read_buffer[..]); + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { + Some(pdu) => { + read_buffer.advance(buf.position() as usize); + break pdu; + } + None => { + // Reset position + buf.set_position(0) + } + } + let recv = socket + .read_buf(&mut read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); + }; + + let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); + match pdu { + Pdu::AssociationRQ(AssociationRQ { + protocol_version, + calling_ae_title, + called_ae_title, + application_context_name, + presentation_contexts, + user_variables, + }) => { + if protocol_version != self.protocol_version { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser( + AssociationRJServiceUserReason::NoReasonGiven, + ), + }), + ) + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return RejectedSnafu.fail(); + } + + if application_context_name != self.application_context_name { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser( + AssociationRJServiceUserReason::ApplicationContextNameNotSupported, + ), + }), + ) + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return RejectedSnafu.fail(); + } + + match self.ae_access_control.check_access( + &self.ae_title, + &calling_ae_title, + &called_ae_title, + user_variables + .iter() + .find_map(|user_variable| match user_variable { + UserVariableItem::UserIdentityItem(user_identity) => { + Some(user_identity) + } + _ => None, + }), + ) { + Ok(()) => {} + Err(reason) => { + write_pdu( + &mut buffer, + &Pdu::AssociationRJ(AssociationRJ { + result: AssociationRJResult::Permanent, + source: AssociationRJSource::ServiceUser(reason), + }), + ) + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + return Err(RejectedSnafu.build()); + } + } + + // fetch requested maximum PDU length + let requestor_max_pdu_length = user_variables + .iter() + .find_map(|item| match item { + UserVariableItem::MaxLength(len) => Some(*len), + _ => None, + }) + .unwrap_or(DEFAULT_MAX_PDU); + + // treat 0 as the maximum size admitted by the standard + let requestor_max_pdu_length = if requestor_max_pdu_length == 0 { + MAXIMUM_PDU_SIZE + } else { + requestor_max_pdu_length + }; + + let presentation_contexts: Vec<_> = presentation_contexts + .into_iter() + .map(|pc| { + if !self + .abstract_syntax_uids + .contains(&trim_uid(Cow::from(pc.abstract_syntax))) + && !self.promiscuous + { + return PresentationContextResult { + id: pc.id, + reason: PresentationContextResultReason::AbstractSyntaxNotSupported, + transfer_syntax: "1.2.840.10008.1.2".to_string(), + }; + } + + let (transfer_syntax, reason) = self + .choose_ts(pc.transfer_syntaxes) + .map(|ts| (ts, PresentationContextResultReason::Acceptance)) + .unwrap_or_else(|| { + ( + "1.2.840.10008.1.2".to_string(), + PresentationContextResultReason::TransferSyntaxesNotSupported, + ) + }); + + PresentationContextResult { + id: pc.id, + reason, + transfer_syntax, + } + }) + .collect(); + + write_pdu( + &mut buffer, + &Pdu::AssociationAC(AssociationAC { + protocol_version: self.protocol_version, + application_context_name, + presentation_contexts: presentation_contexts.clone(), + calling_ae_title: calling_ae_title.clone(), + called_ae_title, + user_variables: vec![ + UserVariableItem::MaxLength(max_pdu_length), + UserVariableItem::ImplementationClassUID( + IMPLEMENTATION_CLASS_UID.to_string(), + ), + UserVariableItem::ImplementationVersionName( + IMPLEMENTATION_VERSION_NAME.to_string(), + ), + ], + }), + ) + .context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + + Ok(ServerAssociation { + presentation_contexts, + requestor_max_pdu_length, + acceptor_max_pdu_length: max_pdu_length, + socket, + client_ae_title: calling_ae_title, + buffer, + strict: self.strict, + read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), + timeout + }) + } + Pdu::ReleaseRQ => { + write_pdu(&mut buffer, &Pdu::ReleaseRP).context(SendResponseSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + AbortedSnafu.fail() + } + pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRP + | pdu @ Pdu::AbortRQ { .. } => UnexpectedRequestSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => UnknownRequestSnafu { pdu }.fail(), + } + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireReadSnafu)? + } else { + task.await + } + + } +} + + impl ServerAssociation{ + /// Send a PDU message to the other intervenient. + pub async fn send(&mut self, msg: &Pdu) -> Result<()> { + let timeout = self.timeout; + let task = async { + self.buffer.clear(); + write_pdu(&mut self.buffer, msg).context(SendSnafu)?; + if self.buffer.len() > self.requestor_max_pdu_length as usize { + return SendTooLongPduSnafu { + length: self.buffer.len(), + } + .fail(); + } + self.socket + .write_all(&self.buffer) + .await + .context(WireSendSnafu) + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireSendSnafu)? + + } else { + task.await + } + } + + /// Read a PDU message from the other intervenient. + pub async fn receive(&mut self) -> Result { + + let timeout = self.timeout; + let task = async { + loop { + let mut buf = Cursor::new(&self.read_buffer[..]); + match read_pdu(&mut buf, self.requestor_max_pdu_length, self.strict) + .context(ReceiveRequestSnafu)? + { + Some(pdu) => { + self.read_buffer.advance(buf.position() as usize); + return Ok(pdu); + } + None => { + // Reset position + buf.set_position(0) + } + } + let recv = self + .socket + .read_buf(&mut self.read_buffer) + .await + .context(ReadPduSnafu) + .context(ReceiveSnafu)?; + ensure!(recv > 0, ConnectionClosedSnafu); + } + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(ReadPduSnafu) + .context(ReceiveSnafu)? + + } else { + task.await + } + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + pub async fn abort(mut self) -> Result<()> { + let timeout = self.timeout; + let task = async { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceProvider( + AbortRQServiceProviderReason::ReasonNotSpecified, + ), + }; + let out = self.send(&pdu).await; + let _ = self.socket.shutdown().await; + out + }; + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireSendSnafu)? + } else { + task.await + } + } + + pub fn inner_stream(&mut self) -> &mut TcpStream { + &mut self.socket + } + } + +} + #[cfg(test)] mod tests { use super::choose_supported; @@ -1080,4 +1164,4 @@ mod tests { Some("1.2.840.10008.1.2.1".to_string()), ); } -} +} \ No newline at end of file From 975045962dd1a53ca776093a32a7e6b6b4b9b716 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Thu, 5 Sep 2024 15:14:30 -0500 Subject: [PATCH 22/28] MAIN: More work updating to separate modules * Update some documentation * Set timeouts in most places * Update tests [skip ci] --- ul/src/association/client.rs | 22 +++--- ul/src/association/pdata.rs | 40 +++++----- ul/src/association/server.rs | 92 +++++++++++++--------- ul/tests/association_echo.rs | 14 ++-- ul/tests/association_promiscuous.rs | 26 +++--- ul/tests/association_store.rs | 18 ++--- ul/tests/association_store_uncompressed.rs | 20 ++--- 7 files changed, 118 insertions(+), 114 deletions(-) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index d77d794f9..e23025745 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -166,17 +166,17 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool /// This is the standard way of requesting and establishing /// an association with another DICOM node, /// that one usually taking the role of a service class provider (SCP). -/// +/// /// You can create either a blocking or non-blocking client by calling either /// `establish` or `establish_async` respectively. /// /// > **⚠️ Warning:** It is highly recommended to set `timeout` to a reasonable value for the /// > async client since there is _no_ default timeout on /// > [`tokio::net::TcpStream`] -/// +/// /// ## Basic usage /// -/// ### Sync +/// ### Sync /// /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; @@ -189,8 +189,8 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool /// # Ok(()) /// # } /// ``` -/// -/// ### Async +/// +/// ### Async /// /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; @@ -204,7 +204,7 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool /// # Ok(()) /// # } /// ``` -/// +/// /// /// At least one presentation context must be specified, /// using the method [`with_presentation_context`](Self::with_presentation_context) @@ -946,7 +946,7 @@ where /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.requestor_max_pdu_length, self.timeout) + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } /// Release implementation function, @@ -997,7 +997,7 @@ pub mod non_blocking { ReceiveResponseSnafu, ReceiveSnafu, RejectedSnafu, SendRequestSnafu, UnexpectedResponseSnafu, UnknownResponseSnafu, }, - pdata::non_blocking::{PDataReader, AsyncPDataWriter} + pdata::non_blocking::{AsyncPDataWriter, PDataReader}, }, pdu::{ AbortRQSource, AssociationAC, AssociationRQ, PresentationContextProposed, @@ -1249,7 +1249,6 @@ pub mod non_blocking { warn!("No timeout set. It is highly recommended to set a timeout."); task.await } - } /// Initiate the TCP connection to the given address @@ -1328,7 +1327,6 @@ pub mod non_blocking { .await .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) .context(WireSendSnafu)? - } else { task.await } @@ -1367,7 +1365,6 @@ pub mod non_blocking { .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) .context(ReadPduSnafu) .context(ReceiveSnafu)? - } else { task.await } @@ -1427,7 +1424,6 @@ pub mod non_blocking { &mut self.socket, presentation_context_id, self.acceptor_max_pdu_length, - self.timeout ) } @@ -1438,7 +1434,7 @@ pub mod non_blocking { /// receives more data PDUs once the bytes collected are consumed. #[cfg(feature = "async")] pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.requestor_max_pdu_length, self.timeout) + PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } /// Release implementation function, diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index f3b86e34b..8aa9721fe 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -1,6 +1,6 @@ use std::{ collections::VecDeque, - io::{BufRead, BufReader, Cursor, Read, Write}, time::Duration + io::{BufRead, BufReader, Cursor, Read, Write}, }; use bytes::{Buf, BytesMut}; @@ -242,11 +242,10 @@ pub struct PDataReader { max_data_length: u32, last_pdu: bool, read_buffer: BytesMut, - timeout: Option } impl PDataReader { - pub fn new(stream: R, max_data_length: u32, timeout: Option) -> Self { + pub fn new(stream: R, max_data_length: u32) -> Self { PDataReader { buffer: VecDeque::with_capacity(max_data_length as usize), stream, @@ -254,7 +253,6 @@ impl PDataReader { max_data_length, last_pdu: false, read_buffer: BytesMut::with_capacity(max_data_length as usize), - timeout } } @@ -333,7 +331,6 @@ where } } - /// Determine the maximum length of actual PDV data /// when encapsulated in a PDU with the given length property. /// Does not account for the first 2 bytes (type + reserved). @@ -346,16 +343,23 @@ fn calculate_max_data_len_single(pdu_len: u32) -> u32 { #[cfg(feature = "async")] pub mod non_blocking { - use std::{io::Cursor, pin::Pin, task::{ready, Context, Poll}, time::Duration}; + use std::{ + future::Future, + io::Cursor, + pin::Pin, + task::{ready, Context, Poll}, + }; use bytes::{Buf, BufMut}; - use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf }; + use tokio::io::{ + AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf, + }; use tracing::warn; use crate::{pdu::PDU_HEADER_SIZE, read_pdu, Pdu}; - use super::{calculate_max_data_len_single, setup_pdata_header}; pub use super::PDataReader; + use super::{calculate_max_data_len_single, setup_pdata_header}; /// A P-Data async value writer. /// @@ -409,7 +413,6 @@ pub mod non_blocking { buffer: Vec, stream: W, max_data_len: u32, - timeout: Option } #[cfg(feature = "async")] @@ -420,7 +423,7 @@ pub mod non_blocking { /// Construct a new P-Data value writer. /// /// `max_pdu_length` is the maximum value of the PDU-length property. - pub(crate) fn new(stream: W, presentation_context_id: u8, max_pdu_length: u32, timeout: Option) -> Self { + pub(crate) fn new(stream: W, presentation_context_id: u8, max_pdu_length: u32) -> Self { let max_data_length = calculate_max_data_len_single(max_pdu_length); let mut buffer = Vec::with_capacity((max_data_length + PDU_HEADER_SIZE) as usize); // initial buffer set up @@ -448,7 +451,6 @@ pub mod non_blocking { stream, max_data_len: max_data_length, buffer, - timeout } } @@ -500,7 +502,6 @@ pub mod non_blocking { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - use std::future::Future; let total_len = self.max_data_len as usize + 12; if self.buffer.len() + buf.len() <= total_len { // accumulate into buffer, do nothing @@ -563,7 +564,7 @@ pub mod non_blocking { cx: &mut Context<'_>, buf: &mut ReadBuf, ) -> Poll> { - if self.buffer.is_empty(){ + if self.buffer.is_empty() { if self.last_pdu { return Poll::Ready(Ok(())); } @@ -603,7 +604,9 @@ pub mod non_blocking { for pdata_value in data { self.presentation_context_id = match self.presentation_context_id { None => Some(pdata_value.presentation_context_id), - Some(cid) if cid == pdata_value.presentation_context_id => Some(cid), + Some(cid) if cid == pdata_value.presentation_context_id => { + Some(cid) + } Some(cid) => { warn!("Received PData value of presentation context {}, but should be {}", pdata_value.presentation_context_id, cid); Some(cid) @@ -619,7 +622,6 @@ pub mod non_blocking { "Unexpected PDU type", ))) } - } } let len = std::cmp::min(self.buffer.len(), buf.remaining()); @@ -629,10 +631,8 @@ pub mod non_blocking { Poll::Ready(Ok(())) } } - } - #[cfg(test)] mod tests { use std::io::{Read, Write}; @@ -686,7 +686,8 @@ mod tests { let mut buf = Vec::new(); { - let mut writer = AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); + let mut writer = + AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); writer .write_all(&(0..64).collect::>()) .await @@ -801,7 +802,8 @@ mod tests { let mut buf = Vec::new(); { - let mut writer = AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); + let mut writer = + AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); writer.write_all(&my_data).await.unwrap(); writer.finish().await.unwrap(); } diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 0f9a48bd6..86ab65705 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -24,7 +24,10 @@ use crate::{ IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, }; -use super::{pdata::{PDataReader, PDataWriter}, uid::trim_uid}; +use super::{ + pdata::{PDataReader, PDataWriter}, + uid::trim_uid, +}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -50,7 +53,7 @@ pub enum Error { source: crate::pdu::WriteError, }, /// Failed to read from the wire - WireRead{ + WireRead { source: std::io::Error, backtrace: Backtrace, }, @@ -254,7 +257,7 @@ impl<'a> Default for ServerAssociationOptions<'a, AcceptAny> { max_pdu_length: DEFAULT_MAX_PDU, strict: true, promiscuous: false, - timeout: None + timeout: None, } } } @@ -318,7 +321,7 @@ where max_pdu_length, strict, promiscuous, - timeout + timeout, } } @@ -385,7 +388,6 @@ where /// Negotiate an association with the given TCP stream. pub fn establish(&self, mut socket: TcpStream) -> Result> { - ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, MissingAbstractSyntaxSnafu @@ -572,7 +574,7 @@ where buffer, strict: self.strict, read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), - timeout: self.timeout + timeout: self.timeout, }) } Pdu::ReleaseRQ => { @@ -628,7 +630,7 @@ where /// When the value falls out of scope, /// the program will shut down the underlying TCP connection. #[derive(Debug)] -pub struct ServerAssociation{ +pub struct ServerAssociation { /// The accorded presentation contexts presentation_contexts: Vec, /// The maximum PDU length that the remote application entity accepts @@ -661,8 +663,7 @@ impl ServerAssociation { } } -impl ServerAssociation{ - +impl ServerAssociation { /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { self.buffer.clear(); @@ -722,7 +723,6 @@ impl ServerAssociation{ out } - /// Prepare a P-Data writer for sending /// one or more data item PDUs. /// @@ -742,7 +742,7 @@ impl ServerAssociation{ /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { - PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length, self.timeout) + PDataReader::new(&mut self.socket, self.acceptor_max_pdu_length) } /// Obtain access to the inner TCP stream @@ -816,37 +816,62 @@ where it.into_iter().find(|ts| is_supported(ts.as_ref())) } - #[cfg(feature = "async")] -pub mod non_blocking{ +pub mod non_blocking { use std::{borrow::Cow, io::Cursor}; use bytes::{Buf, BytesMut}; use snafu::{ensure, ResultExt}; - use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; - - use crate::{association::{server::{AbortedSnafu, ConnectionClosedSnafu, MissingAbstractSyntaxSnafu, ReceiveRequestSnafu, ReceiveSnafu, RejectedSnafu, SendResponseSnafu, UnexpectedRequestSnafu, UnknownRequestSnafu, WireReadSnafu}, uid::trim_uid}, pdu::{AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ, AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, AssociationRQ, PresentationContextResult, PresentationContextResultReason, ReadPduSnafu, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE}, read_pdu, write_pdu, Pdu, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME}; - use super::{AccessControl, Result, SendSnafu, SendTooLongPduSnafu, ServerAssociation, ServerAssociationOptions, WireSendSnafu}; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + + use super::{ + AccessControl, Result, SendSnafu, SendTooLongPduSnafu, ServerAssociation, + ServerAssociationOptions, WireSendSnafu, + }; + use crate::{ + association::{ + server::{ + AbortedSnafu, ConnectionClosedSnafu, MissingAbstractSyntaxSnafu, + ReceiveRequestSnafu, ReceiveSnafu, RejectedSnafu, SendResponseSnafu, + UnexpectedRequestSnafu, UnknownRequestSnafu, WireReadSnafu, + }, + uid::trim_uid, + }, + pdu::{ + AbortRQServiceProviderReason, AbortRQSource, AssociationAC, AssociationRJ, + AssociationRJResult, AssociationRJServiceUserReason, AssociationRJSource, + AssociationRQ, PresentationContextResult, PresentationContextResultReason, + ReadPduSnafu, UserVariableItem, DEFAULT_MAX_PDU, MAXIMUM_PDU_SIZE, + }, + read_pdu, write_pdu, Pdu, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, + }; impl<'a, A> ServerAssociationOptions<'a, A> where A: AccessControl, { /// Negotiate an association with the given TCP stream. - pub async fn establish_async(&self, mut socket: TcpStream) -> Result> { + pub async fn establish_async( + &self, + mut socket: TcpStream, + ) -> Result> { ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, MissingAbstractSyntaxSnafu ); let timeout = self.timeout; let task = async { - let max_pdu_length = self.max_pdu_length; let mut read_buffer = BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize); let pdu = loop { let mut buf = Cursor::new(&read_buffer[..]); - match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict).context(ReceiveRequestSnafu)? { + match read_pdu(&mut buf, MAXIMUM_PDU_SIZE, self.strict) + .context(ReceiveRequestSnafu)? + { Some(pdu) => { read_buffer.advance(buf.position() as usize); break pdu; @@ -1012,7 +1037,7 @@ pub mod non_blocking{ buffer, strict: self.strict, read_buffer: BytesMut::with_capacity(MAXIMUM_PDU_SIZE as usize), - timeout + timeout, }) } Pdu::ReleaseRQ => { @@ -1028,19 +1053,18 @@ pub mod non_blocking{ pdu @ Pdu::Unknown { .. } => UnknownRequestSnafu { pdu }.fail(), } }; - if let Some(timeout) = timeout { - tokio::time::timeout(timeout, task) - .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) - .context(WireReadSnafu)? - } else { - task.await + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, task) + .await + .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) + .context(WireReadSnafu)? + } else { + task.await + } } - } -} - impl ServerAssociation{ + impl ServerAssociation { /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { let timeout = self.timeout; @@ -1063,7 +1087,6 @@ pub mod non_blocking{ .await .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) .context(WireSendSnafu)? - } else { task.await } @@ -1071,7 +1094,6 @@ pub mod non_blocking{ /// Read a PDU message from the other intervenient. pub async fn receive(&mut self) -> Result { - let timeout = self.timeout; let task = async { loop { @@ -1103,7 +1125,6 @@ pub mod non_blocking{ .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) .context(ReadPduSnafu) .context(ReceiveSnafu)? - } else { task.await } @@ -1138,7 +1159,6 @@ pub mod non_blocking{ &mut self.socket } } - } #[cfg(test)] @@ -1164,4 +1184,4 @@ mod tests { Some("1.2.840.10008.1.2.1".to_string()), ); } -} \ No newline at end of file +} diff --git a/ul/tests/association_echo.rs b/ul/tests/association_echo.rs index 60c02217d..ea84fe8c7 100644 --- a/ul/tests/association_echo.rs +++ b/ul/tests/association_echo.rs @@ -18,7 +18,6 @@ static JPEG_BASELINE: &str = "1.2.840.10008.1.2.4.50"; static VERIFICATION_SOP_CLASS: &str = "1.2.840.10008.1.1"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -#[cfg(not(feature = "async"))] fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; @@ -57,8 +56,7 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { Ok((h, addr)) } -#[cfg(feature = "async")] -async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -68,7 +66,7 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) let h = tokio::spawn(async move { let (stream, _addr) = listener.accept().await?; - let mut association = scp.establish(stream).await?; + let mut association = scp.establish_async(stream).await?; assert_eq!( association.presentation_contexts(), @@ -97,7 +95,6 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) } /// Run an SCP and an SCU concurrently, negotiate an association and release it. -#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_test() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -123,10 +120,9 @@ fn scu_scp_association_test() { .expect("Error at the SCP"); } -#[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] -async fn scu_scp_asociation_test(){ - let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); +async fn scu_scp_asociation_test() { + let (scp_handle, scp_addr) = spawn_scp_async().await.unwrap(); let association = ClientAssociationOptions::new() .calling_ae_title(SCU_AE_TITLE) @@ -136,7 +132,7 @@ async fn scu_scp_asociation_test(){ DIGITAL_MG_STORAGE_SOP_CLASS, vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE, JPEG_BASELINE], ) - .establish(scp_addr) + .establish_async(scp_addr) .await .unwrap(); diff --git a/ul/tests/association_promiscuous.rs b/ul/tests/association_promiscuous.rs index 17e3edc9f..ba0d432d4 100644 --- a/ul/tests/association_promiscuous.rs +++ b/ul/tests/association_promiscuous.rs @@ -12,7 +12,6 @@ const IMPLICIT_VR_LE: &str = "1.2.840.10008.1.2"; const MR_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.4\0"; const ULTRASOUND_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.6.1\0"; -#[cfg(not(feature = "async"))] fn spawn_scp( abstract_syntax_uids: &'static [&str], promiscuous: bool, @@ -50,8 +49,7 @@ fn spawn_scp( Ok((handle, addr)) } -#[cfg(feature = "async")] -async fn spawn_scp( +async fn spawn_scp_async( abstract_syntax_uids: &'static [&str], promiscuous: bool, ) -> Result<(tokio::task::JoinHandle>, SocketAddr)> { @@ -68,7 +66,7 @@ async fn spawn_scp( let handle = tokio::spawn(async move { let (stream, _addr) = listener.accept().await?; - let mut association = options.establish(stream).await?; + let mut association = options.establish_async(stream).await?; assert_eq!( association.presentation_contexts(), &[PresentationContextResult { @@ -88,7 +86,6 @@ async fn spawn_scp( Ok((handle, addr)) } -#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_promiscuous_enabled() { // SCP is set to promiscuous mode - all abstract syntaxes are accepted @@ -111,17 +108,16 @@ fn scu_scp_association_promiscuous_enabled() { .expect("Error at the SCP"); } -#[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] -async fn scu_scp_association_promiscuous_enabled() { +async fn scu_scp_association_promiscuous_enabled_async() { // SCP is set to promiscuous mode - all abstract syntaxes are accepted - let (scp_handle, scp_addr) = spawn_scp(&[], true).await.unwrap(); + let (scp_handle, scp_addr) = spawn_scp_async(&[], true).await.unwrap(); let association = ClientAssociationOptions::new() .calling_ae_title(SCU_AE_TITLE) .called_ae_title(SCP_AE_TITLE) .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) - .establish(scp_addr) + .establish_async(scp_addr) .await .unwrap(); @@ -136,7 +132,6 @@ async fn scu_scp_association_promiscuous_enabled() { .expect("Error at the SCP"); } -#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_promiscuous_disabled() { // SCP only accepts Ultrasound Image Storage @@ -155,17 +150,18 @@ fn scu_scp_association_promiscuous_disabled() { )); } -#[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] -async fn scu_scp_association_promiscuous_disabled() { +async fn scu_scp_association_promiscuous_disabled_async() { // SCP only accepts Ultrasound Image Storage - let (_scu_handle, scp_addr) = spawn_scp(&[ULTRASOUND_IMAGE_STORAGE_RAW], false).await.unwrap(); + let (_scu_handle, scp_addr) = spawn_scp_async(&[ULTRASOUND_IMAGE_STORAGE_RAW], false) + .await + .unwrap(); let association = ClientAssociationOptions::new() .calling_ae_title(SCU_AE_TITLE) .called_ae_title(SCP_AE_TITLE) .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) - .establish(scp_addr) + .establish_async(scp_addr) .await; // Assert that no presentation context was accepted @@ -173,4 +169,4 @@ async fn scu_scp_association_promiscuous_disabled() { association, Err(NoAcceptedPresentationContexts { .. }) )); -} \ No newline at end of file +} diff --git a/ul/tests/association_store.rs b/ul/tests/association_store.rs index 894fd55b7..f88be28d7 100644 --- a/ul/tests/association_store.rs +++ b/ul/tests/association_store.rs @@ -20,7 +20,6 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -#[cfg(not(feature = "async"))] fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; @@ -60,8 +59,7 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { Ok((h, addr)) } -#[cfg(feature = "async")] -async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -72,7 +70,7 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) let h = tokio::task::spawn(async move { let (stream, _addr) = listener.accept().await?; - let mut association = scp.establish(stream).await?; + let mut association = scp.establish_async(stream).await?; assert_eq!( association.presentation_contexts(), @@ -103,7 +101,6 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) /// Run an SCP and an SCU concurrently, /// negotiate an association with distinct transfer syntaxes /// and release it. -#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_test() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -141,10 +138,9 @@ fn scu_scp_association_test() { .expect("Error at the SCP"); } -#[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] -async fn scu_scp_association_test() { - let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); +async fn scu_scp_association_test_async() { + let (scp_handle, scp_addr) = spawn_scp_async().await.unwrap(); let association = ClientAssociationOptions::new() .calling_ae_title(SCU_AE_TITLE) @@ -152,7 +148,8 @@ async fn scu_scp_association_test() { .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) // MG storage, JPEG baseline .with_presentation_context(DIGITAL_MG_STORAGE_SOP_CLASS_RAW, vec![JPEG_BASELINE]) - .establish(scp_addr).await + .establish_async(scp_addr) + .await .unwrap(); for pc in association.presentation_contexts() { @@ -170,7 +167,8 @@ async fn scu_scp_association_test() { } association - .release().await + .release() + .await .expect("did not have a peaceful release"); scp_handle diff --git a/ul/tests/association_store_uncompressed.rs b/ul/tests/association_store_uncompressed.rs index 9ee911f69..56124cbd5 100644 --- a/ul/tests/association_store_uncompressed.rs +++ b/ul/tests/association_store_uncompressed.rs @@ -24,7 +24,6 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -#[cfg(not(feature = "async"))] fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; @@ -68,8 +67,7 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { Ok((h, addr)) } -#[cfg(feature = "async")] -async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -82,7 +80,7 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) let h = tokio::task::spawn(async move { let (stream, _addr) = listener.accept().await?; - let mut association = scp.establish(stream).await?; + let mut association = scp.establish_async(stream).await?; assert_eq!( association.presentation_contexts(), @@ -112,11 +110,9 @@ async fn spawn_scp() -> Result<(tokio::task::JoinHandle>, SocketAddr) Ok((h, addr)) } - /// Run an SCP and an SCU concurrently, /// negotiate an association with distinct transfer syntaxes /// and release it. -#[cfg(not(feature = "async"))] #[test] fn scu_scp_association_uncompressed() { let (scp_handle, scp_addr) = spawn_scp().unwrap(); @@ -159,10 +155,9 @@ fn scu_scp_association_uncompressed() { .expect("Error at the SCP"); } -#[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] -async fn scu_scp_association_uncompressed() { - let (scp_handle, scp_addr) = spawn_scp().await.unwrap(); +async fn scu_scp_association_uncompressed_async() { + let (scp_handle, scp_addr) = spawn_scp_async().await.unwrap(); let association = ClientAssociationOptions::new() .calling_ae_title(SCU_AE_TITLE) @@ -173,7 +168,8 @@ async fn scu_scp_association_uncompressed() { DIGITAL_MG_STORAGE_SOP_CLASS_RAW, vec![JPEG_BASELINE, EXPLICIT_VR_LE, IMPLICIT_VR_LE], ) - .establish(scp_addr).await + .establish_async(scp_addr) + .await .unwrap(); for pc in association.presentation_contexts() { @@ -193,7 +189,8 @@ async fn scu_scp_association_uncompressed() { } association - .release().await + .release() + .await .expect("did not have a peaceful release"); scp_handle @@ -201,4 +198,3 @@ async fn scu_scp_association_uncompressed() { .expect("SCP panicked") .expect("Error at the SCP"); } - From de9ab0b26d5e9740d6ff093288c34f1b062087cd Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Thu, 5 Sep 2024 15:24:02 -0500 Subject: [PATCH 23/28] MAIN: Fix tests and doctests [skip ci] --- ul/src/association/client.rs | 3 ++- ul/src/association/mod.rs | 2 +- ul/src/association/pdata.rs | 16 ++++++++-------- ul/tests/association_echo.rs | 2 +- ul/tests/association_store.rs | 4 ++-- ul/tests/association_store_uncompressed.rs | 4 ++-- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index e23025745..50bb2af18 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -195,7 +195,8 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool /// ```no_run /// # use dicom_ul::association::client::ClientAssociationOptions; /// # use std::time::Duration; -/// # fn run() -> Result<(), Box> { +/// #[tokio::main] +/// # async fn run() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_presentation_context("1.2.840.10008.1.1", vec!["1.2.840.10008.1.2.1", "1.2.840.10008.1.2"]) /// .timeout(Duration::from_secs(60)) diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index dd510bd1a..514a537bc 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -20,7 +20,7 @@ pub mod server; mod uid; -pub(crate) mod pdata; +pub mod pdata; pub use client::{ClientAssociation, ClientAssociationOptions}; pub use server::{ServerAssociation, ServerAssociationOptions}; diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 8aa9721fe..673c24a75 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -49,7 +49,7 @@ fn setup_pdata_header(buffer: &mut [u8], is_last: bool) { /// /// ```no_run /// # use std::io::Write; -/// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; +/// # use dicom_ul::association::{ClientAssociationOptions, pdata::PDataWriter}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } @@ -216,7 +216,7 @@ where /// /// ```no_run /// # use std::io::Read; -/// # use dicom_ul::association::{ClientAssociationOptions, PDataReader}; +/// # use dicom_ul::association::{ClientAssociationOptions, pdata::PDataReader}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } @@ -377,14 +377,14 @@ pub mod non_blocking { /// ```no_run /// # use std::io::Write; /// use tokio::io::AsyncWriteExt; - /// # use dicom_ul::association::{ClientAssociationOptions, PDataWriter}; + /// # use dicom_ul::association::{ClientAssociationOptions, pdata::non_blocking::AsyncPDataWriter}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } /// #[tokio::main] /// # async fn main() -> Result<(), Box> { /// let mut association = ClientAssociationOptions::new() - /// .establish("129.168.0.5:104") + /// .establish_async("129.168.0.5:104") /// .await?; /// /// let presentation_context_id = association.presentation_contexts()[0].id; @@ -687,7 +687,7 @@ mod tests { let mut buf = Vec::new(); { let mut writer = - AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); + AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); writer .write_all(&(0..64).collect::>()) .await @@ -803,7 +803,7 @@ mod tests { let mut buf = Vec::new(); { let mut writer = - AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE, None); + AsyncPDataWriter::new(&mut buf, presentation_context_id, MINIMUM_PDU_SIZE); writer.write_all(&my_data).await.unwrap(); writer.finish().await.unwrap(); } @@ -907,7 +907,7 @@ mod tests { let mut buf = Vec::new(); { - let mut reader = PDataReader::new(&mut pdu_stream, MINIMUM_PDU_SIZE, None); + let mut reader = PDataReader::new(&mut pdu_stream, MINIMUM_PDU_SIZE); reader.read_to_end(&mut buf).unwrap(); } assert_eq!(buf, my_data); @@ -950,7 +950,7 @@ mod tests { let inner = pdu_stream.into_inner(); let mut stream = tokio::io::BufReader::new(inner.as_slice()); { - let mut reader = PDataReader::new(&mut stream, MINIMUM_PDU_SIZE, None); + let mut reader = PDataReader::new(&mut stream, MINIMUM_PDU_SIZE); reader.read_to_end(&mut buf).await.unwrap(); } assert_eq!(buf, my_data); diff --git a/ul/tests/association_echo.rs b/ul/tests/association_echo.rs index ea84fe8c7..16804bdb9 100644 --- a/ul/tests/association_echo.rs +++ b/ul/tests/association_echo.rs @@ -77,7 +77,7 @@ async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, Socke transfer_syntax: IMPLICIT_VR_LE.to_string(), }, PresentationContextResult { - id: 2, + id: 3, reason: PresentationContextResultReason::AbstractSyntaxNotSupported, transfer_syntax: IMPLICIT_VR_LE.to_string(), } diff --git a/ul/tests/association_store.rs b/ul/tests/association_store.rs index f88be28d7..e44aed79a 100644 --- a/ul/tests/association_store.rs +++ b/ul/tests/association_store.rs @@ -81,7 +81,7 @@ async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, Socke transfer_syntax: IMPLICIT_VR_LE.to_string(), }, PresentationContextResult { - id: 2, + id: 3, reason: PresentationContextResultReason::Acceptance, transfer_syntax: JPEG_BASELINE.to_string(), } @@ -158,7 +158,7 @@ async fn scu_scp_association_test_async() { // guaranteed to be MR image storage assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); } - 2 => { + 3 => { // guaranteed to be MG image storage assert_eq!(pc.transfer_syntax, JPEG_BASELINE); } diff --git a/ul/tests/association_store_uncompressed.rs b/ul/tests/association_store_uncompressed.rs index 56124cbd5..44c753e04 100644 --- a/ul/tests/association_store_uncompressed.rs +++ b/ul/tests/association_store_uncompressed.rs @@ -93,7 +93,7 @@ async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, Socke // should always pick Explicit VR LE // because JPEG baseline was not explicitly enabled in SCP PresentationContextResult { - id: 2, + id: 3, reason: PresentationContextResultReason::Acceptance, transfer_syntax: EXPLICIT_VR_LE.to_string(), } @@ -180,7 +180,7 @@ async fn scu_scp_association_uncompressed_async() { assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); } // guaranteed to be MG image storage - 2 => { + 3 => { // server picked this one because it did not accept JPEG baseline assert_eq!(pc.transfer_syntax, EXPLICIT_VR_LE); } From d7ea692eb8904600a5e02338c9f3f5a754cf1d9a Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 6 Sep 2024 13:09:23 -0500 Subject: [PATCH 24/28] MAIN: Fix implementation of send_pdata --- ul/src/association/pdata.rs | 68 +++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 673c24a75..c86b46637 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -177,6 +177,7 @@ where self.buffer.extend(buf); debug_assert_eq!(self.buffer.len(), total_len); self.dispatch_pdu()?; + println!("{:?}", buf.len()); Ok(buf.len()) } } @@ -413,6 +414,8 @@ pub mod non_blocking { buffer: Vec, stream: W, max_data_len: u32, + msg: u32, + writing: bool, } #[cfg(feature = "async")] @@ -451,6 +454,8 @@ pub mod non_blocking { stream, max_data_len: max_data_length, buffer, + msg: 0, + writing: false, } } @@ -464,32 +469,19 @@ pub mod non_blocking { } async fn finish_impl(&mut self) -> std::io::Result<()> { + println!("Finish, {}", self.msg); if !self.buffer.is_empty() { // send last PDU setup_pdata_header(&mut self.buffer, true); - self.stream.write_all(&self.buffer[..]).await?; + if let Err(e) = self.stream.write_all(&self.buffer[..]).await { + println!("Error: {:?}", e); + } // clear buffer so that subsequent calls to `finish_impl` // do not send any more PDUs self.buffer.clear(); } Ok(()) } - - /// Use the current state of the buffer to send new PDUs - /// - /// Pre-condition: - /// buffer must have enough data for one P-Data-tf PDU - async fn dispatch_pdu(&mut self) -> std::io::Result<()> { - debug_assert!(self.buffer.len() >= 12); - // send PDU now - setup_pdata_header(&mut self.buffer, false); - self.stream.write_all(&self.buffer).await?; - - // back to just the header - self.buffer.truncate(12); - - Ok(()) - } } #[cfg(feature = "async")] @@ -502,6 +494,26 @@ pub mod non_blocking { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { + // If we're still writing (i.e. last write was pending), continue writing + if self.writing { + let this = self.get_mut(); + let buffer = &this.buffer; + let mut stream = Pin::new(&mut this.stream); + // Each call to `poll_write` may or may not write the whole of `self.buffer` + let write_all = stream.write_all(buffer); + tokio::pin!(write_all); + match write_all.poll(cx) { + Poll::Ready(Ok(_)) => { + this.writing = false; + println!("{:?}", this.msg); + this.msg += 1; + this.buffer.truncate(12); + return Poll::Ready(Ok(buf.len())); + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending, + } + } let total_len = self.max_data_len as usize + 12; if self.buffer.len() + buf.len() <= total_len { // accumulate into buffer, do nothing @@ -513,12 +525,24 @@ pub mod non_blocking { let buf = &buf[..total_len - self.buffer.len()]; self.buffer.extend(buf); debug_assert_eq!(self.buffer.len(), total_len); - let dispatch = self.dispatch_pdu(); - tokio::pin!(dispatch); - match dispatch.poll(cx) { - Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), + setup_pdata_header(&mut self.buffer, false); + let this = self.get_mut(); + let buffer = &this.buffer; + let mut stream = Pin::new(&mut this.stream); + // Each call to `poll_write` may or may not write the whole of `self.buffer` + let write_all = stream.write_all(buffer); + tokio::pin!(write_all); + match write_all.poll(cx) { + Poll::Ready(Ok(_)) => { + this.msg += 1; + this.buffer.truncate(12); + Poll::Ready(Ok(buf.len())) + } Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, + Poll::Pending => { + this.writing = true; + Poll::Pending + } } } } From f833f00a1472e04190438920e411ab64c7c111e2 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 6 Sep 2024 13:10:49 -0500 Subject: [PATCH 25/28] ENH: Update storescu with new implementation * Add `--blocking` flag with default non-blocking implementation --- storescu/Cargo.toml | 4 +--- storescu/src/main.rs | 47 ++++++++++++++++++++----------------- storescu/src/store_async.rs | 19 ++++++++------- storescu/src/store_sync.rs | 8 +++---- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/storescu/Cargo.toml b/storescu/Cargo.toml index f164f9a93..a1fd3c261 100644 --- a/storescu/Cargo.toml +++ b/storescu/Cargo.toml @@ -14,7 +14,6 @@ readme = "README.md" default = ["transcode"] # support DICOM transcoding transcode = ["dep:dicom-pixeldata"] -async = ["dicom-ul/async", "dep:tokio"] [dependencies] clap = { version = "4.0.18", features = ["derive"] } @@ -24,7 +23,7 @@ dicom-encoding = { path = "../encoding/", version = "0.7.1" } dicom-object = { path = '../object', version = "0.7.1" } dicom-pixeldata = { version = "0.7.1", path = "../pixeldata", optional = true } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.7.1" } -dicom-ul = { path = '../ul', version = "0.7.1" } +dicom-ul = { path = '../ul', version = "0.7.1", features = ["async"] } walkdir = "2.3.2" indicatif = "0.17.0" tracing = "0.1.34" @@ -34,4 +33,3 @@ snafu = "0.8" [dependencies.tokio] version = "1.38.0" features = ["rt", "rt-multi-thread", "macros"] -optional = true diff --git a/storescu/src/main.rs b/storescu/src/main.rs index 623388952..593f6bb0c 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -16,9 +16,7 @@ use tracing::{debug, error, info, warn, Level}; use transfer_syntax::TransferSyntaxIndex; use walkdir::WalkDir; -#[cfg(feature = "async")] mod store_async; -#[cfg(not(feature = "async"))] mod store_sync; /// DICOM C-STORE SCU @@ -91,6 +89,9 @@ struct App { conflicts_with("saml_assertion") )] jwt: Option, + + #[arg(long = "blocking")] + blocking: bool, } struct DicomFile { @@ -131,21 +132,25 @@ enum Error { }, } -#[cfg(not(feature = "async"))] fn main() { - run().unwrap_or_else(|e| { - error!("{}", Report::from_error(e)); - std::process::exit(-2); - }); -} - -#[cfg(feature = "async")] -#[tokio::main] -async fn main() { - run().await.unwrap_or_else(|e| { - error!("{}", Report::from_error(e)); - std::process::exit(-2); - }); + let app = App::parse(); + if !app.blocking { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + run_async().await.unwrap_or_else(|e| { + error!("{}", Report::from_error(e)); + std::process::exit(-2); + }); + }); + } else { + run(app).unwrap_or_else(|e| { + error!("{}", Report::from_error(e)); + std::process::exit(-2); + }); + } } fn check_files( @@ -212,8 +217,7 @@ fn check_files( (dicom_files, presentation_contexts) } -#[cfg(not(feature = "async"))] -fn run() -> Result<(), Error> { +fn run(app: App) -> Result<(), Error> { use crate::store_sync::{get_scu, send_file}; let App { addr, @@ -230,7 +234,8 @@ fn run() -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, - } = App::parse(); + blocking: _, + } = app; // never transcode if the feature is disabled if cfg!(not(feature = "transcode")) { @@ -332,8 +337,7 @@ fn run() -> Result<(), Error> { Ok(()) } -#[cfg(feature = "async")] -async fn run() -> Result<(), Error> { +async fn run_async() -> Result<(), Error> { use crate::store_async::{get_scu, send_file}; let App { addr, @@ -350,6 +354,7 @@ async fn run() -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, + blocking: _, } = App::parse(); // never transcode if the feature is disabled diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs index 3be699fda..da3dcae6a 100644 --- a/storescu/src/store_async.rs +++ b/storescu/src/store_async.rs @@ -10,7 +10,7 @@ use dicom_ul::{ }; use indicatif::ProgressBar; use snafu::{OptionExt, ResultExt}; -use tokio::io::AsyncWriteExt; +use tokio::{io::AsyncWriteExt, net::TcpStream}; use tracing::{debug, error, info, warn}; use crate::{ @@ -30,7 +30,7 @@ pub async fn get_scu( saml_assertion: Option, jwt: Option, presentation_contexts: HashSet<(String, String)>, -) -> Result { +) -> Result, Error> { let mut scu_init = ClientAssociationOptions::new() .calling_ae_title(calling_ae_title) .max_pdu_length(max_pdu_length); @@ -63,17 +63,20 @@ pub async fn get_scu( scu_init = scu_init.jwt(jwt); } - scu_init.establish_with(&addr).await.context(InitScuSnafu) + scu_init + .establish_with_async(&addr) + .await + .context(InitScuSnafu) } pub async fn send_file( - mut scu: ClientAssociation, + mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool, -) -> Result { +) -> Result, Error> { if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { if let Some(pb) = &progress_bar { pb.set_message(file.sop_instance_uid.clone()); @@ -154,10 +157,8 @@ pub async fn send_file( { let mut pdata = scu.send_pdata(pc_selected.id).await; - pdata - .write_all(&object_data) - .await - .whatever_context("Failed to send C-STORE-RQ P-Data")?; + pdata.write_all(&object_data).await.unwrap(); + //.whatever_context("Failed to send C-STORE-RQ P-Data")?; } } diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs index 5e8e02736..148188913 100644 --- a/storescu/src/store_sync.rs +++ b/storescu/src/store_sync.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, io::Write}; +use std::{collections::HashSet, io::Write, net::TcpStream}; use dicom_dictionary_std::tags; use dicom_encoding::TransferSyntaxIndex; @@ -29,7 +29,7 @@ pub fn get_scu( saml_assertion: Option, jwt: Option, presentation_contexts: HashSet<(String, String)>, -) -> Result { +) -> Result, Error> { let mut scu_init = ClientAssociationOptions::new() .calling_ae_title(calling_ae_title) .max_pdu_length(max_pdu_length); @@ -66,13 +66,13 @@ pub fn get_scu( } pub fn send_file( - mut scu: ClientAssociation, + mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool, -) -> Result { +) -> Result, Error> { if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { if let Some(pb) = &progress_bar { pb.set_message(file.sop_instance_uid.clone()); From 618bb93db581b4c0531619a51f07c8caae54d97b Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 6 Sep 2024 14:55:19 -0500 Subject: [PATCH 26/28] MAIN: Fix warnings from beta toolchain --- ul/src/association/client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 50bb2af18..250beca5a 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -501,6 +501,7 @@ impl<'a> ClientAssociationOptions<'a> { /// # Ok(()) /// # } /// ``` + #[allow(unreachable_patterns)] pub fn establish_with(self, ae_address: &str) -> Result> { match ae_address.try_into() { Ok(ae_address) => self.establish_impl(ae_address), @@ -1288,6 +1289,7 @@ pub mod non_blocking { /// # Ok(()) /// # } /// ``` + #[allow(unreachable_patterns)] pub async fn establish_with_async( self, ae_address: &str, From cc75478971aef311a4e4c7839cdeb7b0b047a072 Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 4 Oct 2024 07:55:33 -0500 Subject: [PATCH 27/28] MAIN: Format and use fully qualified names for TCPStream --- ul/src/association/client.rs | 64 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 250beca5a..59348cb18 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -6,10 +6,7 @@ //! for details and examples on how to create an association. use bytes::BytesMut; use std::{borrow::Cow, convert::TryInto, io::Cursor, net::ToSocketAddrs, time::Duration}; -use std::{ - io::{BufRead, BufReader, Read, Write}, - net::TcpStream, -}; +use std::io::{BufRead, BufReader, Read, Write}; use crate::{ pdu::{ @@ -129,6 +126,7 @@ pub enum Error { pub type Result = std::result::Result; +/// Helper function to get a PDU from a reader pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool) -> Result { // Receive response @@ -172,7 +170,7 @@ pub fn get_client_pdu(reader: &mut R, max_pdu_length: u32, strict: bool /// /// > **⚠️ Warning:** It is highly recommended to set `timeout` to a reasonable value for the /// > async client since there is _no_ default timeout on -/// > [`tokio::net::TcpStream`] +/// > [`tokio::net::TcpStream`], see the [`ClientAssociationOptions::timeout`] method for details. /// /// ## Basic usage /// @@ -474,7 +472,7 @@ impl<'a> ClientAssociationOptions<'a> { /// Initiate the TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. - pub fn establish(self, address: A) -> Result> { + pub fn establish(self, address: A) -> Result> { self.establish_impl(AeAddr::new_socket_addr(address)) } @@ -502,7 +500,7 @@ impl<'a> ClientAssociationOptions<'a> { /// # } /// ``` #[allow(unreachable_patterns)] - pub fn establish_with(self, ae_address: &str) -> Result> { + pub fn establish_with(self, ae_address: &str) -> Result> { match ae_address.try_into() { Ok(ae_address) => self.establish_impl(ae_address), Err(_) => self.establish_impl(AeAddr::new_socket_addr(ae_address)), @@ -510,6 +508,8 @@ impl<'a> ClientAssociationOptions<'a> { } /// Set the read timeout for the underlying TCP socket + /// + /// This is used to set both the read and write timeout. pub fn timeout(self, timeout: Duration) -> Self { Self { timeout: Some(timeout), @@ -517,7 +517,7 @@ impl<'a> ClientAssociationOptions<'a> { } } - fn establish_impl(self, ae_address: AeAddr) -> Result> + fn establish_impl(self, ae_address: AeAddr) -> Result> where T: ToSocketAddrs, { @@ -761,20 +761,23 @@ impl<'a> ClientAssociationOptions<'a> { } } +/// Trait to close underlying socket pub trait CloseSocket { fn close(&mut self) -> std::io::Result<()>; } -impl CloseSocket for TcpStream { +impl CloseSocket for std::net::TcpStream { fn close(&mut self) -> std::io::Result<()> { self.shutdown(std::net::Shutdown::Both) } } + +/// Trait to release association pub trait Release { fn release(&mut self) -> Result<()>; } -impl Release for ClientAssociation { +impl Release for ClientAssociation { fn release(&mut self) -> Result<()> { self.release_impl() } @@ -793,6 +796,9 @@ impl Release for ClientAssociation { /// the program will automatically try to gracefully release the association /// through a standard C-RELEASE message exchange, /// then shut down the underlying TCP connection. +/// +/// This may either be sync or async depending on which method was called to +/// establish the association. #[derive(Debug)] pub struct ClientAssociation where @@ -848,9 +854,9 @@ where } } -impl ClientAssociation +impl ClientAssociation where - ClientAssociation: Release, + ClientAssociation: Release, { /// Send a PDU message to the other intervenient. pub fn send(&mut self, msg: &Pdu) -> Result<()> { @@ -925,7 +931,7 @@ where /// **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 TcpStream { + pub fn inner_stream(&mut self) -> &mut std::net::TcpStream { &mut self.socket } @@ -934,7 +940,7 @@ where /// /// Returns a writer which automatically /// splits the inner data into separate PDUs if necessary. - pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { + pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut std::net::TcpStream> { PDataWriter::new( &mut self.socket, presentation_context_id, @@ -947,7 +953,7 @@ where /// /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. - pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + pub fn receive_pdata(&mut self) -> PDataReader<&mut std::net::TcpStream> { PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } @@ -1015,10 +1021,7 @@ pub mod non_blocking { }; use bytes::{Buf, BytesMut}; use snafu::{ensure, ResultExt}; - use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWriteExt}, - net::TcpStream, - }; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; use tracing::warn; pub async fn get_client_pdu_async( @@ -1141,7 +1144,7 @@ pub mod non_blocking { }); let socket_addrs: Vec<_> = ae_address.to_socket_addrs().unwrap().collect(); - let mut socket = TcpStream::connect(socket_addrs.as_slice()) + let mut socket = tokio::net::TcpStream::connect(socket_addrs.as_slice()) .await .context(ConnectSnafu)?; let mut buffer: Vec = Vec::with_capacity(max_pdu_length as usize); @@ -1259,7 +1262,7 @@ pub mod non_blocking { pub async fn establish_async( self, address: A, - ) -> Result> { + ) -> Result> { self.establish_impl_async(AeAddr::new_socket_addr(address)) .await } @@ -1293,20 +1296,19 @@ pub mod non_blocking { pub async fn establish_with_async( self, ae_address: &str, - ) -> Result> { + ) -> Result> { match ae_address.try_into() { Ok(ae_address) => self.establish_impl_async(ae_address).await, Err(_) => { - self.establish_impl_async(AeAddr::new_socket_addr(ae_address)) - .await + self.establish_impl_async(AeAddr::new_socket_addr(ae_address)).await } } } } - impl ClientAssociation + impl ClientAssociation where - ClientAssociation: Release, + ClientAssociation: Release, { /// Send a PDU message to the other intervenient. pub async fn send(&mut self, msg: &Pdu) -> Result<()> { @@ -1422,7 +1424,7 @@ pub mod non_blocking { pub async fn send_pdata( &mut self, presentation_context_id: u8, - ) -> AsyncPDataWriter<&mut TcpStream> { + ) -> AsyncPDataWriter<&mut tokio::net::TcpStream> { AsyncPDataWriter::new( &mut self.socket, presentation_context_id, @@ -1436,7 +1438,7 @@ pub mod non_blocking { /// Returns a reader which automatically /// receives more data PDUs once the bytes collected are consumed. #[cfg(feature = "async")] - pub fn receive_pdata(&mut self) -> PDataReader<&mut TcpStream> { + pub fn receive_pdata(&mut self) -> PDataReader<&mut tokio::net::TcpStream> { PDataReader::new(&mut self.socket, self.requestor_max_pdu_length) } @@ -1484,12 +1486,12 @@ pub mod non_blocking { /// **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 TcpStream { + pub fn inner_stream(&mut self) -> &mut tokio::net::TcpStream { &mut self.socket } } - impl Release for ClientAssociation { + impl Release for ClientAssociation { fn release(&mut self) -> super::Result<()> { tokio::task::block_in_place(move || { tokio::runtime::Handle::current().block_on(async move { self.release_impl().await }) @@ -1497,7 +1499,7 @@ pub mod non_blocking { } } /// Automatically release the association and shut down the connection. - impl CloseSocket for TcpStream { + impl CloseSocket for tokio::net::TcpStream { fn close(&mut self) -> std::io::Result<()> { tokio::task::block_in_place(move || { tokio::runtime::Handle::current().block_on(async move { self.shutdown().await }) From e316a739c56e732c4ff42eb980ca1853dc83807b Mon Sep 17 00:00:00 2001 From: Nate Richman Date: Fri, 4 Oct 2024 08:09:52 -0500 Subject: [PATCH 28/28] MAIN: Update store-scp with new ul code as well --- storescp/Cargo.toml | 7 ++----- storescp/src/store_async.rs | 5 +++-- storescp/src/store_sync.rs | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index 04c0b6655..91c960ab9 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] clap = { version = "4.0.18", features = ["derive"] } dicom-core = { path = '../core', version = "0.7.0" } -dicom-ul = { path = '../ul', version = "0.7.1" } +dicom-ul = { path = '../ul', version = "0.7.1", features = ["async"] } dicom-object = { path = '../object', version = "0.7.1" } dicom-encoding = { path = "../encoding/", version = "0.7.1" } dicom-dictionary-std = { path = "../dictionary-std/", version = "0.7.0" } @@ -21,8 +21,5 @@ dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", versio snafu = "0.8" tracing = "0.1.36" tracing-subscriber = "0.3.15" -tokio = { version = "1.38.0", features = ["full"], optional = true } +tokio = { version = "1.38.0", features = ["full"] } -[features] -default = [] -async = ["dicom-ul/async", "dep:tokio"] diff --git a/storescp/src/store_async.rs b/storescp/src/store_async.rs index 703eeb0cc..9407037ff 100644 --- a/storescp/src/store_async.rs +++ b/storescp/src/store_async.rs @@ -7,7 +7,7 @@ use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; use crate::{transfer::ABSTRACT_SYNTAXES, App, create_cecho_response, create_cstore_response}; -pub async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { +pub async fn run_store_async(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Whatever> { let App { verbose, calling_ae_title, @@ -17,6 +17,7 @@ pub async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Wh max_pdu_length, out_dir, port: _, + blocking: _, } = args; let verbose = *verbose; @@ -49,7 +50,7 @@ pub async fn run(scu_stream: tokio::net::TcpStream, args: &App) -> Result<(), Wh } let mut association = options - .establish(scu_stream) + .establish_async(scu_stream) .await .whatever_context("could not establish association")?; diff --git a/storescp/src/store_sync.rs b/storescp/src/store_sync.rs index e2e57fbc9..a5348d2f4 100644 --- a/storescp/src/store_sync.rs +++ b/storescp/src/store_sync.rs @@ -9,7 +9,7 @@ use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; use crate::{create_cecho_response, create_cstore_response, transfer::ABSTRACT_SYNTAXES, App}; -pub fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { +pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { let App { verbose, calling_ae_title, @@ -19,6 +19,7 @@ pub fn run(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> { max_pdu_length, out_dir, port: _, + blocking: _, } = args; let verbose = *verbose;