Skip to main content

dicom_object/
meta.rs

1//! Module containing data structures and readers of DICOM file meta information tables.
2use byteordered::byteorder::{ByteOrder, LittleEndian};
3use dicom_core::dicom_value;
4use dicom_core::header::{DataElement, EmptyObject, HasLength, Header};
5use dicom_core::ops::{
6    ApplyOp, AttributeAction, AttributeOp, AttributeSelector, AttributeSelectorStep,
7};
8use dicom_core::value::{
9    ConvertValueError, DicomValueType, InMemFragment, PrimitiveValue, Value, ValueType,
10};
11use dicom_core::{Length, Tag, VR};
12use dicom_dictionary_std::tags;
13use dicom_encoding::TransferSyntax;
14use dicom_encoding::decode::{self, DecodeFrom};
15use dicom_encoding::encode::EncoderFor;
16use dicom_encoding::encode::explicit_le::ExplicitVRLittleEndianEncoder;
17use dicom_encoding::text::{self, TextCodec};
18use dicom_parser::dataset::{DataSetWriter, IntoTokens};
19use snafu::{Backtrace, OptionExt, ResultExt, Snafu, ensure};
20use std::borrow::Cow;
21use std::io::{Read, Write};
22
23use crate::ops::{
24    ApplyError, ApplyResult, IllegalExtendSnafu, IncompatibleTypesSnafu, MandatorySnafu,
25    UnsupportedActionSnafu, UnsupportedAttributeSnafu,
26};
27use crate::{
28    AtAccessError, AttributeError, DicomAttribute, DicomObject, IMPLEMENTATION_CLASS_UID,
29    IMPLEMENTATION_VERSION_NAME,
30};
31
32const DICM_MAGIC_CODE: [u8; 4] = [b'D', b'I', b'C', b'M'];
33
34#[derive(Debug, Snafu)]
35#[non_exhaustive]
36pub enum Error {
37    /// The file meta group parser could not read
38    /// the magic code `DICM` from its source.
39    #[snafu(display("Could not start reading DICOM data"))]
40    ReadMagicCode {
41        backtrace: Backtrace,
42        source: std::io::Error,
43    },
44
45    /// The file meta group parser could not fetch
46    /// the value of a data element from its source.
47    #[snafu(display("Could not read data value"))]
48    ReadValueData {
49        backtrace: Backtrace,
50        source: std::io::Error,
51    },
52
53    /// The parser could not allocate memory for the
54    /// given length of a data element.
55    #[snafu(display("Could not allocate memory"))]
56    AllocationSize {
57        backtrace: Backtrace,
58        source: std::collections::TryReserveError,
59    },
60
61    /// The file meta group parser could not decode
62    /// the text in one of its data elements.
63    #[snafu(display("Could not decode text in {}", name))]
64    DecodeText {
65        name: std::borrow::Cow<'static, str>,
66        #[snafu(backtrace)]
67        source: dicom_encoding::text::DecodeTextError,
68    },
69
70    /// Invalid DICOM data, detected by checking the `DICM` code.
71    #[snafu(display("Invalid DICOM file (magic code check failed)"))]
72    NotDicom { backtrace: Backtrace },
73
74    /// An issue occurred while decoding the next data element
75    /// in the file meta data set.
76    #[snafu(display("Could not decode data element"))]
77    DecodeElement {
78        #[snafu(backtrace)]
79        source: dicom_encoding::decode::Error,
80    },
81
82    /// A data element with an unexpected tag was retrieved:
83    /// the parser was expecting another tag first,
84    /// or at least one that is part of the the file meta group.
85    #[snafu(display("Unexpected data element tagged {}", tag))]
86    UnexpectedTag { tag: Tag, backtrace: Backtrace },
87
88    /// A required file meta data element is missing.
89    #[snafu(display("Missing data element `{}`", alias))]
90    MissingElement {
91        alias: &'static str,
92        backtrace: Backtrace,
93    },
94
95    /// The value length of a data elements in the file meta group
96    /// was unexpected.
97    #[snafu(display("Unexpected length {} for data element tagged {}", length, tag))]
98    UnexpectedDataValueLength {
99        tag: Tag,
100        length: Length,
101        backtrace: Backtrace,
102    },
103
104    /// The value length of a data element is undefined,
105    /// but knowing the length is required in its context.
106    #[snafu(display("Undefined value length for data element tagged {}", tag))]
107    UndefinedValueLength { tag: Tag, backtrace: Backtrace },
108
109    /// The file meta group data set could not be written.
110    #[snafu(display("Could not write file meta group data set"))]
111    WriteSet {
112        #[snafu(backtrace)]
113        source: dicom_parser::dataset::write::Error,
114    },
115}
116
117type Result<T, E = Error> = std::result::Result<T, E>;
118
119/// DICOM File Meta Information Table.
120///
121/// This data type contains the relevant parts of the file meta information table,
122/// as specified in [part 6, chapter 7][1] of the standard.
123///
124/// Creating a new file meta table from scratch
125/// is more easily done using a [`FileMetaTableBuilder`].
126/// When modifying the struct's public fields,
127/// it is possible to update the information group length
128/// through method [`update_information_group_length`][2].
129///
130/// [1]: http://dicom.nema.org/medical/dicom/current/output/chtml/part06/chapter_7.html
131/// [2]: FileMetaTable::update_information_group_length
132#[derive(Debug, Clone)]
133pub struct FileMetaTable {
134    /// File Meta Information Group Length
135    pub information_group_length: u32,
136    /// File Meta Information Version
137    pub information_version: [u8; 2],
138    /// Media Storage SOP Class UID
139    pub media_storage_sop_class_uid: String,
140    /// Media Storage SOP Instance UID
141    pub media_storage_sop_instance_uid: String,
142    /// Transfer Syntax UID
143    pub transfer_syntax: String,
144    /// Implementation Class UID
145    pub implementation_class_uid: String,
146
147    /// Implementation Version Name
148    pub implementation_version_name: Option<String>,
149    /// Source Application Entity Title
150    pub source_application_entity_title: Option<String>,
151    /// Sending Application Entity Title
152    pub sending_application_entity_title: Option<String>,
153    /// Receiving Application Entity Title
154    pub receiving_application_entity_title: Option<String>,
155    /// Private Information Creator UID
156    pub private_information_creator_uid: Option<String>,
157    /// Private Information
158    pub private_information: Option<Vec<u8>>,
159    /*
160    Missing attributes:
161
162    (0002,0026) Source Presentation Address Source​Presentation​Address UR 1
163    (0002,0027) Sending Presentation Address Sending​Presentation​Address UR 1
164    (0002,0028) Receiving Presentation Address Receiving​Presentation​Address UR 1
165    (0002,0031) RTV Meta Information Version RTV​Meta​Information​Version OB 1
166    (0002,0032) RTV Communication SOP Class UID RTV​Communication​SOP​Class​UID UI 1
167    (0002,0033) RTV Communication SOP Instance UID RTV​Communication​SOP​Instance​UID UI 1
168    (0002,0035) RTV Source Identifier RTV​Source​Identifier OB 1
169    (0002,0036) RTV Flow Identifier RTV​Flow​Identifier OB 1
170    (0002,0037) RTV Flow RTP Sampling Rate RTV​Flow​RTP​Sampling​Rate UL 1
171    (0002,0038) RTV Flow Actual Frame Duration RTV​Flow​Actual​Frame​Duration FD 1
172    */
173}
174
175/// This comparison ignores trailing white space in strings and binary vectors,
176/// so that any padding to even length does not affect the result.
177/// For instance, a table with the transfer syntax UID `"1.2.840.10008.1.2.​4.​110"`
178/// can be equal to a table with the transfer syntax `"1.2.840.10008.1.2.​4.​110\0"`,
179/// as long as the remaining fields are semantically equal.
180impl std::cmp::PartialEq for FileMetaTable {
181    fn eq(&self, other: &Self) -> bool {
182        self.information_group_length == other.information_group_length
183            && self.information_version == other.information_version
184            && self.media_storage_sop_class_uid() == other.media_storage_sop_class_uid()
185            && self.media_storage_sop_instance_uid() == other.media_storage_sop_instance_uid()
186            && self.transfer_syntax() == other.transfer_syntax()
187            && self.implementation_class_uid() == other.implementation_class_uid()
188            && self.implementation_version_name() == other.implementation_version_name()
189            && self.source_application_entity_title() == other.source_application_entity_title()
190            && self.sending_application_entity_title() == other.sending_application_entity_title()
191            && self.receiving_application_entity_title()
192                == other.receiving_application_entity_title()
193            && self.private_information_creator_uid() == other.private_information_creator_uid()
194            && match (&self.private_information, &other.private_information) {
195                (None, None) => true,
196                (Some(private_info_1), Some(private_info_2)) => {
197                    bytes_eq_without_trailing_byte(private_info_1, private_info_2)
198                }
199                (Some(_), None) | (None, Some(_)) => false,
200            }
201    }
202}
203
204/// Given two byte slices check whether they are equal
205/// but without considering the padding to even length.
206fn bytes_eq_without_trailing_byte(v1: &[u8], v2: &[u8]) -> bool {
207    let v1 = if v1.len() % 2 == 0 && v1.last().copied() == Some(0) {
208        &v1[..v1.len() - 1]
209    } else {
210        v1
211    };
212    let v2 = if v2.len() % 2 == 0 && v2.last().copied() == Some(0) {
213        &v2[..v2.len() - 1]
214    } else {
215        v2
216    };
217
218    v1 == v2
219}
220
221/// Utility function for reading the body of the DICOM element as a UID.
222fn read_str_body<'s, S, T>(source: &'s mut S, text: &T, len: u32) -> Result<String>
223where
224    S: Read + 's,
225    T: TextCodec,
226{
227    let mut v = Vec::new();
228    v.try_reserve_exact(len as usize)
229        .context(AllocationSizeSnafu)?;
230    v.resize(len as usize, 0);
231    source.read_exact(&mut v).context(ReadValueDataSnafu)?;
232
233    text.decode(&v)
234        .context(DecodeTextSnafu { name: text.name() })
235}
236
237impl FileMetaTable {
238    /// Construct a file meta group table
239    /// by parsing a DICOM data set from a reader.
240    ///
241    /// This method fails if the first four bytes
242    /// are not the DICOM magic code `DICM`.
243    pub fn from_reader<R: Read>(file: R) -> Result<Self> {
244        FileMetaTable::read_from(file)
245    }
246
247    /// Getter for the transfer syntax UID,
248    /// with trailing characters already excluded.
249    pub fn transfer_syntax(&self) -> &str {
250        self.transfer_syntax
251            .trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
252    }
253
254    /// Getter for the media storage SOP instance UID,
255    /// with trailing characters already excluded.
256    pub fn media_storage_sop_instance_uid(&self) -> &str {
257        self.media_storage_sop_instance_uid
258            .trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
259    }
260
261    /// Getter for the media storage SOP class UID,
262    /// with trailing characters already excluded.
263    pub fn media_storage_sop_class_uid(&self) -> &str {
264        self.media_storage_sop_class_uid
265            .trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
266    }
267
268    /// Getter for the implementation class UID,
269    /// with trailing characters already excluded.
270    pub fn implementation_class_uid(&self) -> &str {
271        self.implementation_class_uid
272            .trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
273    }
274
275    /// Getter for the implementation version name,
276    /// with trailing characters already excluded.
277    pub fn implementation_version_name(&self) -> Option<&str> {
278        self.implementation_version_name
279            .as_ref()
280            .map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
281    }
282
283    /// Getter for the source application entity title,
284    /// with trailing characters already excluded.
285    pub fn source_application_entity_title(&self) -> Option<&str> {
286        self.source_application_entity_title
287            .as_ref()
288            .map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
289    }
290
291    /// Getter for the sending application entity title,
292    /// with trailing characters already excluded.
293    pub fn sending_application_entity_title(&self) -> Option<&str> {
294        self.sending_application_entity_title
295            .as_ref()
296            .map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
297    }
298
299    /// Getter for the receiving application entity title,
300    /// with trailing characters already excluded.
301    pub fn receiving_application_entity_title(&self) -> Option<&str> {
302        self.receiving_application_entity_title
303            .as_ref()
304            .map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
305    }
306
307    /// Getter for the private information creator UID,
308    /// with trailing characters already excluded.
309    pub fn private_information_creator_uid(&self) -> Option<&str> {
310        self.private_information_creator_uid
311            .as_ref()
312            .map(|s| s.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
313    }
314
315    /// Set the file meta table's transfer syntax
316    /// according to the given transfer syntax descriptor.
317    ///
318    /// This replaces the table's transfer syntax UID
319    /// to the given transfer syntax, without padding to even length.
320    /// The information group length field is automatically recalculated.
321    pub fn set_transfer_syntax<D, R, W>(&mut self, ts: &TransferSyntax<D, R, W>) {
322        self.transfer_syntax = ts
323            .uid()
324            .trim_end_matches(|c: char| c.is_whitespace() || c == '\0')
325            .to_string();
326        self.update_information_group_length();
327    }
328
329    /// Calculate the expected file meta group length
330    /// according to the file meta attributes currently set,
331    /// and assign it to the field `information_group_length`.
332    pub fn update_information_group_length(&mut self) {
333        self.information_group_length = self.calculate_information_group_length();
334    }
335
336    /// Apply the given attribute operation on this file meta information table.
337    ///
338    /// See the [`dicom_core::ops`] module
339    /// for more information.
340    fn apply(&mut self, op: AttributeOp) -> ApplyResult {
341        let AttributeSelectorStep::Tag(tag) = op.selector.first_step() else {
342            return UnsupportedAttributeSnafu.fail();
343        };
344
345        match *tag {
346            tags::TRANSFER_SYNTAX_UID => Self::apply_required_string(op, &mut self.transfer_syntax),
347            tags::MEDIA_STORAGE_SOP_CLASS_UID => {
348                Self::apply_required_string(op, &mut self.media_storage_sop_class_uid)
349            }
350            tags::MEDIA_STORAGE_SOP_INSTANCE_UID => {
351                Self::apply_required_string(op, &mut self.media_storage_sop_instance_uid)
352            }
353            tags::IMPLEMENTATION_CLASS_UID => {
354                Self::apply_required_string(op, &mut self.implementation_class_uid)
355            }
356            tags::IMPLEMENTATION_VERSION_NAME => {
357                Self::apply_optional_string(op, &mut self.implementation_version_name)
358            }
359            tags::SOURCE_APPLICATION_ENTITY_TITLE => {
360                Self::apply_optional_string(op, &mut self.source_application_entity_title)
361            }
362            tags::SENDING_APPLICATION_ENTITY_TITLE => {
363                Self::apply_optional_string(op, &mut self.sending_application_entity_title)
364            }
365            tags::RECEIVING_APPLICATION_ENTITY_TITLE => {
366                Self::apply_optional_string(op, &mut self.receiving_application_entity_title)
367            }
368            tags::PRIVATE_INFORMATION_CREATOR_UID => {
369                Self::apply_optional_string(op, &mut self.private_information_creator_uid)
370            }
371            _ if matches!(
372                op.action,
373                AttributeAction::Remove | AttributeAction::Empty | AttributeAction::Truncate(_)
374            ) =>
375            {
376                // any other attribute is not supported
377                // (ignore Remove, Empty, Truncate)
378                Ok(())
379            }
380            _ => UnsupportedAttributeSnafu.fail(),
381        }?;
382
383        self.update_information_group_length();
384
385        Ok(())
386    }
387
388    fn apply_required_string(op: AttributeOp, target_attribute: &mut String) -> ApplyResult {
389        match op.action {
390            AttributeAction::Remove | AttributeAction::Empty => MandatorySnafu.fail(),
391            AttributeAction::SetVr(_) | AttributeAction::Truncate(_) => {
392                // ignore
393                Ok(())
394            }
395            AttributeAction::Set(value) | AttributeAction::Replace(value) => {
396                // require value to be textual
397                if let Ok(value) = value.string() {
398                    *target_attribute = value.to_string();
399                    Ok(())
400                } else {
401                    IncompatibleTypesSnafu {
402                        kind: ValueType::Str,
403                    }
404                    .fail()
405                }
406            }
407            AttributeAction::SetStr(string) | AttributeAction::ReplaceStr(string) => {
408                *target_attribute = string.to_string();
409                Ok(())
410            }
411            AttributeAction::SetIfMissing(_) | AttributeAction::SetStrIfMissing(_) => {
412                // no-op
413                Ok(())
414            }
415            AttributeAction::PushStr(_) => IllegalExtendSnafu.fail(),
416            AttributeAction::PushI32(_)
417            | AttributeAction::PushU32(_)
418            | AttributeAction::PushI16(_)
419            | AttributeAction::PushU16(_)
420            | AttributeAction::PushF32(_)
421            | AttributeAction::PushF64(_) => IncompatibleTypesSnafu {
422                kind: ValueType::Str,
423            }
424            .fail(),
425            _ => UnsupportedActionSnafu.fail(),
426        }
427    }
428
429    fn apply_optional_string(
430        op: AttributeOp,
431        target_attribute: &mut Option<String>,
432    ) -> ApplyResult {
433        match op.action {
434            AttributeAction::Remove => {
435                target_attribute.take();
436                Ok(())
437            }
438            AttributeAction::Empty => {
439                if let Some(s) = target_attribute.as_mut() {
440                    s.clear();
441                }
442                Ok(())
443            }
444            AttributeAction::SetVr(_) => {
445                // ignore
446                Ok(())
447            }
448            AttributeAction::Set(value) => {
449                // require value to be textual
450                if let Ok(value) = value.string() {
451                    *target_attribute = Some(value.to_string());
452                    Ok(())
453                } else {
454                    IncompatibleTypesSnafu {
455                        kind: ValueType::Str,
456                    }
457                    .fail()
458                }
459            }
460            AttributeAction::SetStr(value) => {
461                *target_attribute = Some(value.to_string());
462                Ok(())
463            }
464            AttributeAction::SetIfMissing(value) => {
465                if target_attribute.is_some() {
466                    return Ok(());
467                }
468
469                // require value to be textual
470                if let Ok(value) = value.string() {
471                    *target_attribute = Some(value.to_string());
472                    Ok(())
473                } else {
474                    IncompatibleTypesSnafu {
475                        kind: ValueType::Str,
476                    }
477                    .fail()
478                }
479            }
480            AttributeAction::SetStrIfMissing(value) => {
481                if target_attribute.is_none() {
482                    *target_attribute = Some(value.to_string());
483                }
484                Ok(())
485            }
486            AttributeAction::Replace(value) => {
487                if target_attribute.is_none() {
488                    return Ok(());
489                }
490
491                // require value to be textual
492                if let Ok(value) = value.string() {
493                    *target_attribute = Some(value.to_string());
494                    Ok(())
495                } else {
496                    IncompatibleTypesSnafu {
497                        kind: ValueType::Str,
498                    }
499                    .fail()
500                }
501            }
502            AttributeAction::ReplaceStr(value) => {
503                if target_attribute.is_some() {
504                    *target_attribute = Some(value.to_string());
505                }
506                Ok(())
507            }
508            AttributeAction::PushStr(_) => IllegalExtendSnafu.fail(),
509            AttributeAction::PushI32(_)
510            | AttributeAction::PushU32(_)
511            | AttributeAction::PushI16(_)
512            | AttributeAction::PushU16(_)
513            | AttributeAction::PushF32(_)
514            | AttributeAction::PushF64(_) => IncompatibleTypesSnafu {
515                kind: ValueType::Str,
516            }
517            .fail(),
518            _ => UnsupportedActionSnafu.fail(),
519        }
520    }
521
522    /// Calculate the expected file meta group length,
523    /// ignoring `information_group_length`.
524    fn calculate_information_group_length(&self) -> u32 {
525        // determine the expected meta group size based on the given fields.
526        // attribute FileMetaInformationGroupLength is not included
527        // in the calculations intentionally
528        14 + 8
529            + dicom_len(&self.media_storage_sop_class_uid)
530            + 8
531            + dicom_len(&self.media_storage_sop_instance_uid)
532            + 8
533            + dicom_len(&self.transfer_syntax)
534            + 8
535            + dicom_len(&self.implementation_class_uid)
536            + self
537                .implementation_version_name
538                .as_ref()
539                .map(|s| 8 + dicom_len(s))
540                .unwrap_or(0)
541            + self
542                .source_application_entity_title
543                .as_ref()
544                .map(|s| 8 + dicom_len(s))
545                .unwrap_or(0)
546            + self
547                .sending_application_entity_title
548                .as_ref()
549                .map(|s| 8 + dicom_len(s))
550                .unwrap_or(0)
551            + self
552                .receiving_application_entity_title
553                .as_ref()
554                .map(|s| 8 + dicom_len(s))
555                .unwrap_or(0)
556            + self
557                .private_information_creator_uid
558                .as_ref()
559                .map(|s| 8 + dicom_len(s))
560                .unwrap_or(0)
561            + self
562                .private_information
563                .as_ref()
564                .map(|x| 12 + ((x.len() as u32 + 1) & !1))
565                .unwrap_or(0)
566    }
567
568    /// Read the DICOM magic code (`b"DICM"`)
569    /// and the whole file meta group from the given reader.
570    fn read_from<S: Read>(mut file: S) -> Result<Self> {
571        let mut buff: [u8; 4] = [0; 4];
572        {
573            // check magic code
574            file.read_exact(&mut buff).context(ReadMagicCodeSnafu)?;
575
576            ensure!(buff == DICM_MAGIC_CODE, NotDicomSnafu);
577        }
578
579        let decoder = decode::file_header_decoder();
580        let text = text::DefaultCharacterSetCodec;
581
582        let builder = FileMetaTableBuilder::new();
583
584        let group_length: u32 = {
585            let (elem, _bytes_read) = decoder
586                .decode_header(&mut file)
587                .context(DecodeElementSnafu)?;
588            if elem.tag() != Tag(0x0002, 0x0000) {
589                return UnexpectedTagSnafu { tag: elem.tag() }.fail();
590            }
591            if elem.length() != Length(4) {
592                return UnexpectedDataValueLengthSnafu {
593                    tag: elem.tag(),
594                    length: elem.length(),
595                }
596                .fail();
597            }
598            let mut buff: [u8; 4] = [0; 4];
599            file.read_exact(&mut buff).context(ReadValueDataSnafu)?;
600            LittleEndian::read_u32(&buff)
601        };
602
603        let mut total_bytes_read = 0;
604        let mut builder = builder.group_length(group_length);
605
606        // Fetch optional data elements
607        while total_bytes_read < group_length {
608            let (elem, header_bytes_read) = decoder
609                .decode_header(&mut file)
610                .context(DecodeElementSnafu)?;
611            let elem_len = match elem.length().get() {
612                None => {
613                    return UndefinedValueLengthSnafu { tag: elem.tag() }.fail();
614                }
615                Some(len) => len,
616            };
617            builder = match elem.tag() {
618                Tag(0x0002, 0x0001) => {
619                    // Implementation Version
620                    if elem.length() != Length(2) {
621                        return UnexpectedDataValueLengthSnafu {
622                            tag: elem.tag(),
623                            length: elem.length(),
624                        }
625                        .fail();
626                    }
627                    let mut hbuf = [0u8; 2];
628                    file.read_exact(&mut hbuf[..]).context(ReadValueDataSnafu)?;
629
630                    builder.information_version(hbuf)
631                }
632                // Media Storage SOP Class UID
633                Tag(0x0002, 0x0002) => {
634                    builder.media_storage_sop_class_uid(read_str_body(&mut file, &text, elem_len)?)
635                }
636                // Media Storage SOP Instance UID
637                Tag(0x0002, 0x0003) => builder
638                    .media_storage_sop_instance_uid(read_str_body(&mut file, &text, elem_len)?),
639                // Transfer Syntax
640                Tag(0x0002, 0x0010) => {
641                    builder.transfer_syntax(read_str_body(&mut file, &text, elem_len)?)
642                }
643                // Implementation Class UID
644                Tag(0x0002, 0x0012) => {
645                    builder.implementation_class_uid(read_str_body(&mut file, &text, elem_len)?)
646                }
647                Tag(0x0002, 0x0013) => {
648                    // Implementation Version Name
649                    let mut v = Vec::new();
650                    v.try_reserve_exact(elem_len as usize)
651                        .context(AllocationSizeSnafu)?;
652                    v.resize(elem_len as usize, 0);
653                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
654
655                    builder.implementation_version_name(
656                        text.decode(&v)
657                            .context(DecodeTextSnafu { name: text.name() })?,
658                    )
659                }
660                Tag(0x0002, 0x0016) => {
661                    // Source Application Entity Title
662                    let mut v = Vec::new();
663                    v.try_reserve_exact(elem_len as usize)
664                        .context(AllocationSizeSnafu)?;
665                    v.resize(elem_len as usize, 0);
666                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
667
668                    builder.source_application_entity_title(
669                        text.decode(&v)
670                            .context(DecodeTextSnafu { name: text.name() })?,
671                    )
672                }
673                Tag(0x0002, 0x0017) => {
674                    // Sending Application Entity Title
675                    let mut v = Vec::new();
676                    v.try_reserve_exact(elem_len as usize)
677                        .context(AllocationSizeSnafu)?;
678                    v.resize(elem_len as usize, 0);
679                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
680
681                    builder.sending_application_entity_title(
682                        text.decode(&v)
683                            .context(DecodeTextSnafu { name: text.name() })?,
684                    )
685                }
686                Tag(0x0002, 0x0018) => {
687                    // Receiving Application Entity Title
688                    let mut v = Vec::new();
689                    v.try_reserve_exact(elem_len as usize)
690                        .context(AllocationSizeSnafu)?;
691                    v.resize(elem_len as usize, 0);
692                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
693
694                    builder.receiving_application_entity_title(
695                        text.decode(&v)
696                            .context(DecodeTextSnafu { name: text.name() })?,
697                    )
698                }
699                Tag(0x0002, 0x0100) => {
700                    // Private Information Creator UID
701                    let mut v = Vec::new();
702                    v.try_reserve_exact(elem_len as usize)
703                        .context(AllocationSizeSnafu)?;
704                    v.resize(elem_len as usize, 0);
705                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
706
707                    builder.private_information_creator_uid(
708                        text.decode(&v)
709                            .context(DecodeTextSnafu { name: text.name() })?,
710                    )
711                }
712                Tag(0x0002, 0x0102) => {
713                    // Private Information
714                    let mut v = Vec::new();
715                    v.try_reserve_exact(elem_len as usize)
716                        .context(AllocationSizeSnafu)?;
717                    v.resize(elem_len as usize, 0);
718                    file.read_exact(&mut v).context(ReadValueDataSnafu)?;
719
720                    builder.private_information(v)
721                }
722                tag @ Tag(0x0002, _) => {
723                    // unknown tag, do nothing
724                    // could be an unsupported or non-standard attribute
725                    tracing::info!("Unknown tag {}", tag);
726                    // consume value without saving it
727                    let bytes_read =
728                        std::io::copy(&mut (&mut file).take(elem_len as u64), &mut std::io::sink())
729                            .context(ReadValueDataSnafu)?;
730                    if bytes_read != elem_len as u64 {
731                        // reported element length longer than actual stream
732                        return UnexpectedDataValueLengthSnafu {
733                            tag: elem.tag(),
734                            length: elem_len,
735                        }
736                        .fail();
737                    }
738                    builder
739                }
740                tag => {
741                    // unexpected tag from another group! do nothing for now,
742                    // but this could pose an issue up ahead (see #50)
743                    tracing::warn!("Unexpected off-group tag {}", tag);
744                    // consume value without saving it
745                    let bytes_read =
746                        std::io::copy(&mut (&mut file).take(elem_len as u64), &mut std::io::sink())
747                            .context(ReadValueDataSnafu)?;
748                    if bytes_read != elem_len as u64 {
749                        // reported element length longer than actual stream
750                        return UnexpectedDataValueLengthSnafu {
751                            tag: elem.tag(),
752                            length: elem_len,
753                        }
754                        .fail();
755                    }
756                    builder
757                }
758            };
759            total_bytes_read = total_bytes_read
760                .saturating_add(header_bytes_read as u32)
761                .saturating_add(elem_len);
762        }
763
764        builder.build()
765    }
766
767    /// Create an iterator over the defined data elements
768    /// of the file meta group,
769    /// consuming the file meta table.
770    ///
771    /// See [`to_element_iter`](FileMetaTable::to_element_iter)
772    /// for a version which copies the element from the table.
773    pub fn into_element_iter(self) -> impl Iterator<Item = DataElement<EmptyObject, [u8; 0]>> {
774        let mut elems = vec![
775            // file information group length
776            DataElement::new(
777                Tag(0x0002, 0x0000),
778                VR::UL,
779                Value::Primitive(self.information_group_length.into()),
780            ),
781            DataElement::new(
782                Tag(0x0002, 0x0001),
783                VR::OB,
784                Value::Primitive(dicom_value!(
785                    U8,
786                    [self.information_version[0], self.information_version[1]]
787                )),
788            ),
789            DataElement::new(
790                Tag(0x0002, 0x0002),
791                VR::UI,
792                Value::Primitive(self.media_storage_sop_class_uid.into()),
793            ),
794            DataElement::new(
795                Tag(0x0002, 0x0003),
796                VR::UI,
797                Value::Primitive(self.media_storage_sop_instance_uid.into()),
798            ),
799            DataElement::new(
800                Tag(0x0002, 0x0010),
801                VR::UI,
802                Value::Primitive(self.transfer_syntax.into()),
803            ),
804            DataElement::new(
805                Tag(0x0002, 0x0012),
806                VR::UI,
807                Value::Primitive(self.implementation_class_uid.into()),
808            ),
809        ];
810        if let Some(v) = self.implementation_version_name {
811            elems.push(DataElement::new(
812                Tag(0x0002, 0x0013),
813                VR::SH,
814                Value::Primitive(v.into()),
815            ));
816        }
817        if let Some(v) = self.source_application_entity_title {
818            elems.push(DataElement::new(
819                Tag(0x0002, 0x0016),
820                VR::AE,
821                Value::Primitive(v.into()),
822            ));
823        }
824        if let Some(v) = self.sending_application_entity_title {
825            elems.push(DataElement::new(
826                Tag(0x0002, 0x0017),
827                VR::AE,
828                Value::Primitive(v.into()),
829            ));
830        }
831        if let Some(v) = self.receiving_application_entity_title {
832            elems.push(DataElement::new(
833                Tag(0x0002, 0x0018),
834                VR::AE,
835                Value::Primitive(v.into()),
836            ));
837        }
838        if let Some(v) = self.private_information_creator_uid {
839            elems.push(DataElement::new(
840                Tag(0x0002, 0x0100),
841                VR::UI,
842                Value::Primitive(v.into()),
843            ));
844        }
845        if let Some(v) = self.private_information {
846            elems.push(DataElement::new(
847                Tag(0x0002, 0x0102),
848                VR::OB,
849                Value::Primitive(PrimitiveValue::U8(v.into())),
850            ));
851        }
852
853        elems.into_iter()
854    }
855
856    /// Create an iterator of data elements copied from the file meta group.
857    ///
858    /// See [`into_element_iter`](FileMetaTable::into_element_iter)
859    /// for a version which consumes the table.
860    pub fn to_element_iter(&self) -> impl Iterator<Item = DataElement<EmptyObject, [u8; 0]>> + '_ {
861        self.clone().into_element_iter()
862    }
863
864    pub fn write<W: Write>(&self, writer: W) -> Result<()> {
865        let mut dset = DataSetWriter::new(
866            writer,
867            EncoderFor::new(ExplicitVRLittleEndianEncoder::default()),
868        );
869        //There are no sequences in the `FileMetaTable`, so the value of `invalidate_sq_len` is
870        //not important
871        dset.write_sequence(
872            self.clone()
873                .into_element_iter()
874                .flat_map(IntoTokens::into_tokens),
875        )
876        .context(WriteSetSnafu)?;
877
878        dset.flush().context(WriteSetSnafu)
879    }
880}
881
882/// An attribute selector for a file meta information table.
883#[derive(Debug)]
884pub struct FileMetaAttribute<'a> {
885    meta: &'a FileMetaTable,
886    tag_e: u16,
887}
888
889impl HasLength for FileMetaAttribute<'_> {
890    fn length(&self) -> Length {
891        match Tag(0x0002, self.tag_e) {
892            tags::FILE_META_INFORMATION_GROUP_LENGTH => Length(4),
893            tags::MEDIA_STORAGE_SOP_CLASS_UID => {
894                Length(self.meta.media_storage_sop_class_uid.len() as u32)
895            }
896            tags::MEDIA_STORAGE_SOP_INSTANCE_UID => {
897                Length(self.meta.media_storage_sop_instance_uid.len() as u32)
898            }
899            tags::IMPLEMENTATION_CLASS_UID => {
900                Length(self.meta.implementation_class_uid.len() as u32)
901            }
902            tags::IMPLEMENTATION_VERSION_NAME => Length(
903                self.meta
904                    .implementation_version_name
905                    .as_ref()
906                    .map(|s| s.len() as u32)
907                    .unwrap_or(0),
908            ),
909            tags::SOURCE_APPLICATION_ENTITY_TITLE => Length(
910                self.meta
911                    .source_application_entity_title
912                    .as_ref()
913                    .map(|s| s.len() as u32)
914                    .unwrap_or(0),
915            ),
916            tags::SENDING_APPLICATION_ENTITY_TITLE => Length(
917                self.meta
918                    .sending_application_entity_title
919                    .as_ref()
920                    .map(|s| s.len() as u32)
921                    .unwrap_or(0),
922            ),
923            tags::TRANSFER_SYNTAX_UID => Length(self.meta.transfer_syntax.len() as u32),
924            tags::PRIVATE_INFORMATION_CREATOR_UID => Length(
925                self.meta
926                    .private_information_creator_uid
927                    .as_ref()
928                    .map(|s| s.len() as u32)
929                    .unwrap_or(0),
930            ),
931            _ => unreachable!(),
932        }
933    }
934}
935
936impl DicomValueType for FileMetaAttribute<'_> {
937    fn value_type(&self) -> ValueType {
938        match Tag(0x0002, self.tag_e) {
939            tags::MEDIA_STORAGE_SOP_CLASS_UID
940            | tags::MEDIA_STORAGE_SOP_INSTANCE_UID
941            | tags::TRANSFER_SYNTAX_UID
942            | tags::IMPLEMENTATION_CLASS_UID
943            | tags::IMPLEMENTATION_VERSION_NAME
944            | tags::SOURCE_APPLICATION_ENTITY_TITLE
945            | tags::SENDING_APPLICATION_ENTITY_TITLE
946            | tags::RECEIVING_APPLICATION_ENTITY_TITLE
947            | tags::PRIVATE_INFORMATION_CREATOR_UID => ValueType::Str,
948            tags::FILE_META_INFORMATION_GROUP_LENGTH => ValueType::U32,
949            tags::FILE_META_INFORMATION_VERSION => ValueType::U8,
950            tags::PRIVATE_INFORMATION => ValueType::U8,
951            _ => unreachable!(),
952        }
953    }
954
955    fn cardinality(&self) -> usize {
956        match Tag(0x0002, self.tag_e) {
957            tags::MEDIA_STORAGE_SOP_CLASS_UID
958            | tags::MEDIA_STORAGE_SOP_INSTANCE_UID
959            | tags::SOURCE_APPLICATION_ENTITY_TITLE
960            | tags::SENDING_APPLICATION_ENTITY_TITLE
961            | tags::RECEIVING_APPLICATION_ENTITY_TITLE
962            | tags::TRANSFER_SYNTAX_UID
963            | tags::IMPLEMENTATION_CLASS_UID
964            | tags::IMPLEMENTATION_VERSION_NAME
965            | tags::PRIVATE_INFORMATION_CREATOR_UID => 1,
966            tags::FILE_META_INFORMATION_GROUP_LENGTH => 1,
967            tags::PRIVATE_INFORMATION => 1,
968            tags::FILE_META_INFORMATION_VERSION => 2,
969            _ => 1,
970        }
971    }
972}
973
974impl DicomAttribute for FileMetaAttribute<'_> {
975    type Item<'b>
976        = EmptyObject
977    where
978        Self: 'b;
979    type PixelData<'b>
980        = InMemFragment
981    where
982        Self: 'b;
983
984    fn to_primitive_value(&self) -> Result<PrimitiveValue, AttributeError> {
985        Ok(match Tag(0x0002, self.tag_e) {
986            tags::FILE_META_INFORMATION_GROUP_LENGTH => {
987                PrimitiveValue::from(self.meta.information_group_length)
988            }
989            tags::FILE_META_INFORMATION_VERSION => {
990                PrimitiveValue::from(self.meta.information_version)
991            }
992            tags::MEDIA_STORAGE_SOP_CLASS_UID => {
993                PrimitiveValue::from(self.meta.media_storage_sop_class_uid.clone())
994            }
995            tags::MEDIA_STORAGE_SOP_INSTANCE_UID => {
996                PrimitiveValue::from(self.meta.media_storage_sop_instance_uid.clone())
997            }
998            tags::SOURCE_APPLICATION_ENTITY_TITLE => {
999                PrimitiveValue::from(self.meta.source_application_entity_title.clone().unwrap())
1000            }
1001            tags::SENDING_APPLICATION_ENTITY_TITLE => {
1002                PrimitiveValue::from(self.meta.sending_application_entity_title.clone().unwrap())
1003            }
1004            tags::RECEIVING_APPLICATION_ENTITY_TITLE => PrimitiveValue::from(
1005                self.meta
1006                    .receiving_application_entity_title
1007                    .clone()
1008                    .unwrap(),
1009            ),
1010            tags::TRANSFER_SYNTAX_UID => PrimitiveValue::from(self.meta.transfer_syntax.clone()),
1011            tags::IMPLEMENTATION_CLASS_UID => {
1012                PrimitiveValue::from(self.meta.implementation_class_uid.clone())
1013            }
1014            tags::IMPLEMENTATION_VERSION_NAME => {
1015                PrimitiveValue::from(self.meta.implementation_version_name.clone().unwrap())
1016            }
1017            tags::PRIVATE_INFORMATION_CREATOR_UID => {
1018                PrimitiveValue::from(self.meta.private_information_creator_uid.clone().unwrap())
1019            }
1020            tags::PRIVATE_INFORMATION => {
1021                PrimitiveValue::from(self.meta.private_information.clone().unwrap())
1022            }
1023            _ => unreachable!(),
1024        })
1025    }
1026
1027    fn to_str(&self) -> std::result::Result<std::borrow::Cow<'_, str>, AttributeError> {
1028        match Tag(0x0002, self.tag_e) {
1029            tags::FILE_META_INFORMATION_GROUP_LENGTH => {
1030                Ok(self.meta.information_group_length.to_string().into())
1031            }
1032            tags::FILE_META_INFORMATION_VERSION => Ok(format!(
1033                "{:02X}{:02X}",
1034                self.meta.information_version[0], self.meta.information_version[1]
1035            )
1036            .into()),
1037            tags::MEDIA_STORAGE_SOP_CLASS_UID => {
1038                Ok(Cow::Borrowed(self.meta.media_storage_sop_class_uid()))
1039            }
1040            tags::MEDIA_STORAGE_SOP_INSTANCE_UID => {
1041                Ok(Cow::Borrowed(self.meta.media_storage_sop_instance_uid()))
1042            }
1043            tags::TRANSFER_SYNTAX_UID => Ok(Cow::Borrowed(self.meta.transfer_syntax())),
1044            tags::IMPLEMENTATION_CLASS_UID => {
1045                Ok(Cow::Borrowed(self.meta.implementation_class_uid()))
1046            }
1047            tags::IMPLEMENTATION_VERSION_NAME => Ok(self
1048                .meta
1049                .implementation_version_name
1050                .as_deref()
1051                .map(Cow::Borrowed)
1052                .unwrap_or_default()),
1053            tags::SOURCE_APPLICATION_ENTITY_TITLE => Ok(self
1054                .meta
1055                .source_application_entity_title
1056                .as_deref()
1057                .map(Cow::Borrowed)
1058                .unwrap_or_default()),
1059            tags::SENDING_APPLICATION_ENTITY_TITLE => Ok(self
1060                .meta
1061                .sending_application_entity_title
1062                .as_deref()
1063                .map(Cow::Borrowed)
1064                .unwrap_or_default()),
1065            tags::RECEIVING_APPLICATION_ENTITY_TITLE => Ok(self
1066                .meta
1067                .receiving_application_entity_title
1068                .as_deref()
1069                .map(Cow::Borrowed)
1070                .unwrap_or_default()),
1071            tags::PRIVATE_INFORMATION_CREATOR_UID => Ok(self
1072                .meta
1073                .private_information_creator_uid
1074                .as_deref()
1075                .map(|v| {
1076                    Cow::Borrowed(v.trim_end_matches(|c: char| c.is_whitespace() || c == '\0'))
1077                })
1078                .unwrap_or_default()),
1079            tags::PRIVATE_INFORMATION => Err(AttributeError::ConvertValue {
1080                source: ConvertValueError {
1081                    cause: None,
1082                    original: ValueType::U8,
1083                    requested: "str",
1084                },
1085            }),
1086            _ => unreachable!(),
1087        }
1088    }
1089
1090    fn item(&self, _index: u32) -> Result<Self::Item<'_>, AttributeError> {
1091        Err(AttributeError::NotDataSet)
1092    }
1093
1094    fn num_items(&self) -> Option<u32> {
1095        None
1096    }
1097
1098    fn fragment(&self, _index: u32) -> Result<Self::PixelData<'_>, AttributeError> {
1099        Err(AttributeError::NotPixelData)
1100    }
1101
1102    fn num_fragments(&self) -> Option<u32> {
1103        None
1104    }
1105}
1106
1107impl DicomObject for FileMetaTable {
1108    type Attribute<'a>
1109        = FileMetaAttribute<'a>
1110    where
1111        Self: 'a;
1112
1113    type LeafAttribute<'a>
1114        = FileMetaAttribute<'a>
1115    where
1116        Self: 'a;
1117
1118    fn attr_opt(
1119        &self,
1120        tag: Tag,
1121    ) -> std::result::Result<Option<Self::Attribute<'_>>, crate::AccessError> {
1122        // check that the attribute value is in the table,
1123        // then return a suitable `FileMetaAttribute`
1124
1125        if match tag {
1126            // mandatory attributes
1127            tags::FILE_META_INFORMATION_GROUP_LENGTH
1128            | tags::FILE_META_INFORMATION_VERSION
1129            | tags::MEDIA_STORAGE_SOP_CLASS_UID
1130            | tags::MEDIA_STORAGE_SOP_INSTANCE_UID
1131            | tags::TRANSFER_SYNTAX_UID
1132            | tags::IMPLEMENTATION_CLASS_UID
1133            | tags::IMPLEMENTATION_VERSION_NAME => true,
1134            // optional attributes
1135            tags::SOURCE_APPLICATION_ENTITY_TITLE
1136                if self.source_application_entity_title.is_some() =>
1137            {
1138                true
1139            }
1140            tags::SENDING_APPLICATION_ENTITY_TITLE
1141                if self.sending_application_entity_title.is_some() =>
1142            {
1143                true
1144            }
1145            tags::RECEIVING_APPLICATION_ENTITY_TITLE
1146                if self.receiving_application_entity_title.is_some() =>
1147            {
1148                true
1149            }
1150            tags::PRIVATE_INFORMATION_CREATOR_UID
1151                if self.private_information_creator_uid.is_some() =>
1152            {
1153                true
1154            }
1155            tags::PRIVATE_INFORMATION if self.private_information.is_some() => true,
1156            _ => false,
1157        } {
1158            Ok(Some(FileMetaAttribute {
1159                meta: self,
1160                tag_e: tag.element(),
1161            }))
1162        } else {
1163            Ok(None)
1164        }
1165    }
1166
1167    fn attr_by_name_opt<'a>(
1168        &'a self,
1169        name: &str,
1170    ) -> std::result::Result<Option<Self::Attribute<'a>>, crate::AccessByNameError> {
1171        let tag = match name {
1172            "FileMetaInformationGroupLength" => tags::FILE_META_INFORMATION_GROUP_LENGTH,
1173            "FileMetaInformationVersion" => tags::FILE_META_INFORMATION_VERSION,
1174            "MediaStorageSOPClassUID" => tags::MEDIA_STORAGE_SOP_CLASS_UID,
1175            "MediaStorageSOPInstanceUID" => tags::MEDIA_STORAGE_SOP_INSTANCE_UID,
1176            "TransferSyntaxUID" => tags::TRANSFER_SYNTAX_UID,
1177            "ImplementationClassUID" => tags::IMPLEMENTATION_CLASS_UID,
1178            "ImplementationVersionName" => tags::IMPLEMENTATION_VERSION_NAME,
1179            "SourceApplicationEntityTitle" => tags::SOURCE_APPLICATION_ENTITY_TITLE,
1180            "SendingApplicationEntityTitle" => tags::SENDING_APPLICATION_ENTITY_TITLE,
1181            "ReceivingApplicationEntityTitle" => tags::RECEIVING_APPLICATION_ENTITY_TITLE,
1182            "PrivateInformationCreatorUID" => tags::PRIVATE_INFORMATION_CREATOR_UID,
1183            "PrivateInformation" => tags::PRIVATE_INFORMATION,
1184            _ => return Ok(None),
1185        };
1186        self.attr_opt(tag)
1187            .map_err(|_| crate::NoSuchAttributeNameSnafu { name }.build())
1188    }
1189
1190    fn at(
1191        &self,
1192        selector: impl Into<AttributeSelector>,
1193    ) -> Result<Self::LeafAttribute<'_>, crate::AtAccessError> {
1194        let selector: AttributeSelector = selector.into();
1195        match selector.split_first() {
1196            (AttributeSelectorStep::Tag(tag), None) => self
1197                .attr(tag)
1198                .map_err(|_| AtAccessError::MissingLeafElement { selector }),
1199            (_, Some(_)) => crate::NotASequenceSnafu {
1200                selector,
1201                step_index: 0_u32,
1202            }
1203            .fail(),
1204            (_, None) => unreachable!("broken invariant: nested step at end of selector"),
1205        }
1206    }
1207}
1208
1209impl ApplyOp for FileMetaTable {
1210    type Err = ApplyError;
1211
1212    /// Apply the given attribute operation on this file meta information table.
1213    ///
1214    /// See the [`dicom_core::ops`] module
1215    /// for more information.
1216    fn apply(&mut self, op: AttributeOp) -> ApplyResult {
1217        self.apply(op)
1218    }
1219}
1220
1221/// A builder for DICOM meta information tables.
1222#[derive(Debug, Default, Clone)]
1223pub struct FileMetaTableBuilder {
1224    /// File Meta Information Group Length (UL)
1225    information_group_length: Option<u32>,
1226    /// File Meta Information Version (OB)
1227    information_version: Option<[u8; 2]>,
1228    /// Media Storage SOP Class UID (UI)
1229    media_storage_sop_class_uid: Option<String>,
1230    /// Media Storage SOP Instance UID (UI)
1231    media_storage_sop_instance_uid: Option<String>,
1232    /// Transfer Syntax UID (UI)
1233    transfer_syntax: Option<String>,
1234    /// Implementation Class UID (UI)
1235    implementation_class_uid: Option<String>,
1236
1237    /// Implementation Version Name (SH)
1238    implementation_version_name: Option<String>,
1239    /// Source Application Entity Title (AE)
1240    source_application_entity_title: Option<String>,
1241    /// Sending Application Entity Title (AE)
1242    sending_application_entity_title: Option<String>,
1243    /// Receiving Application Entity Title (AE)
1244    receiving_application_entity_title: Option<String>,
1245    /// Private Information Creator UID (UI)
1246    private_information_creator_uid: Option<String>,
1247    /// Private Information (OB)
1248    private_information: Option<Vec<u8>>,
1249}
1250
1251/// Ensure that the string is even lengthed, by adding a trailing character
1252/// if not.
1253#[inline]
1254fn padded<T>(s: T, pad: char) -> String
1255where
1256    T: Into<String>,
1257{
1258    let mut s = s.into();
1259    if s.len() % 2 == 1 {
1260        s.push(pad);
1261    }
1262    s
1263}
1264
1265/// Ensure that the string is even lengthed with trailing '\0's.
1266fn ui_padded<T>(s: T) -> String
1267where
1268    T: Into<String>,
1269{
1270    padded(s, '\0')
1271}
1272
1273/// Ensure that the string is even lengthed with trailing spaces.
1274fn txt_padded<T>(s: T) -> String
1275where
1276    T: Into<String>,
1277{
1278    padded(s, ' ')
1279}
1280
1281impl FileMetaTableBuilder {
1282    /// Create a new, empty builder.
1283    pub fn new() -> FileMetaTableBuilder {
1284        FileMetaTableBuilder::default()
1285    }
1286
1287    /// Define the meta information group length.
1288    pub fn group_length(mut self, value: u32) -> FileMetaTableBuilder {
1289        self.information_group_length = Some(value);
1290        self
1291    }
1292
1293    /// Define the meta information version.
1294    pub fn information_version(mut self, value: [u8; 2]) -> FileMetaTableBuilder {
1295        self.information_version = Some(value);
1296        self
1297    }
1298
1299    /// Define the media storage SOP class UID.
1300    pub fn media_storage_sop_class_uid<T>(mut self, value: T) -> FileMetaTableBuilder
1301    where
1302        T: Into<String>,
1303    {
1304        self.media_storage_sop_class_uid = Some(ui_padded(value));
1305        self
1306    }
1307
1308    /// Define the media storage SOP instance UID.
1309    pub fn media_storage_sop_instance_uid<T>(mut self, value: T) -> FileMetaTableBuilder
1310    where
1311        T: Into<String>,
1312    {
1313        self.media_storage_sop_instance_uid = Some(ui_padded(value));
1314        self
1315    }
1316
1317    /// Define the transfer syntax UID.
1318    pub fn transfer_syntax<T>(mut self, value: T) -> FileMetaTableBuilder
1319    where
1320        T: Into<String>,
1321    {
1322        self.transfer_syntax = Some(ui_padded(value));
1323        self
1324    }
1325
1326    /// Define the implementation class UID.
1327    pub fn implementation_class_uid<T>(mut self, value: T) -> FileMetaTableBuilder
1328    where
1329        T: Into<String>,
1330    {
1331        self.implementation_class_uid = Some(ui_padded(value));
1332        self
1333    }
1334
1335    /// Define the implementation version name.
1336    pub fn implementation_version_name<T>(mut self, value: T) -> FileMetaTableBuilder
1337    where
1338        T: Into<String>,
1339    {
1340        self.implementation_version_name = Some(txt_padded(value));
1341        self
1342    }
1343
1344    /// Define the source application entity title.
1345    pub fn source_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
1346    where
1347        T: Into<String>,
1348    {
1349        self.source_application_entity_title = Some(txt_padded(value));
1350        self
1351    }
1352
1353    /// Define the sending application entity title.
1354    pub fn sending_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
1355    where
1356        T: Into<String>,
1357    {
1358        self.sending_application_entity_title = Some(txt_padded(value));
1359        self
1360    }
1361
1362    /// Define the receiving application entity title.
1363    pub fn receiving_application_entity_title<T>(mut self, value: T) -> FileMetaTableBuilder
1364    where
1365        T: Into<String>,
1366    {
1367        self.receiving_application_entity_title = Some(txt_padded(value));
1368        self
1369    }
1370
1371    /// Define the private information creator UID.
1372    pub fn private_information_creator_uid<T>(mut self, value: T) -> FileMetaTableBuilder
1373    where
1374        T: Into<String>,
1375    {
1376        self.private_information_creator_uid = Some(ui_padded(value));
1377        self
1378    }
1379
1380    /// Define the private information as a vector of bytes.
1381    pub fn private_information<T>(mut self, value: T) -> FileMetaTableBuilder
1382    where
1383        T: Into<Vec<u8>>,
1384    {
1385        self.private_information = Some(value.into());
1386        self
1387    }
1388
1389    /// Build the table.
1390    pub fn build(self) -> Result<FileMetaTable> {
1391        let information_version = self.information_version.unwrap_or(
1392            // Missing information version, will assume (00H, 01H). See #28
1393            [0, 1],
1394        );
1395        let media_storage_sop_class_uid = self.media_storage_sop_class_uid.unwrap_or_else(|| {
1396            tracing::warn!("MediaStorageSOPClassUID is missing. Defaulting to empty string.");
1397            String::default()
1398        });
1399        let media_storage_sop_instance_uid =
1400            self.media_storage_sop_instance_uid.unwrap_or_else(|| {
1401                tracing::warn!(
1402                    "MediaStorageSOPInstanceUID is missing. Defaulting to empty string."
1403                );
1404                String::default()
1405            });
1406        let transfer_syntax = self.transfer_syntax.context(MissingElementSnafu {
1407            alias: "TransferSyntax",
1408        })?;
1409        let mut implementation_version_name = self.implementation_version_name;
1410        let implementation_class_uid = self.implementation_class_uid.unwrap_or_else(|| {
1411            // override implementation version name
1412            implementation_version_name = Some(IMPLEMENTATION_VERSION_NAME.to_string());
1413
1414            IMPLEMENTATION_CLASS_UID.to_string()
1415        });
1416
1417        let mut table = FileMetaTable {
1418            // placeholder value which will be replaced on update
1419            information_group_length: 0x00,
1420            information_version,
1421            media_storage_sop_class_uid,
1422            media_storage_sop_instance_uid,
1423            transfer_syntax,
1424            implementation_class_uid,
1425            implementation_version_name,
1426            source_application_entity_title: self.source_application_entity_title,
1427            sending_application_entity_title: self.sending_application_entity_title,
1428            receiving_application_entity_title: self.receiving_application_entity_title,
1429            private_information_creator_uid: self.private_information_creator_uid,
1430            private_information: self.private_information,
1431        };
1432        table.update_information_group_length();
1433        debug_assert!(table.information_group_length > 0);
1434        Ok(table)
1435    }
1436}
1437
1438fn dicom_len<T: AsRef<str>>(x: T) -> u32 {
1439    (x.as_ref().len() as u32 + 1) & !1
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use crate::{IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME};
1445
1446    use super::{FileMetaTable, FileMetaTableBuilder, dicom_len};
1447    use dicom_core::ops::{AttributeAction, AttributeOp};
1448    use dicom_core::value::Value;
1449    use dicom_core::{DataElement, PrimitiveValue, Tag, VR, dicom_value};
1450    use dicom_dictionary_std::tags;
1451
1452    const TEST_META_1: &[u8] = &[
1453        // magic code
1454        b'D', b'I', b'C', b'M',
1455        // File Meta Information Group Length: (0000,0002) ; UL ; 4 ; 200
1456        0x02, 0x00, 0x00, 0x00, b'U', b'L', 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00,
1457        // File Meta Information Version: (0002, 0001) ; OB ; 2 ; [0x00, 0x01]
1458        0x02, 0x00, 0x01, 0x00, b'O', b'B', 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
1459        // Media Storage SOP Class UID (0002, 0002) ; UI ; 26 ; "1.2.840.10008.5.1.4.1.1.1\0" (ComputedRadiographyImageStorage)
1460        0x02, 0x00, 0x02, 0x00, b'U', b'I', 0x1a, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x38, 0x34, 0x30,
1461        0x2e, 0x31, 0x30, 0x30, 0x30, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e,
1462        0x31, 0x2e, 0x31, 0x00,
1463        // Media Storage SOP Instance UID (0002, 0003) ; UI ; 56 ; "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0"
1464        0x02, 0x00, 0x03, 0x00, b'U', b'I', 0x38, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x2e, 0x34,
1465        0x2e, 0x35, 0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x2e, 0x31, 0x32, 0x33,
1466        0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
1467        0x2e, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2e, 0x31, 0x32, 0x33, 0x34,
1468        0x35, 0x36, 0x37, 0x00,
1469        // Transfer Syntax UID (0002, 0010) ; UI ; 20 ; "1.2.840.10008.1.2.1\0" (LittleEndianExplicit)
1470        0x02, 0x00, 0x10, 0x00, b'U', b'I', 0x14, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x38, 0x34, 0x30,
1471        0x2e, 0x31, 0x30, 0x30, 0x30, 0x38, 0x2e, 0x31, 0x2e, 0x32, 0x2e, 0x31, 0x00,
1472        // Implementation Class UID (0002, 0012) ; UI ; 20 ; "1.2.345.6.7890.1.234"
1473        0x02, 0x00, 0x12, 0x00, b'U', b'I', 0x14, 0x00, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x34, 0x35,
1474        0x2e, 0x36, 0x2e, 0x37, 0x38, 0x39, 0x30, 0x2e, 0x31, 0x2e, 0x32, 0x33, 0x34,
1475        // optional elements:
1476
1477        // Implementation Version Name (0002,0013) ; SH ; "RUSTY_DICOM_269"
1478        0x02, 0x00, 0x13, 0x00, b'S', b'H', 0x10, 0x00, 0x52, 0x55, 0x53, 0x54, 0x59, 0x5f, 0x44,
1479        0x49, 0x43, 0x4f, 0x4d, 0x5f, 0x32, 0x36, 0x39, 0x20,
1480        // Source Application Entity Title (0002, 0016) ; AE ; 0 (no data)
1481        0x02, 0x00, 0x16, 0x00, b'A', b'E', 0x00, 0x00,
1482    ];
1483
1484    #[test]
1485    fn read_meta_table_from_reader() {
1486        let mut source = TEST_META_1;
1487
1488        let table = FileMetaTable::from_reader(&mut source).unwrap();
1489
1490        let gt = FileMetaTable {
1491            information_group_length: 200,
1492            information_version: [0u8, 1u8],
1493            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1494            media_storage_sop_instance_uid:
1495                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1496            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1497            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1498            implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
1499            source_application_entity_title: Some("".to_owned()),
1500            sending_application_entity_title: None,
1501            receiving_application_entity_title: None,
1502            private_information_creator_uid: None,
1503            private_information: None,
1504        };
1505
1506        assert_eq!(table.information_group_length, 200);
1507        assert_eq!(table.information_version, [0u8, 1u8]);
1508        assert_eq!(
1509            table.media_storage_sop_class_uid,
1510            "1.2.840.10008.5.1.4.1.1.1\0"
1511        );
1512        assert_eq!(
1513            table.media_storage_sop_instance_uid,
1514            "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0"
1515        );
1516        assert_eq!(table.transfer_syntax, "1.2.840.10008.1.2.1\0");
1517        assert_eq!(table.implementation_class_uid, "1.2.345.6.7890.1.234");
1518        assert_eq!(
1519            table.implementation_version_name,
1520            Some("RUSTY_DICOM_269 ".to_owned())
1521        );
1522        assert_eq!(table.source_application_entity_title, Some("".into()));
1523        assert_eq!(table.sending_application_entity_title, None);
1524        assert_eq!(table.receiving_application_entity_title, None);
1525        assert_eq!(table.private_information_creator_uid, None);
1526        assert_eq!(table.private_information, None);
1527
1528        assert_eq!(table, gt);
1529    }
1530
1531    #[test]
1532    fn create_meta_table_with_builder() {
1533        let table = FileMetaTableBuilder::new()
1534            .information_version([0, 1])
1535            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
1536            .media_storage_sop_instance_uid(
1537                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
1538            )
1539            .transfer_syntax("1.2.840.10008.1.2.1")
1540            .implementation_class_uid("1.2.345.6.7890.1.234")
1541            .implementation_version_name("RUSTY_DICOM_269")
1542            .source_application_entity_title("")
1543            .build()
1544            .unwrap();
1545
1546        let gt = FileMetaTable {
1547            information_group_length: 200,
1548            information_version: [0u8, 1u8],
1549            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1550            media_storage_sop_instance_uid:
1551                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1552            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1553            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1554            implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
1555            source_application_entity_title: Some("".to_owned()),
1556            sending_application_entity_title: None,
1557            receiving_application_entity_title: None,
1558            private_information_creator_uid: None,
1559            private_information: None,
1560        };
1561
1562        assert_eq!(table.information_group_length, gt.information_group_length);
1563        assert_eq!(table, gt);
1564    }
1565
1566    /// Build a file meta table with the minimum set of parameters.
1567    #[test]
1568    fn create_meta_table_with_builder_minimal() {
1569        let table = FileMetaTableBuilder::new()
1570            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
1571            .media_storage_sop_instance_uid(
1572                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
1573            )
1574            .transfer_syntax("1.2.840.10008.1.2")
1575            .build()
1576            .unwrap();
1577
1578        let gt = FileMetaTable {
1579            information_group_length: 154
1580                + dicom_len(IMPLEMENTATION_CLASS_UID)
1581                + dicom_len(IMPLEMENTATION_VERSION_NAME),
1582            information_version: [0u8, 1u8],
1583            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1584            media_storage_sop_instance_uid:
1585                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1586            transfer_syntax: "1.2.840.10008.1.2\0".to_owned(),
1587            implementation_class_uid: IMPLEMENTATION_CLASS_UID.to_owned(),
1588            implementation_version_name: Some(IMPLEMENTATION_VERSION_NAME.to_owned()),
1589            source_application_entity_title: None,
1590            sending_application_entity_title: None,
1591            receiving_application_entity_title: None,
1592            private_information_creator_uid: None,
1593            private_information: None,
1594        };
1595
1596        assert_eq!(table.information_group_length, gt.information_group_length);
1597        assert_eq!(table, gt);
1598    }
1599
1600    /// Changing the transfer syntax updates the file meta group length.
1601    #[test]
1602    fn change_transfer_syntax_update_table() {
1603        let mut table = FileMetaTableBuilder::new()
1604            .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1")
1605            .media_storage_sop_instance_uid(
1606                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567",
1607            )
1608            .transfer_syntax("1.2.840.10008.1.2.1")
1609            .build()
1610            .unwrap();
1611
1612        assert_eq!(
1613            table.information_group_length,
1614            156 + dicom_len(IMPLEMENTATION_CLASS_UID) + dicom_len(IMPLEMENTATION_VERSION_NAME)
1615        );
1616
1617        table.set_transfer_syntax(
1618            &dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN,
1619        );
1620        assert_eq!(
1621            table.information_group_length,
1622            154 + dicom_len(IMPLEMENTATION_CLASS_UID) + dicom_len(IMPLEMENTATION_VERSION_NAME)
1623        );
1624    }
1625
1626    #[test]
1627    fn read_meta_table_into_iter() {
1628        let table = FileMetaTable {
1629            information_group_length: 200,
1630            information_version: [0u8, 1u8],
1631            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1632            media_storage_sop_instance_uid:
1633                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1634            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1635            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1636            implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
1637            source_application_entity_title: Some("".to_owned()),
1638            sending_application_entity_title: None,
1639            receiving_application_entity_title: None,
1640            private_information_creator_uid: None,
1641            private_information: None,
1642        };
1643
1644        assert_eq!(table.calculate_information_group_length(), 200);
1645
1646        let gt = vec![
1647            // Information Group Length
1648            DataElement::new(Tag(0x0002, 0x0000), VR::UL, dicom_value!(U32, 200)),
1649            // Information Version
1650            DataElement::new(Tag(0x0002, 0x0001), VR::OB, dicom_value!(U8, [0, 1])),
1651            // Media Storage SOP Class UID
1652            DataElement::new(
1653                Tag(0x0002, 0x0002),
1654                VR::UI,
1655                Value::Primitive("1.2.840.10008.5.1.4.1.1.1\0".into()),
1656            ),
1657            // Media Storage SOP Instance UID
1658            DataElement::new(
1659                Tag(0x0002, 0x0003),
1660                VR::UI,
1661                Value::Primitive(
1662                    "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".into(),
1663                ),
1664            ),
1665            // Transfer Syntax
1666            DataElement::new(
1667                Tag(0x0002, 0x0010),
1668                VR::UI,
1669                Value::Primitive("1.2.840.10008.1.2.1\0".into()),
1670            ),
1671            // Implementation Class UID
1672            DataElement::new(
1673                Tag(0x0002, 0x0012),
1674                VR::UI,
1675                Value::Primitive("1.2.345.6.7890.1.234".into()),
1676            ),
1677            // Implementation Version Name
1678            DataElement::new(
1679                Tag(0x0002, 0x0013),
1680                VR::SH,
1681                Value::Primitive("RUSTY_DICOM_269 ".into()),
1682            ),
1683            // Source Application Entity Title
1684            DataElement::new(Tag(0x0002, 0x0016), VR::AE, Value::Primitive("".into())),
1685        ];
1686
1687        let elems: Vec<_> = table.into_element_iter().collect();
1688        assert_eq!(elems, gt);
1689    }
1690
1691    /// Test `PartialEq` impl for `FileMetaTable`
1692    #[test]
1693    fn meta_table_eq() {
1694        let mut table = FileMetaTable {
1695            information_group_length: 200,
1696            information_version: [0u8, 1u8],
1697            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1698            media_storage_sop_instance_uid:
1699                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1700            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1701            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1702            implementation_version_name: Some("RUSTY_DICOM_001".to_owned()),
1703            source_application_entity_title: Some("".to_owned()),
1704            sending_application_entity_title: None,
1705            receiving_application_entity_title: None,
1706            private_information_creator_uid: None,
1707            private_information: None,
1708        };
1709
1710        // it equals itself
1711        assert_eq!(&table, &table);
1712
1713        let mut table2 = table.clone();
1714        table2.implementation_version_name = Some("RUSTY_DICOM_002".to_owned());
1715
1716        // Implementation Version Name mismatch
1717        assert_ne!(&table, &table2);
1718
1719        // it ignores trailing whitespace (padding to even length)
1720        table2.implementation_version_name = Some("RUSTY_DICOM_001 ".to_owned());
1721        assert_eq!(&table, &table2);
1722
1723        // media storage SOP class UID also ignores trailing whitespace
1724        table2.media_storage_sop_class_uid = "1.2.840.10008.5.1.4.1.1.1".to_owned();
1725        assert_eq!(&table, &table2);
1726
1727        // private information creator UID also ignores trailing whitespace
1728        table.private_information_creator_uid = Some("xxx".to_owned());
1729        table2.private_information_creator_uid = Some("xxx\0".to_owned());
1730        assert_eq!(&table, &table2);
1731
1732        // private information ignores single trailing zero
1733        table.private_information = Some(vec![1, 2, 5]);
1734        table2.private_information = Some(vec![1, 2, 5, 0]);
1735        assert_eq!(&table, &table2);
1736    }
1737
1738    #[test]
1739    fn update_table_with_length() {
1740        let mut table = FileMetaTable {
1741            information_group_length: 55, // dummy value
1742            information_version: [0u8, 1u8],
1743            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1744            media_storage_sop_instance_uid:
1745                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1746            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1747            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1748            implementation_version_name: Some("RUSTY_DICOM_269 ".to_owned()),
1749            source_application_entity_title: Some("".to_owned()),
1750            sending_application_entity_title: None,
1751            receiving_application_entity_title: None,
1752            private_information_creator_uid: None,
1753            private_information: None,
1754        };
1755
1756        table.update_information_group_length();
1757
1758        assert_eq!(table.information_group_length, 200);
1759    }
1760
1761    #[test]
1762    fn table_ops() {
1763        let mut table = FileMetaTable {
1764            information_group_length: 200,
1765            information_version: [0u8, 1u8],
1766            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.1\0".to_owned(),
1767            media_storage_sop_instance_uid:
1768                "1.2.3.4.5.12345678.1234567890.1234567.123456789.1234567\0".to_owned(),
1769            transfer_syntax: "1.2.840.10008.1.2.1\0".to_owned(),
1770            implementation_class_uid: "1.2.345.6.7890.1.234".to_owned(),
1771            implementation_version_name: None,
1772            source_application_entity_title: None,
1773            sending_application_entity_title: None,
1774            receiving_application_entity_title: None,
1775            private_information_creator_uid: None,
1776            private_information: None,
1777        };
1778
1779        // replace does not set missing attributes
1780        table
1781            .apply(AttributeOp::new(
1782                tags::IMPLEMENTATION_VERSION_NAME,
1783                AttributeAction::ReplaceStr("MY_DICOM_1.1".into()),
1784            ))
1785            .unwrap();
1786
1787        assert_eq!(table.implementation_version_name, None);
1788
1789        // but SetStr does
1790        table
1791            .apply(AttributeOp::new(
1792                tags::IMPLEMENTATION_VERSION_NAME,
1793                AttributeAction::SetStr("MY_DICOM_1.1".into()),
1794            ))
1795            .unwrap();
1796
1797        assert_eq!(
1798            table.implementation_version_name.as_deref(),
1799            Some("MY_DICOM_1.1"),
1800        );
1801
1802        // Set (primitive) also works
1803        table
1804            .apply(AttributeOp::new(
1805                tags::SOURCE_APPLICATION_ENTITY_TITLE,
1806                AttributeAction::Set(PrimitiveValue::Str("RICOOGLE-STORAGE".into())),
1807            ))
1808            .unwrap();
1809
1810        assert_eq!(
1811            table.source_application_entity_title.as_deref(),
1812            Some("RICOOGLE-STORAGE"),
1813        );
1814
1815        // set if missing works only if value isn't set yet
1816        table
1817            .apply(AttributeOp::new(
1818                tags::SOURCE_APPLICATION_ENTITY_TITLE,
1819                AttributeAction::SetStrIfMissing("STORE-SCU".into()),
1820            ))
1821            .unwrap();
1822
1823        assert_eq!(
1824            table.source_application_entity_title.as_deref(),
1825            Some("RICOOGLE-STORAGE"),
1826        );
1827
1828        table
1829            .apply(AttributeOp::new(
1830                tags::SENDING_APPLICATION_ENTITY_TITLE,
1831                AttributeAction::SetStrIfMissing("STORE-SCU".into()),
1832            ))
1833            .unwrap();
1834
1835        assert_eq!(
1836            table.sending_application_entity_title.as_deref(),
1837            Some("STORE-SCU"),
1838        );
1839
1840        // replacing mandatory field
1841        table
1842            .apply(AttributeOp::new(
1843                tags::MEDIA_STORAGE_SOP_CLASS_UID,
1844                AttributeAction::Replace(PrimitiveValue::Str("1.2.840.10008.5.1.4.1.1.7".into())),
1845            ))
1846            .unwrap();
1847
1848        assert_eq!(
1849            table.media_storage_sop_class_uid(),
1850            "1.2.840.10008.5.1.4.1.1.7",
1851        );
1852    }
1853
1854    /// writing file meta information and reading it back
1855    /// should not fail and the the group length should be the same
1856    #[test]
1857    fn write_read_does_not_fail() {
1858        let mut table = FileMetaTable {
1859            information_group_length: 0,
1860            information_version: [0u8, 1u8],
1861            media_storage_sop_class_uid: "1.2.840.10008.5.1.4.1.1.7".to_owned(),
1862            media_storage_sop_instance_uid: "2.25.137731752600317795446120660167595746868"
1863                .to_owned(),
1864            transfer_syntax: "1.2.840.10008.1.2.4.91".to_owned(),
1865            implementation_class_uid: "2.25.305828488182831875890203105390285383139".to_owned(),
1866            implementation_version_name: Some("MYTOOL100".to_owned()),
1867            source_application_entity_title: Some("RUSTY".to_owned()),
1868            receiving_application_entity_title: None,
1869            sending_application_entity_title: None,
1870            private_information_creator_uid: None,
1871            private_information: None,
1872        };
1873
1874        table.update_information_group_length();
1875
1876        let mut buf = vec![b'D', b'I', b'C', b'M'];
1877        table.write(&mut buf).unwrap();
1878
1879        let table2 = FileMetaTable::from_reader(&mut buf.as_slice())
1880            .expect("Should not fail to read the table from the written data");
1881
1882        assert_eq!(
1883            table.information_group_length,
1884            table2.information_group_length
1885        );
1886    }
1887
1888    /// Can access file meta properties via the DicomObject trait
1889    #[test]
1890    fn dicom_object_api() {
1891        use crate::{DicomAttribute as _, DicomObject as _};
1892        use dicom_dictionary_std::uids;
1893
1894        let meta = FileMetaTableBuilder::new()
1895            .transfer_syntax(uids::RLE_LOSSLESS)
1896            .media_storage_sop_class_uid(uids::ENHANCED_MR_IMAGE_STORAGE)
1897            .media_storage_sop_instance_uid("2.25.94766187067244888884745908966163363746")
1898            .implementation_version_name("RUSTY_DICOM_269")
1899            .build()
1900            .unwrap();
1901
1902        assert_eq!(
1903            meta.attr(tags::TRANSFER_SYNTAX_UID)
1904                .unwrap()
1905                .to_str()
1906                .unwrap(),
1907            uids::RLE_LOSSLESS
1908        );
1909
1910        let sop_class_uid = meta.attr_opt(tags::MEDIA_STORAGE_SOP_CLASS_UID).unwrap();
1911        let sop_class_uid = sop_class_uid.as_ref().map(|v| v.to_str().unwrap());
1912        assert_eq!(
1913            sop_class_uid.as_deref(),
1914            Some(uids::ENHANCED_MR_IMAGE_STORAGE)
1915        );
1916
1917        assert_eq!(
1918            meta.attr_by_name("MediaStorageSOPInstanceUID")
1919                .unwrap()
1920                .to_str()
1921                .unwrap(),
1922            "2.25.94766187067244888884745908966163363746"
1923        );
1924
1925        assert!(meta.attr_opt(tags::PRIVATE_INFORMATION).unwrap().is_none());
1926    }
1927}