1use dicom_core::header::Header;
2use dicom_core::value::{CastValueError, DataSetSequence};
3use dicom_core::{DicomValue, VR};
4use dicom_object::mem::{InMemDicomObject, InMemElement};
5use dicom_object::{AccessError, DefaultDicomObject};
6use log::warn;
7use std::borrow::Cow;
8use thiserror::Error;
9
10use crate::actions::errors::ActionError;
11use crate::config::Config;
12
13#[derive(Error, Debug, PartialEq)]
14pub enum Error {
15 #[error("Value error: {}", .0.to_lowercase())]
16 ValueError(String),
17
18 #[error("Element error: {}", .0.to_lowercase())]
19 ElementError(String),
20
21 #[error("Anonymization error: {}", .0.to_lowercase())]
22 AnonymizationError(String),
23}
24
25impl From<CastValueError> for Error {
26 fn from(err: CastValueError) -> Self {
27 Error::ValueError(format!("{err}"))
28 }
29}
30
31impl From<AccessError> for Error {
32 fn from(err: AccessError) -> Self {
33 Error::ElementError(format!("{err}"))
34 }
35}
36
37impl From<ActionError> for Error {
38 fn from(err: ActionError) -> Self {
39 Error::AnonymizationError(format!("{err}"))
40 }
41}
42
43pub type Result<T, E = Error> = std::result::Result<T, E>;
44
45pub trait Processor {
46 fn process_element<'a>(
47 &'a self,
48 obj: &DefaultDicomObject,
49 elem: &'a InMemElement,
50 ) -> Result<Option<Cow<'a, InMemElement>>>;
51}
52
53#[derive(Debug, Clone, PartialEq)]
59pub struct DefaultProcessor {
60 config: Config,
61}
62
63impl DefaultProcessor {
64 pub fn new(config: Config) -> Self {
74 Self { config }
75 }
76
77 fn process_sequence<'a>(
94 &self,
95 obj: &DefaultDicomObject,
96 seq_elem: &InMemElement,
97 ) -> Result<Option<Cow<'a, InMemElement>>> {
98 let DicomValue::Sequence(sequence) = seq_elem.value() else {
99 return Ok(Some(Cow::Owned(seq_elem.clone())));
101 };
102
103 let mut new_items: Vec<InMemDicomObject> = Vec::with_capacity(sequence.items().len());
104
105 for item in sequence.items() {
106 let mut new_item = InMemDicomObject::new_empty();
107
108 for elem in item {
109 if let Some(processed_elem) = self.process_element(obj, elem)? {
110 new_item.put(processed_elem.into_owned());
111 }
112 }
113
114 if new_item.iter().count() > 0 {
115 new_items.push(new_item);
116 }
117 }
118
119 match new_items.is_empty() {
120 true => Ok(None),
121 false => Ok(Some(Cow::Owned(InMemElement::new(
122 seq_elem.tag(),
123 VR::SQ,
124 DataSetSequence::from(new_items),
125 )))),
126 }
127 }
128}
129
130impl Processor for DefaultProcessor {
131 fn process_element<'a>(
148 &'a self,
149 obj: &DefaultDicomObject,
150 elem: &'a InMemElement,
151 ) -> Result<Option<Cow<'a, InMemElement>>> {
152 let action = self.config.get_action(&elem.tag());
153 let action_struct = action.get_action_struct();
154 let process_result = action_struct.process(&self.config, obj, elem);
155
156 match process_result {
157 Ok(None) => Ok(None),
158 Ok(Some(processed_elem)) => match processed_elem.vr() {
159 VR::SQ => self.process_sequence(obj, &processed_elem),
160 _ => Ok(Some(Cow::Owned(processed_elem.into_owned()))),
161 },
162 Err(ActionError::InvalidHashDateTag(e)) => {
163 warn!("{}", e);
165 Ok(Some(Cow::Borrowed(elem)))
166 }
167 Err(e) => Err(Error::from(e)),
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq)]
173struct NoopProcessor;
174
175impl NoopProcessor {
176 fn new() -> Self {
177 Self {}
178 }
179}
180
181impl Default for NoopProcessor {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl Processor for NoopProcessor {
188 fn process_element<'a>(
189 &'a self,
190 _obj: &DefaultDicomObject,
191 elem: &'a InMemElement,
192 ) -> Result<Option<Cow<'a, InMemElement>>> {
193 Ok(Some(Cow::Borrowed(elem)))
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 use dicom_core::header::HasLength;
203 use dicom_core::value::DataSetSequence;
204 use dicom_core::value::Value;
205 use dicom_core::Tag;
206 use dicom_core::{header, PrimitiveValue, VR};
207 use dicom_object::FileDicomObject;
208
209 use crate::actions::Action;
210 use crate::config::builder::ConfigBuilder;
211 use crate::tags;
212 use crate::test_utils::make_file_meta;
213
214 #[test]
215 fn test_process_element_hash_length() {
216 let meta = make_file_meta();
217 let mut obj = FileDicomObject::new_empty_with_meta(meta);
218
219 obj.put(InMemElement::new(
220 tags::ACCESSION_NUMBER,
221 VR::SH,
222 Value::from("0123456789ABCDEF"),
223 ));
224
225 let config = ConfigBuilder::new()
226 .tag_action(tags::ACCESSION_NUMBER, Action::Hash { length: None })
227 .build();
228
229 let elem = obj.element(tags::ACCESSION_NUMBER).unwrap();
230 let processor = DefaultProcessor::new(config);
231 let processed = processor.process_element(&obj, elem).unwrap();
232 assert_eq!(processed.unwrap().value().length(), header::Length(16));
233 }
234
235 #[test]
236 fn test_process_element_hash_max_length() {
237 let meta = make_file_meta();
238 let mut obj = FileDicomObject::new_empty_with_meta(meta);
239
240 obj.put(InMemElement::new(
241 tags::ACCESSION_NUMBER,
242 VR::SH,
243 Value::from("0123456789ABCDEF"),
244 ));
245
246 let config = ConfigBuilder::new()
247 .tag_action(tags::ACCESSION_NUMBER, Action::Hash { length: Some(32) })
248 .build();
249
250 let elem = obj.element(tags::ACCESSION_NUMBER).unwrap();
251 let processor = DefaultProcessor::new(config);
252 let processed = processor.process_element(&obj, elem).unwrap();
253 assert_eq!(processed.unwrap().value().length(), header::Length(16));
255 }
256
257 #[test]
258 fn test_process_element_hash_length_with_value() {
259 let meta = make_file_meta();
260 let mut obj = FileDicomObject::new_empty_with_meta(meta);
261
262 obj.put(InMemElement::new(
263 tags::ACCESSION_NUMBER,
264 VR::SH,
265 Value::from("0123456789ABCDEF"),
266 ));
267
268 let config = ConfigBuilder::new()
269 .tag_action(tags::ACCESSION_NUMBER, Action::Hash { length: Some(8) })
270 .build();
271
272 let elem = obj.element(tags::ACCESSION_NUMBER).unwrap();
273 let processor = DefaultProcessor::new(config);
274 let processed = processor.process_element(&obj, elem).unwrap();
275 assert_eq!(processed.unwrap().value().length(), header::Length(8));
276 }
277
278 #[test]
279 fn test_process_element_hash_date_invalid_hash_date_tag_error() {
280 let meta = make_file_meta();
281 let mut obj = FileDicomObject::new_empty_with_meta(meta);
282
283 obj.put(InMemElement::new(
284 tags::STUDY_DATE,
285 VR::DA,
286 Value::from("20010102"),
287 ));
288
289 let config = ConfigBuilder::new()
290 .tag_action(tags::STUDY_DATE, Action::HashDate)
291 .build();
292
293 let elem = obj.element(tags::STUDY_DATE).unwrap();
294 let processor = DefaultProcessor::new(config);
295 let processed = processor.process_element(&obj, elem).unwrap();
296
297 assert_eq!(&processed.unwrap().into_owned(), elem);
299 }
300
301 #[test]
302 fn test_process_element_replace() {
303 let meta = make_file_meta();
304 let mut obj = FileDicomObject::new_empty_with_meta(meta);
305
306 obj.put(InMemElement::new(
307 tags::PATIENT_NAME,
308 VR::PN,
309 Value::from("John Doe"),
310 ));
311
312 let config = ConfigBuilder::new()
313 .tag_action(
314 tags::PATIENT_NAME,
315 Action::Replace {
316 value: "Jane Doe".into(),
317 },
318 )
319 .build();
320
321 let elem = obj.element(tags::PATIENT_NAME).unwrap();
322 let processor = DefaultProcessor::new(config);
323 let processed = processor.process_element(&obj, elem).unwrap();
324 assert_eq!(processed.unwrap().value(), &Value::from("Jane Doe"));
325 }
326
327 #[test]
328 fn test_process_element_keep() {
329 let meta = make_file_meta();
330 let mut obj = FileDicomObject::new_empty_with_meta(meta);
331
332 obj.put(InMemElement::new(
333 tags::PATIENT_NAME,
334 VR::PN,
335 Value::from("John Doe"),
336 ));
337
338 let config = ConfigBuilder::new()
339 .tag_action(tags::PATIENT_NAME, Action::Keep)
340 .build();
341
342 let elem = obj.element(tags::PATIENT_NAME).unwrap();
343 let processor = DefaultProcessor::new(config);
344 let processed = processor.process_element(&obj, elem).unwrap();
345 assert_eq!(&processed.unwrap().into_owned(), elem);
346 }
347
348 #[test]
349 fn test_process_element_empty() {
350 let meta = make_file_meta();
351 let mut obj = FileDicomObject::new_empty_with_meta(meta);
352
353 obj.put(InMemElement::new(
354 tags::PATIENT_NAME,
355 VR::PN,
356 Value::from("John Doe"),
357 ));
358
359 let config = ConfigBuilder::new()
360 .tag_action(tags::PATIENT_NAME, Action::Empty)
361 .build();
362
363 let elem = obj.element(tags::PATIENT_NAME).unwrap();
364 let processor = DefaultProcessor::new(config);
365 let processed = processor.process_element(&obj, elem).unwrap();
366 assert_eq!(
367 processed.unwrap().value(),
368 &Value::Primitive(PrimitiveValue::Empty)
369 );
370 }
371
372 #[test]
373 fn test_process_element_remove() {
374 let meta = make_file_meta();
375 let mut obj = FileDicomObject::new_empty_with_meta(meta);
376
377 obj.put(InMemElement::new(
378 tags::PATIENT_NAME,
379 VR::PN,
380 Value::from("John Doe"),
381 ));
382
383 let config = ConfigBuilder::new()
384 .tag_action(tags::PATIENT_NAME, Action::Remove)
385 .build();
386
387 let elem = obj.element(tags::PATIENT_NAME).unwrap();
388 let processor = DefaultProcessor::new(config);
389 let processed = processor.process_element(&obj, elem).unwrap();
390 assert_eq!(processed, None);
391 }
392
393 #[test]
394 fn test_noop_processor() {
395 let meta = make_file_meta();
396 let mut obj = FileDicomObject::new_empty_with_meta(meta);
397
398 obj.put(InMemElement::new(
399 tags::PATIENT_NAME,
400 VR::PN,
401 Value::from("John Doe"),
402 ));
403
404 let elem = obj.element(tags::PATIENT_NAME).unwrap();
405 let processor = NoopProcessor::new();
406 let processed = processor.process_element(&obj, elem).unwrap();
407 assert_eq!(processed.unwrap().into_owned(), elem.clone());
408 }
409
410 #[test]
411 fn test_process_sequence_replace_action_inside_item() {
412 let meta = make_file_meta();
413 let mut obj = FileDicomObject::new_empty_with_meta(meta);
414
415 let mut item = InMemDicomObject::new_empty();
416 item.put(InMemElement::new(
417 tags::PATIENT_NAME,
418 VR::PN,
419 Value::from("John Doe"),
420 ));
421
422 let seq_tag = Tag(0x0008, 0x1110);
423 let seq_value = Value::Sequence(DataSetSequence::from(vec![item]));
424 obj.put(InMemElement::new(seq_tag, VR::SQ, seq_value));
425
426 let config = ConfigBuilder::new()
427 .tag_action(
428 tags::PATIENT_NAME,
429 Action::Replace {
430 value: "Jane Doe".into(),
431 },
432 )
433 .build();
434
435 let elem = obj.element(seq_tag).unwrap();
436 let processor = DefaultProcessor::new(config);
437 let processed = processor
438 .process_element(&obj, elem)
439 .unwrap()
440 .unwrap()
441 .into_owned();
442
443 if let Value::Sequence(seq) = processed.value() {
444 let pn_elem = seq.items()[0].element(tags::PATIENT_NAME).unwrap();
445 assert_eq!(pn_elem.value(), &Value::from("Jane Doe"));
446 } else {
447 panic!("Expected a sequence element");
448 }
449 }
450
451 #[test]
452 fn test_process_sequence_remove_whole_sequence_if_no_items_left() {
453 let meta = make_file_meta();
454 let mut obj = FileDicomObject::new_empty_with_meta(meta);
455
456 let mut item = InMemDicomObject::new_empty();
457 item.put(InMemElement::new(
458 tags::PATIENT_NAME,
459 VR::PN,
460 Value::from("John Doe"),
461 ));
462
463 let seq_tag = Tag(0x0008, 0x1110);
464 let seq_value = Value::Sequence(DataSetSequence::from(vec![item]));
465 obj.put(InMemElement::new(seq_tag, VR::SQ, seq_value));
466
467 let config = ConfigBuilder::new()
468 .tag_action(tags::PATIENT_NAME, Action::Remove)
469 .build();
470
471 let elem = obj.element(seq_tag).unwrap();
472 let processor = DefaultProcessor::new(config);
473 let processed = processor.process_element(&obj, elem).unwrap();
474
475 assert!(processed.is_none(), "Sequence should have been removed");
476 }
477}