diff --git a/.gitignore b/.gitignore index e900fa9..12b595b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,8 @@ target/ # VSCode .vscode/ -# Fleet -.fleet/ +# Zed editor +.zed/ # Avoid adding config files that overwrite the default config -config.toml \ No newline at end of file +config.toml diff --git a/Cargo.toml b/Cargo.toml index 26f0a32..412fc7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ nursery = "warn" cargo = "warn" module_name_repetitions = "allow" must_use_candidate = "allow" +lint_groups_priority = "allow" [profile.release] codegen-units = 1 diff --git a/src/api/aets.rs b/src/api/aets.rs index f57bc4c..da7d210 100644 --- a/src/api/aets.rs +++ b/src/api/aets.rs @@ -15,8 +15,8 @@ async fn all_aets(state: State) -> impl IntoResponse { let aets = &state.config.aets; Json(serde_json::Value::Array( - aets.into_iter() - .map(|ae| serde_json::Value::String(ae.aet.to_owned())) + aets.iter() + .map(|ae| serde_json::Value::String(ae.aet.clone())) .collect::>(), )) } diff --git a/src/api/mod.rs b/src/api/mod.rs index 1405d65..657f1db 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -68,7 +68,7 @@ impl TryFrom> for MatchCriteria { } } -/// helper function to convert a query parameter value to a PrimitiveValue +/// helper function to convert a query parameter value to a `PrimitiveValue` fn to_primitive_value(tag: Tag, raw_value: &str) -> Result { if raw_value.is_empty() { return Ok(PrimitiveValue::Empty); @@ -135,8 +135,7 @@ fn to_primitive_value(tag: Tag, raw_value: &str) -> Result Err(format!( - "Attribute {} cannot be used for matching due to unsupported VR {:?}", - tag, vr + "Attribute {tag} cannot be used for matching due to unsupported VR {vr:?}", )), } } diff --git a/src/api/mwl/routes.rs b/src/api/mwl/routes.rs index b59c455..1b65400 100644 --- a/src/api/mwl/routes.rs +++ b/src/api/mwl/routes.rs @@ -12,7 +12,7 @@ use dicom_json::DicomJson; use futures::TryStreamExt; use tracing::instrument; -use super::{MwlQueryParameters, MwlRequestHeaderFields, MwlSearchError, MwlSearchRequest}; +use super::{MwlQueryParameters, MwlSearchError, MwlSearchRequest}; /// HTTP Router for the Modality Worklist. /// @@ -58,9 +58,6 @@ async fn all_workitems( provider: ServiceProvider, Query(parameters): Query, ) -> impl IntoResponse { - let request = MwlSearchRequest { - parameters, - headers: MwlRequestHeaderFields::default(), - }; + let request = MwlSearchRequest { parameters }; mwl_handler(provider, request).await } diff --git a/src/api/mwl/service.rs b/src/api/mwl/service.rs index 3a118de..edbec7b 100644 --- a/src/api/mwl/service.rs +++ b/src/api/mwl/service.rs @@ -16,7 +16,6 @@ pub trait MwlService: Send + Sync { pub struct MwlSearchRequest { pub parameters: MwlQueryParameters, - pub headers: MwlRequestHeaderFields, } /// Query parameters for a MWL-RS request. @@ -48,25 +47,6 @@ impl Default for MwlQueryParameters { } } -#[derive(Debug, Default)] -pub struct MwlRequestHeaderFields { - pub accept: Option, - pub accept_charset: Option, -} - -/// -#[derive(Debug, Default)] -pub struct ResponseHeaderFields { - /// The DICOM Media Type of the response payload. - /// Shall be present if the response has a payload. - pub content_type: Option, - /// Shall be present if no transfer coding has been applied to the payload. - pub content_length: Option, - /// Shall be present if a transfer encoding has been applied to the payload. - pub transfer_encoding: Option, - pub warning: Vec, -} - pub struct MwlSearchResponse<'a> { pub stream: BoxStream<'a, Result>, } diff --git a/src/api/qido/routes.rs b/src/api/qido/routes.rs index 34684fd..84ecc3d 100644 --- a/src/api/qido/routes.rs +++ b/src/api/qido/routes.rs @@ -1,6 +1,4 @@ -use crate::api::qido::{ - QueryParameters, RequestHeaderFields, ResourceQuery, SearchError, SearchRequest, -}; +use crate::api::qido::{QueryParameters, ResourceQuery, SearchError, SearchRequest}; use crate::backend::ServiceProvider; use crate::types::QueryRetrieveLevel; use crate::AppState; @@ -15,7 +13,6 @@ use axum_streams::StreamBodyAs; use dicom::object::InMemDicomObject; use dicom_json::DicomJson; use futures::TryStreamExt; -use std::default::Default; use tracing::instrument; /// HTTP Router for the Search Transaction. @@ -74,7 +71,6 @@ async fn all_studies( series_instance_uid: None, }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } @@ -92,7 +88,6 @@ async fn studys_series( series_instance_uid: None, }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } @@ -110,7 +105,6 @@ async fn studys_series_instances( series_instance_uid: Some(series), }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } @@ -128,7 +122,6 @@ async fn studys_instances( series_instance_uid: None, }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } @@ -145,7 +138,6 @@ async fn all_series( series_instance_uid: None, }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } @@ -162,7 +154,6 @@ async fn all_instances( series_instance_uid: None, }, parameters, - headers: RequestHeaderFields::default(), }; qido_handler(provider, request).await } diff --git a/src/api/qido/service.rs b/src/api/qido/service.rs index 2fd088e..fb4b2bf 100644 --- a/src/api/qido/service.rs +++ b/src/api/qido/service.rs @@ -19,7 +19,6 @@ pub trait QidoService: Send + Sync { pub struct SearchRequest { pub query: ResourceQuery, pub parameters: QueryParameters, - pub headers: RequestHeaderFields, } /// Query parameters for a QIDO-RS request. @@ -51,25 +50,6 @@ impl Default for QueryParameters { } } -#[derive(Debug, Default)] -pub struct RequestHeaderFields { - pub accept: Option, - pub accept_charset: Option, -} - -/// -#[derive(Debug, Default)] -pub struct ResponseHeaderFields { - /// The DICOM Media Type of the response payload. - /// Shall be present if the response has a payload. - pub content_type: Option, - /// Shall be present if no transfer coding has been applied to the payload. - pub content_length: Option, - /// Shall be present if a transfer encoding has been applied to the payload. - pub transfer_encoding: Option, - pub warning: Vec, -} - pub struct SearchResponse<'a> { pub stream: BoxStream<'a, Result>, } diff --git a/src/api/stow/routes.rs b/src/api/stow/routes.rs index 43a60d7..75cb466 100644 --- a/src/api/stow/routes.rs +++ b/src/api/stow/routes.rs @@ -16,7 +16,7 @@ use multer::Error; use tracing::{error, instrument, warn}; /// HTTP Router for the Store Transaction -/// https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5 +/// pub fn routes() -> Router { Router::new() .route("/studies", post(studies)) @@ -37,38 +37,33 @@ async fn studies( instances.push(file); } Err(err) => { - let err = match &err { - Error::StreamReadFailed(stream_error) => { - let is_limit_exceeded = stream_error - .downcast_ref::() - .and_then(std::error::Error::source) - .and_then(|err| err.downcast_ref::()) - .is_some(); + let err = if let Error::StreamReadFailed(stream_error) = &err { + let is_limit_exceeded = stream_error + .downcast_ref::() + .and_then(std::error::Error::source) + .and_then(|err| err.downcast_ref::()) + .is_some(); - if is_limit_exceeded { - warn!("Upload limit exceeded."); - StoreError::UploadLimitExceeded - } else { - error!("Failed to read multipart stream: {err:?}"); - StoreError::Stream(err) - } - } - _ => { - error!("Failed to read multipart stream: {:?}", err); + if is_limit_exceeded { + warn!("Upload limit exceeded."); + StoreError::UploadLimitExceeded + } else { + error!("Failed to read multipart stream: {err:?}"); StoreError::Stream(err) } + } else { + error!("Failed to read multipart stream: {:?}", err); + StoreError::Stream(err) }; return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(); } }; } - let request = StoreRequest { - instances, - study_instance_uid: None, // TODO - }; + let request = StoreRequest { instances }; if let Some(stow) = provider.stow { + #[allow(clippy::option_if_let_else)] if let Ok(response) = stow.store(request).await { let json = DicomJson::from(InMemDicomObject::from(response)); diff --git a/src/api/stow/service.rs b/src/api/stow/service.rs index caf4915..150dea9 100644 --- a/src/api/stow/service.rs +++ b/src/api/stow/service.rs @@ -10,7 +10,6 @@ use thiserror::Error; pub struct StoreRequest { pub instances: Vec>, - pub study_instance_uid: Option, } /// @@ -89,10 +88,6 @@ pub trait StowService: Sync + Send { #[derive(Debug, Error)] pub enum StoreError { - #[error("Instance does not match the provided Study Instance UID")] - StudyInstanceUidMismatch { study_instance_uid: String }, - #[error("The media type {media_type} is not supported")] - UnsupportedMediaType { media_type: String }, #[error("The file exceeds the configured upload size limit")] UploadLimitExceeded, #[error(transparent)] diff --git a/src/api/wado/routes.rs b/src/api/wado/routes.rs index 553d0c0..ce5eb6c 100644 --- a/src/api/wado/routes.rs +++ b/src/api/wado/routes.rs @@ -1,6 +1,5 @@ use crate::api::wado::{ - MetadataRequest, RenderedResponse, RenderingRequest, RetrieveError, RetrieveInstanceRequest, - ThumbnailRequest, + MetadataRequest, RenderedResponse, RenderingRequest, RetrieveInstanceRequest, ThumbnailRequest, }; use crate::backend::dimse::cmove::movescu::MoveError; use crate::backend::dimse::wado::DicomMultipartStream; @@ -8,7 +7,6 @@ use crate::backend::ServiceProvider; use crate::types::UI; use crate::AppState; use axum::body::Body; -use axum::extract::State; use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; use axum::http::{Response, StatusCode, Uri}; use axum::response::{IntoResponse, Redirect}; @@ -26,7 +24,7 @@ use std::sync::Arc; use tracing::{error, instrument}; /// HTTP Router for the Retrieve Transaction -/// https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4 +/// #[rustfmt::skip] pub fn routes() -> Router { Router::new() @@ -135,7 +133,6 @@ async fn rendered_resource( async fn metadata_resource( provider: ServiceProvider, request: MetadataRequest, - state: &AppState, ) -> impl IntoResponse { let Some(wado) = provider.wado else { return Response::builder() @@ -179,7 +176,7 @@ async fn metadata_resource( } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BulkdataRemovalOptions { pub max_length: u32, } @@ -255,28 +252,19 @@ async fn instance( instance_resource(provider, request).await } -async fn study_metadata( - provider: ServiceProvider, - request: MetadataRequest, - State(state): State, -) -> impl IntoResponse { - metadata_resource(provider, request, &state).await +async fn study_metadata(provider: ServiceProvider, request: MetadataRequest) -> impl IntoResponse { + metadata_resource(provider, request).await } -async fn series_metadata( - provider: ServiceProvider, - request: MetadataRequest, - State(state): State, -) -> impl IntoResponse { - metadata_resource(provider, request, &state).await +async fn series_metadata(provider: ServiceProvider, request: MetadataRequest) -> impl IntoResponse { + metadata_resource(provider, request).await } async fn instance_metadata( provider: ServiceProvider, request: MetadataRequest, - State(state): State, ) -> impl IntoResponse { - metadata_resource(provider, request, &state).await + metadata_resource(provider, request).await } #[instrument(skip_all)] diff --git a/src/api/wado/service.rs b/src/api/wado/service.rs index e3a5952..cb51347 100644 --- a/src/api/wado/service.rs +++ b/src/api/wado/service.rs @@ -11,7 +11,7 @@ use axum::response::{IntoResponse, Response}; use dicom::object::{FileDicomObject, InMemDicomObject}; use futures::stream::BoxStream; use serde::de::{Error, Visitor}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer}; use std::fmt::{Debug, Formatter}; use std::num::ParseIntError; use std::str::FromStr; @@ -36,14 +36,12 @@ pub enum RetrieveError { Backend { source: anyhow::Error }, } -pub type RetrieveInstanceRequest = RetrieveRequest; -pub type RenderedRequest = RetrieveRequest; -pub type ThumbnailRequest = RetrieveRequest; +pub struct RetrieveInstanceRequest { + pub query: ResourceQuery, +} -pub struct RetrieveRequest { +pub struct ThumbnailRequest { pub query: ResourceQuery, - pub parameters: Q, - pub headers: RequestHeaderFields, } #[derive(Debug, Clone, PartialEq)] @@ -52,25 +50,25 @@ pub struct RenderingRequest { pub options: RenderingOptions, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MetadataRequest { pub query: ResourceQuery, } -/// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#table_8.3.5-1 +/// #[derive(Debug, PartialEq, Deserialize)] pub struct RetrieveRenderedQueryParameters { - /// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.3.html#sect_8.3.3.1 + /// pub accept: Option, - /// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.2 + /// pub quality: Option, - /// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.3 + /// #[serde(deserialize_with = "deserialize_viewport", default)] pub viewport: Option, - /// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.4 + /// #[serde(deserialize_with = "deserialize_window", default)] pub window: Option, - /// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.5 + /// #[serde(rename = "iccprofile")] pub icc_profile: Option, } @@ -145,24 +143,7 @@ where .await .map_err(PathRejection::into_response)?; - let Query(parameters): Query = - Query::from_request_parts(parts, state) - .await - .map_err(QueryRejection::into_response)?; - - let accept = parts - .headers - .get(ACCEPT) - .map(|h| String::from(h.to_str().unwrap_or_default())); - - Ok(Self { - query, - parameters, - headers: RequestHeaderFields { - accept, - ..RequestHeaderFields::default() - }, - }) + Ok(Self { query }) } } @@ -178,17 +159,7 @@ where .await .map_err(PathRejection::into_response)?; - let Query(parameters): Query = - Query::from_request_parts(parts, state) - .await - .map_err(QueryRejection::into_response)?; - - Ok(Self { - query, - parameters, - // TODO: currently unused - headers: RequestHeaderFields::default(), - }) + Ok(Self { query }) } } @@ -198,7 +169,7 @@ pub struct InstanceResponse { pub struct RenderedResponse(pub Vec); -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct ResourceQuery { #[serde(rename = "aet")] pub aet: AE, @@ -210,35 +181,6 @@ pub struct ResourceQuery { pub sop_instance_uid: Option, } -#[derive(Debug, Default)] -pub struct RequestHeaderFields { - pub accept: Option, - pub accept_charset: Option, -} - -#[derive(Debug, Default)] -pub struct ResponseHeaderFields { - pub content_type: Option, -} - -pub trait QueryParameters {} -impl QueryParameters for InstanceQueryParameters {} -impl QueryParameters for MetadataQueryParameters {} -impl QueryParameters for RenderedQueryParameters {} -impl QueryParameters for ThumbnailQueryParameters {} - -#[derive(Debug, Default, Deserialize)] -pub struct InstanceQueryParameters { - /// Should not be used when the Accept header can be used instead. - pub accept: Option, -} - -#[derive(Debug, Default)] -pub struct MetadataQueryParameters { - pub accept: Option, - pub charset: Option, -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize)] pub struct ImageQuality(u8); @@ -249,7 +191,7 @@ impl ImageQuality { _ => Err(ParseImageQualityError::OutOfRange { value }), } } - pub const fn as_u8(&self) -> u8 { + pub const fn as_u8(self) -> u8 { self.0 } } @@ -286,17 +228,11 @@ impl FromStr for ImageQuality { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ImageAnnotation { - Patient, - Technique, -} - /// Controls the viewport scaling of the images or video /// -/// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.3.5.html#sect_8.3.5.1.3 -#[derive(Debug, Clone, PartialEq, Deserialize)] +/// +#[allow(clippy::struct_field_names)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct Viewport { /// Width of the viewport in pixels. pub viewport_width: u32, @@ -314,7 +250,7 @@ pub struct Viewport { struct ViewportVisitor; -impl<'a> Visitor<'a> for ViewportVisitor { +impl Visitor<'_> for ViewportVisitor { type Value = Option; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -375,7 +311,7 @@ pub struct Window { /// [`crate::dicomweb::qido::IncludeField::All`] is returned instead. struct WindowVisitor; -impl<'a> Visitor<'a> for WindowVisitor { +impl Visitor<'_> for WindowVisitor { type Value = Option; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { @@ -408,9 +344,10 @@ where } /// -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] pub enum VoiLutFunction { /// + #[default] Linear, /// LinearExact, @@ -418,12 +355,6 @@ pub enum VoiLutFunction { Sigmoid, } -impl Default for VoiLutFunction { - fn default() -> Self { - Self::Linear - } -} - #[derive(Debug, Error)] pub enum ParseVoiLutFunctionError { #[error("Unknown VOI LUT function: {function}")] @@ -446,7 +377,7 @@ impl FromStr for VoiLutFunction { /// Specifies the inclusion of an ICC Profile in the rendered images. /// /// -#[derive(Debug, Copy, Clone, PartialEq, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] pub enum IccProfile { /// Indicates that no ICC profile shall be present in the rendered image in the response. No, @@ -469,33 +400,6 @@ pub enum IccProfile { /// \[ISO 22028-2]. RommRgb, } -impl ImageAnnotation { - pub const fn as_str(&self) -> &str { - match self { - Self::Patient => "patient", - Self::Technique => "technique", - } - } -} - -#[derive(Debug, Default, Deserialize, PartialEq)] -pub struct RenderedQueryParameters { - pub accept: Option, - pub annotation: Option, - pub quality: Option, - #[serde(deserialize_with = "deserialize_viewport", default)] - pub viewport: Option, - #[serde(deserialize_with = "deserialize_window", default)] - pub window: Option, - pub iccprofile: Option, -} - -#[derive(Debug, Default, Deserialize, PartialEq)] -pub struct ThumbnailQueryParameters { - pub accept: Option, - #[serde(deserialize_with = "deserialize_viewport", default)] - pub viewport: Option, -} #[cfg(test)] mod tests { @@ -530,13 +434,12 @@ mod tests { fn parse_rendered_query_params() { let uri = Uri::from_static("http://test?window=100,200,SIGMOID&viewport=100,100,0,0,100,100"); - let Query(params) = Query::::try_from_uri(&uri).unwrap(); + let Query(params) = Query::::try_from_uri(&uri).unwrap(); assert_eq!( params, - RenderedQueryParameters { + RetrieveRenderedQueryParameters { accept: None, - annotation: None, quality: None, viewport: Some(Viewport { viewport_width: 100, @@ -551,7 +454,7 @@ mod tests { width: 200.0, function: VoiLutFunction::Sigmoid, }), - iccprofile: None, + icc_profile: None, } ); } diff --git a/src/backend/dimse/association/client.rs b/src/backend/dimse/association/client.rs index 6745690..03f903b 100644 --- a/src/backend/dimse/association/client.rs +++ b/src/backend/dimse/association/client.rs @@ -17,7 +17,6 @@ pub struct ClientAssociation { uuid: Uuid, tcp_stream: TcpStream, presentation_context: Vec, - acceptor_max_pdu_length: u32, } pub struct ClientAssociationOptions { @@ -89,7 +88,6 @@ impl ClientAssociation { ); let presentation_contexts = Vec::from(association.presentation_contexts()); - let acceptor_max_pdu_length = association.acceptor_max_pdu_length(); let stream = association .inner_stream() @@ -97,7 +95,7 @@ impl ClientAssociation { .expect("TcpStream should be cloneable"); connect_tx - .send(Ok((stream, presentation_contexts, acceptor_max_pdu_length))) + .send(Ok((stream, presentation_contexts))) .map_err(|_| ())?; association @@ -148,7 +146,7 @@ impl ClientAssociation { }) .map_err(AssociationError::OsThread)?; - let (tcp_stream, presentation_context, acceptor_max_pdu_length) = + let (tcp_stream, presentation_context) = connect_result.await.expect("connect_result.await")?; Ok(Self { @@ -156,11 +154,10 @@ impl ClientAssociation { uuid, tcp_stream, presentation_context, - acceptor_max_pdu_length, }) } - pub fn uuid(&self) -> &Uuid { + pub const fn uuid(&self) -> &Uuid { &self.uuid } } diff --git a/src/backend/dimse/association/mod.rs b/src/backend/dimse/association/mod.rs index 396ff36..58bafdd 100644 --- a/src/backend/dimse/association/mod.rs +++ b/src/backend/dimse/association/mod.rs @@ -1,10 +1,10 @@ -use dicom::ul::pdu::{PresentationContextNegotiated, PresentationContextResult}; +use dicom::ul::pdu::PresentationContextNegotiated; use dicom::ul::Pdu; +use std::future::Future; use std::time::Duration; use thiserror::Error; use tokio::sync::mpsc::Sender; use tokio::sync::oneshot; -use tracing::error; pub mod client; pub mod pool; @@ -23,9 +23,16 @@ pub enum AssociationError { } pub trait Association { - async fn receive(&self, timeout: Duration) -> Result; + fn receive( + &self, + timeout: Duration, + ) -> impl Future> + Send; - async fn send(&self, pdu: Pdu, timeout: Duration) -> Result<(), AssociationError>; + fn send( + &self, + pdu: Pdu, + timeout: Duration, + ) -> impl Future> + Send; fn close(&mut self); diff --git a/src/backend/dimse/association/pool.rs b/src/backend/dimse/association/pool.rs index 96151ac..045873c 100644 --- a/src/backend/dimse/association/pool.rs +++ b/src/backend/dimse/association/pool.rs @@ -67,6 +67,7 @@ impl Pool { .rposition(|slot| slot.parameter == parameter) .and_then(|position| slots.remove(position)); + #[allow(clippy::option_if_let_else)] if let Some(target_slot) = target_slot { Some(target_slot) } else { @@ -171,6 +172,7 @@ struct ObjectInner { #[derive(Debug)] pub struct Metrics { + #[allow(unused)] pub created: Instant, pub recycle_count: usize, pub last_used: Instant, @@ -261,6 +263,7 @@ impl AssociationPools { pub fn new(config: &AppConfig) -> Self { let mut pools = HashMap::with_capacity(config.server.dimse.len()); for ae_config in &config.aets { + #[allow(irrefutable_let_patterns)] // the lint is not aware of the S3 feature if let BackendConfig::Dimse(dimse_config) = &ae_config.backend { let pool_size = dimse_config.pool.size; diff --git a/src/backend/dimse/association/server.rs b/src/backend/dimse/association/server.rs index 7dc3f9b..d971f4d 100644 --- a/src/backend/dimse/association/server.rs +++ b/src/backend/dimse/association/server.rs @@ -10,7 +10,6 @@ use uuid::Uuid; #[derive(Debug)] pub struct ServerAssociation { - uuid: Uuid, channel: Sender, presentation_contexts: Vec, tcp_stream: TcpStream, @@ -30,9 +29,9 @@ impl ServerAssociation { .promiscuous(true); for syntax in TransferSyntaxRegistry.iter() { - if options.uncompressed && syntax.is_codec_free() { - server_options = server_options.with_transfer_syntax(syntax.uid()); - } else if !options.uncompressed && !syntax.is_unsupported() { + if (options.uncompressed && syntax.is_codec_free()) + || (!options.uncompressed && !syntax.is_unsupported()) + { server_options = server_options.with_transfer_syntax(syntax.uid()); } } @@ -55,11 +54,7 @@ impl ServerAssociation { "Established new server association" ); - let pcs = association - .presentation_contexts() - .into_iter() - .cloned() - .collect(); + let pcs = association.presentation_contexts().to_vec(); let stream = association .inner_stream() @@ -78,7 +73,9 @@ impl ServerAssociation { while let Some(command) = rx.blocking_recv() { let result = match command { Command::Send(pdu, response) => { - let send_result = association.send(&pdu).map_err(|e| e.into()); + let send_result = association + .send(&pdu) + .map_err(AssociationError::Association); response .send(send_result) .map_err(|_value| ChannelError::Closed) @@ -122,7 +119,6 @@ impl ServerAssociation { Ok(Self { channel: tx, - uuid, presentation_contexts, tcp_stream, }) diff --git a/src/backend/dimse/cecho/mod.rs b/src/backend/dimse/cecho/mod.rs index eec6010..4b34cb0 100644 --- a/src/backend/dimse/cecho/mod.rs +++ b/src/backend/dimse/cecho/mod.rs @@ -10,7 +10,6 @@ use dicom::object::mem::InMemElement; use dicom::object::InMemDicomObject; const COMMAND_FIELD_COMPOSITE_ECHO_REQUEST: US = 0x0030; -const COMMAND_FIELD_COMPOSITE_ECHO_RESPONSE: US = 0x8030; /// C-ECHO-RQ #[derive(Debug)] diff --git a/src/backend/dimse/cfind/mod.rs b/src/backend/dimse/cfind/mod.rs index b2cb413..b1c6f9d 100644 --- a/src/backend/dimse/cfind/mod.rs +++ b/src/backend/dimse/cfind/mod.rs @@ -10,7 +10,6 @@ pub mod findscu; // Magic numbers defined by the DICOM specification. pub const COMMAND_FIELD_COMPOSITE_FIND_REQUEST: US = 0x0020; -pub const COMMAND_FIELD_COMPOSITE_FIND_RESPONSE: US = 0x8020; /// C-FIND-RQ /// diff --git a/src/backend/dimse/cmove/mediator.rs b/src/backend/dimse/cmove/mediator.rs index e6f6e5f..053bfd3 100644 --- a/src/backend/dimse/cmove/mediator.rs +++ b/src/backend/dimse/cmove/mediator.rs @@ -7,7 +7,7 @@ use std::sync::{Arc, Weak}; use thiserror::Error; use tokio::sync::mpsc::Sender; use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore}; -use tracing::{error, info}; +use tracing::info; pub type Callback = Sender>; @@ -126,6 +126,7 @@ pub struct SubscriptionTopic { pub struct Subscription { topic: SubscriptionTopic, + #[allow(unused)] // we never use it, but still need to hold ownership permit: Option, mediator: Weak, } diff --git a/src/backend/dimse/cmove/mod.rs b/src/backend/dimse/cmove/mod.rs index 08eed26..dffa47c 100644 --- a/src/backend/dimse/cmove/mod.rs +++ b/src/backend/dimse/cmove/mod.rs @@ -1,5 +1,5 @@ use crate::backend::dimse::{DicomMessage, DATA_SET_EXISTS}; -use crate::types::{Priority, AE, US}; +use crate::types::{AE, US}; use dicom::core::{DataElement, VR}; use dicom::dicom_value; use dicom::dictionary_std::{tags, uids}; @@ -12,7 +12,6 @@ pub use mediator::*; // Magic numbers defined by the DICOM specification. pub const COMMAND_FIELD_COMPOSITE_MOVE_REQUEST: US = 0x0021; -pub const COMMAND_FIELD_COMPOSITE_MOVE_RESPONSE: US = 0x8021; /// C-MOVE-RQ pub struct CompositeMoveRequest { @@ -22,22 +21,6 @@ pub struct CompositeMoveRequest { pub destination: AE, } -impl CompositeMoveRequest { - pub fn new(message_id: US, destination: AE) -> Self { - Self { - identifier: InMemDicomObject::new_empty(), - priority: Priority::Medium as US, - message_id, - destination, - } - } - - pub fn identifier(mut self, identifier: InMemDicomObject) -> Self { - self.identifier = identifier; - self - } -} - impl From for DicomMessage { #[rustfmt::skip] fn from(request: CompositeMoveRequest) -> Self { @@ -58,9 +41,6 @@ impl From for DicomMessage { } } -/// C-MOVE-RSP -pub struct CompositeMoveResponse {} - pub enum MoveSubOperation { Completed, Pending(Arc>), diff --git a/src/backend/dimse/cmove/movescu.rs b/src/backend/dimse/cmove/movescu.rs index 6bd09f2..1bcc677 100644 --- a/src/backend/dimse/cmove/movescu.rs +++ b/src/backend/dimse/cmove/movescu.rs @@ -18,15 +18,10 @@ pub struct MoveServiceClassUser { } impl MoveServiceClassUser { - pub fn new(pool: AssociationPool, timeout: Duration) -> Self { + pub const fn new(pool: AssociationPool, timeout: Duration) -> Self { Self { pool, timeout } } - pub const fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } - #[instrument(skip_all, name = "MOVE-SCU")] pub async fn invoke(&self, request: CompositeMoveRequest) -> Result<(), MoveError> { let association = self diff --git a/src/backend/dimse/mod.rs b/src/backend/dimse/mod.rs index 3537a89..7973e11 100644 --- a/src/backend/dimse/mod.rs +++ b/src/backend/dimse/mod.rs @@ -1,7 +1,7 @@ //! This module contains the DIMSE backend. //! - QIDO-RS is implemented as a find service class user (C-FIND service). //! - WADO-RS is implemented as a move service class user (C-MOVE service). -//! It depends on a store service class provider that must run in the background. +//! It depends on a store service class provider that must run in the background. //! - STOR-RS is implemented as a store service class user (C-STORE service). //! - MWL-RS is implemented as a find service class user (C-FIND service). //! @@ -30,6 +30,7 @@ use dicom::transfer_syntax::TransferSyntaxRegistry; use dicom::ul::pdu::{PDataValue, PDataValueType}; use dicom::ul::Pdu; use std::fmt::{Debug, Formatter}; +use std::future::Future; use std::sync::atomic::{AtomicU16, Ordering}; use std::time::Duration; use thiserror::Error; @@ -37,7 +38,7 @@ use tracing::{instrument, trace}; /// Should be set for [`tags::COMMAND_DATA_SET_TYPE`] if a DICOM message contains a data set. /// This is the recommended value when creating new [`InMemDicomObject`]s for compatibility reasons. -/// For reading DICOM messages, prefer checking if (command_data_set_type != DATA_SET_MISSING) as +/// For reading DICOM messages, prefer checking if (`command_data_set_type != DATA_SET_MISSING`) as /// AEs are free to choose another value for a truthy state. pub const DATA_SET_EXISTS: US = 0x0102; /// Should be set for [`tags::COMMAND_DATA_SET_TYPE`] if a DICOM message has no data set. @@ -66,6 +67,8 @@ impl Debug for DicomMessage { impl DicomMessage { /// Dumps the command set and data set (if present) of this DICOM message to stdout. + #[allow(unused)] + #[cfg(test)] pub fn dump(&self) -> Result<(), std::io::Error> { dicom::dump::dump_object(&self.command)?; if let Some(data) = &self.data { @@ -103,28 +106,32 @@ impl TryFrom for StatusType { } pub trait DicomMessageReader { - async fn read_message(&self, timeout: Duration) -> Result; + fn read_message( + &self, + timeout: Duration, + ) -> impl Future> + Send; } pub trait DicomMessageWriter { - async fn write_message( + fn write_message( &self, - message: impl Into, + message: impl Into + Send, presentation_context_id: Option, timeout: Duration, - ) -> Result<(), WriteError>; + ) -> impl Future> + Send; } -impl DicomMessageWriter for A { +impl DicomMessageWriter for A { #[instrument(skip_all)] async fn write_message( &self, - message: impl Into, + message: impl Into + Send, presentation_context_id: Option, timeout: Duration, ) -> Result<(), WriteError> { let message: DicomMessage = Into::into(message); + #[allow(clippy::option_if_let_else)] let presentation_context = match presentation_context_id { None => self.presentation_contexts().first(), Some(presentation_context_id) => self @@ -158,7 +165,7 @@ impl DicomMessageWriter for A { )) })?; let mut data_buf = Vec::new(); - data.write_dataset_with_ts(&mut data_buf, &transfer_syntax)?; + data.write_dataset_with_ts(&mut data_buf, transfer_syntax)?; let data_pdu = Pdu::PData { data: vec![PDataValue { @@ -212,7 +219,7 @@ pub enum NegotiationError { NoPresentationContext, } -impl DicomMessageReader for A { +impl DicomMessageReader for A { #[instrument(skip_all)] async fn read_message(&self, timeout: Duration) -> Result { let mut command_fragments = Vec::new(); diff --git a/src/backend/dimse/mwl.rs b/src/backend/dimse/mwl.rs index b421cc4..0e0deb7 100644 --- a/src/backend/dimse/mwl.rs +++ b/src/backend/dimse/mwl.rs @@ -55,7 +55,7 @@ impl MwlService for DimseMwlService { attributes.push((AttributeSelector::from(tag), PrimitiveValue::Empty)); } } - }; + } for (selector, value) in attributes { if let Err(err) = identifier.apply(AttributeOp::new(selector, AttributeAction::Set(value))) diff --git a/src/backend/dimse/qido.rs b/src/backend/dimse/qido.rs index d78c4f1..1f06ef6 100644 --- a/src/backend/dimse/qido.rs +++ b/src/backend/dimse/qido.rs @@ -63,7 +63,7 @@ impl QidoService for DimseQidoService { attributes.push((AttributeSelector::from(tag), PrimitiveValue::Empty)); } } - }; + } attributes.push(( AttributeSelector::from(tags::QUERY_RETRIEVE_LEVEL), diff --git a/src/backend/dimse/stow.rs b/src/backend/dimse/stow.rs index e3995d1..61b9bec 100644 --- a/src/backend/dimse/stow.rs +++ b/src/backend/dimse/stow.rs @@ -9,13 +9,12 @@ use tracing::info; pub struct DimseStowService { storescu: StoreServiceClassUser, - timeout: Duration, } impl DimseStowService { pub const fn new(pool: AssociationPool, timeout: Duration) -> Self { let storescu = StoreServiceClassUser::new(pool, timeout); - Self { storescu, timeout } + Self { storescu } } } @@ -32,7 +31,7 @@ impl StowService for DimseStowService { let response = self.storescu.store(instance).await; match response { - Ok(_) => { + Ok(()) => { info!(sop_instance_uid, "Successfully stored instance"); referenced_sequence.push(InstanceReference { sop_class_uid, diff --git a/src/backend/dimse/wado.rs b/src/backend/dimse/wado.rs index 3089d6a..903e8db 100644 --- a/src/backend/dimse/wado.rs +++ b/src/backend/dimse/wado.rs @@ -1,6 +1,6 @@ use crate::api::wado::{ - InstanceQueryParameters, InstanceResponse, MetadataRequest, RenderedResponse, RenderingRequest, - RequestHeaderFields, RetrieveError, RetrieveInstanceRequest, WadoService, + InstanceResponse, MetadataRequest, RenderedResponse, RenderingRequest, RetrieveError, + RetrieveInstanceRequest, WadoService, }; use crate::backend::dimse::association; use crate::backend::dimse::cmove::movescu::{MoveError, MoveServiceClassUser}; @@ -18,7 +18,6 @@ use async_trait::async_trait; use dicom::core::VR; use dicom::dictionary_std::tags; use dicom::object::{FileDicomObject, InMemDicomObject}; -use dicom_pixeldata::WindowLevel; use futures::stream::BoxStream; use futures::{Stream, StreamExt}; use pin_project::pin_project; @@ -34,7 +33,6 @@ use tracing::{error, trace, warn}; pub struct DimseWadoService { movescu: Arc, mediator: MoveMediator, - timeout: Duration, config: WadoConfig, } @@ -106,8 +104,6 @@ impl WadoService for DimseWadoService { async fn metadata(&self, request: MetadataRequest) -> Result { self.retrieve(RetrieveInstanceRequest { query: request.query, - parameters: InstanceQueryParameters::default(), - headers: RequestHeaderFields::default(), }) .await } @@ -130,7 +126,6 @@ impl DimseWadoService { Self { movescu: Arc::new(movescu), mediator, - timeout, config, } } @@ -276,7 +271,7 @@ impl<'a> DicomMultipartStream<'a> { }) }) .chain(futures::stream::once(async { - Ok("--boundary--".as_bytes().to_owned()) + Ok(Vec::from(b"--boundary--")) })) .boxed(); @@ -294,8 +289,8 @@ impl<'a> DicomMultipartStream<'a> { let mut buffer = Vec::new(); writeln!(buffer, "--boundary\r")?; - writeln!(buffer, "Content-Type: {}\r", "application/dicom")?; - writeln!(buffer, "Content-Length: {}\r", file_length)?; + writeln!(buffer, "Content-Type: application/dicom\r")?; + writeln!(buffer, "Content-Length: {file_length}\r")?; writeln!(buffer, "\r")?; buffer.append(&mut dcm); writeln!(buffer, "\r")?; @@ -304,7 +299,7 @@ impl<'a> DicomMultipartStream<'a> { } } -impl<'a> Stream for DicomMultipartStream<'a> { +impl Stream for DicomMultipartStream<'_> { type Item = Result, MoveError>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { diff --git a/src/config/mod.rs b/src/config/mod.rs index ebb0ca6..d7047ef 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -164,17 +164,13 @@ impl Default for WadoConfig { #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "kebab-case")] +#[derive(Default)] pub enum RetrieveMode { + #[default] Concurrent, Sequential, } -impl Default for RetrieveMode { - fn default() -> Self { - Self::Concurrent - } -} - #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct StowConfig { @@ -203,7 +199,7 @@ impl AppConfig { /// Loads the application configuration from the following sources: /// 1. Defaults (defined in `defaults.toml`) /// 2. `config.toml` in the same folder as the executable binary - /// 3. From environment variables, prefixed with DICOM_RST + /// 3. From environment variables, prefixed with `DICOM_RST` /// # Errors /// Returns a [`config::ConfigError`] if source collection fails. pub fn new() -> Result { @@ -311,20 +307,6 @@ impl DimseServerConfig { } } -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum Backend { - Disabled, - Dimse, - S3, -} - -impl Default for Backend { - fn default() -> Self { - Self::Dimse - } -} - impl Default for DimseServerConfig { fn default() -> Self { Self { diff --git a/src/main.rs b/src/main.rs index e326254..5470cd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::multiple_crate_versions)] + pub(crate) mod api; pub(crate) mod backend; pub(crate) mod config; @@ -13,7 +15,6 @@ use crate::types::AE; use association::pool::AssociationPools; use axum::extract::{DefaultBodyLimit, Request}; use axum::response::Response; -use axum::routing::IntoMakeService; use axum::ServiceExt; use std::net::SocketAddr; use std::time::Duration; @@ -78,7 +79,7 @@ fn init_sentry(config: &AppConfig) -> sentry::ClientInitGuard { if let Some(dsn) = &config.telemetry.sentry { info!(dsn, "Enabled Sentry for tracing and error tracking"); - }; + } guard } @@ -191,8 +192,8 @@ async fn shutdown_signal() { let terminate = std::future::pending(); tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, + () = ctrl_c => {}, + () = terminate => {}, } } diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index 002d030..474a565 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -1,4 +1,4 @@ -use crate::api::wado::{ImageQuality, RenderedRequest, Viewport, Window}; +use crate::api::wado::{ImageQuality, Viewport, Window}; use anyhow::bail; use dicom::dictionary_std::tags; use dicom::object::{DefaultDicomObject, FileDicomObject, InMemDicomObject}; @@ -12,13 +12,6 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use std::sync::Arc; use thiserror::Error; -use tracing::{error, instrument, trace, warn}; - -#[derive(Debug, Error)] -pub enum RenderingError { - #[error(transparent)] - PixelData(#[from] dicom_pixeldata::Error), -} #[derive(Debug, Clone, PartialEq)] pub struct RenderingOptions { @@ -77,7 +70,7 @@ fn decode_single_frame_image( Ok(image) } -/// Renders the instance as an image using the options provided in the [RenderingOptions]. +/// Renders the instance as an image using the options provided in the [`RenderingOptions`]. /// /// This supports the following rendered media types: /// - `image/jpeg` @@ -111,39 +104,6 @@ fn render_single_frame_image( Ok(render_buffer) } -#[instrument(skip_all)] -pub fn render( - dicom_file: &FileDicomObject, - request: &RenderedRequest, -) -> Result { - trace!( - sop_instance_uid = dicom_file.meta().media_storage_sop_instance_uid(), - "Rendering DICOM file" - ); - - let pixel_data = dicom_file.decode_pixel_data()?; - - // Convert the pixel data to an image - #[allow(clippy::option_if_let_else)] - let options = match &request.parameters.window { - Some(windowing) => ConvertOptions::new() - .with_voi_lut(VoiLutOption::Custom(WindowLevel { - center: windowing.center, - width: windowing.width, - })) - .force_8bit(), - None => ConvertOptions::default().force_8bit(), - }; - - let mut image = pixel_data.to_dynamic_image_with_options(0, &options)?; - - if let Some(viewport) = &request.parameters.viewport { - image = apply_viewport(&image, viewport); - } - - Ok(image) -} - /// 1. Crop our image to the source rectangle /// 2. Scale the cropped image to the viewport size /// 3. Center the scaled image on a new canvas of the viewport size @@ -170,7 +130,7 @@ fn apply_viewport(image: &DynamicImage, viewport: &Viewport) -> DynamicImage { canvas } -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum RenderedMediaType { #[default] Jpeg, @@ -188,6 +148,7 @@ impl<'de> Deserialize<'de> for RenderedMediaType { } } +#[allow(unused)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResourceCategory { SingleFrameImage, diff --git a/src/types.rs b/src/types.rs index 13879ca..9c0d882 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter}; pub type UI = String; /// UL (Unsigned Long) value representation. +#[allow(unused)] pub type UL = u32; /// US (Unsigned Short) value representation. @@ -15,32 +16,24 @@ pub type US = u16; pub type AE = String; /// Priority (0000,0700) values for DIMSE operations. -#[derive(Debug, Copy, Clone)] +#[allow(unused)] +#[derive(Debug, Copy, Clone, Default)] pub enum Priority { Low = 0x0002, + #[default] Medium = 0x0000, High = 0x0001, } -impl Default for Priority { - fn default() -> Self { - Self::Medium - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[allow(unused)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] pub enum QueryInformationModel { + #[default] Study, Patient, Worklist, } -impl Default for QueryInformationModel { - fn default() -> Self { - Self::Study - } -} - impl QueryInformationModel { pub const fn as_sop_class(&self) -> &str { match self { diff --git a/src/utils/multipart.rs b/src/utils/multipart.rs index 2db76b6..289382d 100644 --- a/src/utils/multipart.rs +++ b/src/utils/multipart.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use axum::extract::{FromRequest, Request}; use axum::http::header::CONTENT_TYPE; use axum::http::{HeaderValue, StatusCode};