dicom_ul/pdu/
writer.rs

1/// PDU writer module
2use crate::pdu::*;
3use byteordered::byteorder::{BigEndian, WriteBytesExt};
4use dicom_encoding::text::TextCodec;
5use snafu::{Backtrace, ResultExt, Snafu};
6use std::io::Write;
7
8#[derive(Debug, Snafu)]
9#[non_exhaustive]
10pub enum Error {
11    #[snafu(display("Could not write chunk of {} PDU structure", name))]
12    WriteChunk {
13        /// the name of the PDU structure
14        name: &'static str,
15        source: WriteChunkError,
16    },
17
18    #[snafu(display("Could not write field `{}`", field))]
19    WriteField {
20        field: &'static str,
21        backtrace: Backtrace,
22        source: std::io::Error,
23    },
24
25    #[snafu(display("Could not write {} reserved bytes", bytes))]
26    WriteReserved {
27        bytes: u32,
28        backtrace: Backtrace,
29        source: std::io::Error,
30    },
31
32    #[snafu(display("Could not write field `{}`", field))]
33    EncodeField {
34        field: &'static str,
35        #[snafu(backtrace)]
36        source: dicom_encoding::text::EncodeTextError,
37    },
38}
39
40pub type Result<T> = std::result::Result<T, Error>;
41
42#[derive(Debug, Snafu)]
43pub enum WriteChunkError {
44    #[snafu(display("Failed to build chunk"))]
45    BuildChunk {
46        #[snafu(backtrace)]
47        source: Box<Error>,
48    },
49    #[snafu(display("Failed to write chunk length"))]
50    WriteLength {
51        backtrace: Backtrace,
52        source: std::io::Error,
53    },
54    #[snafu(display("Failed to write chunk data"))]
55    WriteData {
56        backtrace: Backtrace,
57        source: std::io::Error,
58    },
59}
60
61fn write_chunk_u32<F>(writer: &mut dyn Write, func: F) -> std::result::Result<(), WriteChunkError>
62where
63    F: FnOnce(&mut Vec<u8>) -> Result<()>,
64{
65    let mut data = vec![];
66    func(&mut data)
67        .map_err(Box::from)
68        .context(BuildChunkSnafu)?;
69
70    let length = data.len() as u32;
71    writer
72        .write_u32::<BigEndian>(length)
73        .context(WriteLengthSnafu)?;
74
75    writer.write_all(&data).context(WriteDataSnafu)?;
76
77    Ok(())
78}
79
80fn write_chunk_u16<F>(writer: &mut dyn Write, func: F) -> std::result::Result<(), WriteChunkError>
81where
82    F: FnOnce(&mut Vec<u8>) -> Result<()>,
83{
84    let mut data = vec![];
85    func(&mut data)
86        .map_err(Box::from)
87        .context(BuildChunkSnafu)?;
88
89    let length = data.len() as u16;
90    writer
91        .write_u16::<BigEndian>(length)
92        .context(WriteLengthSnafu)?;
93
94    writer.write_all(&data).context(WriteDataSnafu)?;
95
96    Ok(())
97}
98
99pub fn write_pdu<W>(writer: &mut W, pdu: &Pdu) -> Result<()>
100where
101    W: Write,
102{
103    let codec = dicom_encoding::text::DefaultCharacterSetCodec;
104    match pdu {
105        Pdu::AssociationRQ(AssociationRQ {
106            protocol_version,
107            calling_ae_title,
108            called_ae_title,
109            application_context_name,
110            presentation_contexts,
111            user_variables,
112        }) => {
113            // A-ASSOCIATE-RQ PDU Structure
114
115            // 1 - PDU-type - 01H
116            writer
117                .write_u8(0x01)
118                .context(WriteFieldSnafu { field: "PDU-type" })?;
119
120            // 2 - Reserved - This reserved field shall be sent with a value 00H but not
121            // tested to this value when received.
122            writer
123                .write_u8(0x00)
124                .context(WriteReservedSnafu { bytes: 1_u32 })?;
125
126            write_chunk_u32(writer, |writer| {
127                // 7-8  Protocol-version - This two byte field shall use one bit to identify
128                // each version of the DICOM UL protocol supported by the calling end-system.
129                // This is Version 1 and shall be identified with bit 0 set. A receiver of this
130                // PDU implementing only this version of the DICOM UL protocol shall only test
131                // that bit 0 is set.
132                writer
133                    .write_u16::<BigEndian>(*protocol_version)
134                    .context(WriteFieldSnafu {
135                        field: "Protocol-version",
136                    })?;
137
138                // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but
139                // not tested to this value when received.
140                writer
141                    .write_u16::<BigEndian>(0x00)
142                    .context(WriteReservedSnafu { bytes: 2_u32 })?;
143
144                // 11-26 - Called-AE-title - Destination DICOM Application Name. It shall be
145                // encoded as 16 characters as defined by the ISO 646:1990-Basic G0 Set with
146                // leading and trailing spaces (20H) being non-significant. The value made of 16
147                // spaces (20H) meaning "no Application Name specified" shall not be used. For a
148                // complete description of the use of this field, see Section 7.1.1.4.
149                let mut ae_title_bytes =
150                    codec.encode(called_ae_title).context(EncodeFieldSnafu {
151                        field: "Called-AE-title",
152                    })?;
153                ae_title_bytes.resize(16, b' ');
154                writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
155                    field: "Called-AE-title",
156                })?;
157
158                // 27-42 - Calling-AE-title - Source DICOM Application Name. It shall be encoded
159                // as 16 characters as defined by the ISO 646:1990-Basic G0 Set with leading and
160                // trailing spaces (20H) being non-significant. The value made of 16 spaces
161                // (20H) meaning "no Application Name specified" shall not be used. For a
162                // complete description of the use of this field, see Section 7.1.1.3.
163                let mut ae_title_bytes =
164                    codec.encode(calling_ae_title).context(EncodeFieldSnafu {
165                        field: "Calling-AE-title",
166                    })?;
167                ae_title_bytes.resize(16, b' ');
168                writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
169                    field: "Called-AE-title",
170                })?;
171
172                // 43-74 - Reserved - This reserved field shall be sent with a value 00H for all
173                // bytes but not tested to this value when received
174                writer
175                    .write_all(&[0; 32])
176                    .context(WriteReservedSnafu { bytes: 32_u32 })?;
177
178                write_pdu_variable_application_context_name(
179                    writer,
180                    application_context_name,
181                    &codec,
182                )?;
183
184                for presentation_context in presentation_contexts {
185                    write_pdu_variable_presentation_context_proposed(
186                        writer,
187                        presentation_context,
188                        &codec,
189                    )?;
190                }
191
192                write_pdu_variable_user_variables(writer, user_variables, &codec)?;
193
194                Ok(())
195            })
196            .context(WriteChunkSnafu {
197                name: "A-ASSOCIATE-RQ",
198            })?;
199
200            Ok(())
201        }
202        Pdu::AssociationAC(AssociationAC {
203            protocol_version,
204            application_context_name,
205            called_ae_title,
206            calling_ae_title,
207            presentation_contexts,
208            user_variables,
209        }) => {
210            // A-ASSOCIATE-AC PDU Structure
211
212            // 1 - PDU-type - 02H
213            writer
214                .write_u8(0x02)
215                .context(WriteFieldSnafu { field: "PDU-type" })?;
216
217            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
218            // this value when received.
219            writer
220                .write_u8(0x00)
221                .context(WriteReservedSnafu { bytes: 1_u32 })?;
222
223            write_chunk_u32(writer, |writer| {
224                // 7-8 - Protocol-version - This two byte field shall use one bit to identify each
225                // version of the DICOM UL protocol supported by the calling end-system. This is
226                // Version 1 and shall be identified with bit 0 set. A receiver of this PDU
227                // implementing only this version of the DICOM UL protocol shall only test that bit
228                // 0 is set.
229                writer
230                    .write_u16::<BigEndian>(*protocol_version)
231                    .context(WriteFieldSnafu {
232                        field: "Protocol-version",
233                    })?;
234
235                // 9-10 - Reserved - This reserved field shall be sent with a value 0000H but not
236                // tested to this value when received.
237                writer
238                    .write_u16::<BigEndian>(0x00)
239                    .context(WriteReservedSnafu { bytes: 2_u32 })?;
240
241                // 11-26 - Reserved - This reserved field shall be sent with a value identical to
242                // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
243                // shall not be tested when received.
244                let mut ae_title_bytes =
245                    codec.encode(called_ae_title).context(EncodeFieldSnafu {
246                        field: "Called-AE-title",
247                    })?;
248                ae_title_bytes.resize(16, b' ');
249                writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
250                    field: "Called-AE-title",
251                })?;
252                // 27-42 - Reserved - This reserved field shall be sent with a value identical to
253                // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
254                // shall not be tested when received.
255                let mut ae_title_bytes =
256                    codec.encode(calling_ae_title).context(EncodeFieldSnafu {
257                        field: "Calling-AE-title",
258                    })?;
259                ae_title_bytes.resize(16, b' ');
260                writer.write_all(&ae_title_bytes).context(WriteFieldSnafu {
261                    field: "Calling-AE-title",
262                })?;
263
264                // 43-74 - Reserved - This reserved field shall be sent with a value identical to
265                // the value received in the same field of the A-ASSOCIATE-RQ PDU, but its value
266                // shall not be tested when received.
267                writer
268                    .write_all(&[0; 32])
269                    .context(WriteReservedSnafu { bytes: 32_u32 })?;
270
271                // 75-xxx - Variable items - This variable field shall contain the following items:
272                // one Application Context Item, one or more Presentation Context Item(s) and one
273                // User Information Item. For a complete description of these items see Section
274                // 7.1.1.2, Section 7.1.1.14, and Section 7.1.1.6.
275                write_pdu_variable_application_context_name(
276                    writer,
277                    application_context_name,
278                    &codec,
279                )?;
280
281                for presentation_context in presentation_contexts {
282                    write_pdu_variable_presentation_context_result(
283                        writer,
284                        presentation_context,
285                        &codec,
286                    )?;
287                }
288
289                write_pdu_variable_user_variables(writer, user_variables, &codec)?;
290
291                Ok(())
292            })
293            .context(WriteChunkSnafu {
294                name: "A-ASSOCIATE-AC",
295            })
296        }
297        Pdu::AssociationRJ(AssociationRJ { result, source }) => {
298            // 1 - PDU-type - 03H
299            writer
300                .write_u8(0x03)
301                .context(WriteFieldSnafu { field: "PDU-type" })?;
302
303            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received.
304            writer
305                .write_u8(0x00)
306                .context(WriteReservedSnafu { bytes: 1_u32 })?;
307
308            write_chunk_u32(writer, |writer| {
309                // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested to this value when received.
310                writer.write_u8(0x00).context(WriteReservedSnafu { bytes: 1_u32 })?;
311
312                // 8 - Result - This Result field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used:
313                // - 1 - rejected-permanent
314                // - 2 - rejected-transient
315                writer.write_u8(match result {
316                    AssociationRJResult::Permanent => {
317                        0x01
318                    }
319                    AssociationRJResult::Transient => {
320                        0x02
321                    }
322                }).context(WriteFieldSnafu { field: "AssociationRJResult" })?;
323
324                // 9 - Source - This Source field shall contain an integer value encoded as an unsigned binary number. One of the following values shall be used:
325                // - 1 - DICOM UL service-user
326                // - 2 - DICOM UL service-provider (ACSE related function)
327                // - 3 - DICOM UL service-provider (Presentation related function)
328                // 10 - Reason/Diag - This field shall contain an integer value encoded as an unsigned binary number.
329                // If the Source field has the value (1) "DICOM UL service-user", it shall take one of the following:
330                // - 1 - no-reason-given
331                // - 2 - application-context-name-not-supported
332                // - 3 - calling-AE-title-not-recognized
333                // - 4-6 - reserved
334                // - 7 - called-AE-title-not-recognized
335                // - 8-10 - reserved
336                // If the Source field has the value (2) "DICOM UL service provided (ACSE related function)", it shall take one of the following:
337                // - 1 - no-reason-given
338                // - 2 - protocol-version-not-supported
339                // If the Source field has the value (3) "DICOM UL service provided (Presentation related function)", it shall take one of the following:
340                // 0 - reserved
341                // 1 - temporary-congestion
342                // 2 - local-limit-exceeded
343                // 3-7 - reserved
344                match source {
345                    AssociationRJSource::ServiceUser(reason) => {
346                        writer.write_u8(0x01).context(WriteFieldSnafu { field: "AssociationRJServiceUserReason" })?;
347                        writer.write_u8(match reason {
348                            AssociationRJServiceUserReason::NoReasonGiven => {
349                                0x01
350                            }
351                            AssociationRJServiceUserReason::ApplicationContextNameNotSupported => {
352                                0x02
353                            }
354                            AssociationRJServiceUserReason::CallingAETitleNotRecognized => {
355                                0x03
356                            }
357                            AssociationRJServiceUserReason::CalledAETitleNotRecognized => {
358                                0x07
359                            }
360                            AssociationRJServiceUserReason::Reserved(data) => {
361                                *data
362                            }
363                        }).context(WriteFieldSnafu { field: "AssociationRJServiceUserReason (2)" })?;
364                    }
365                    AssociationRJSource::ServiceProviderASCE(reason) => {
366                        writer.write_u8(0x02).context(WriteFieldSnafu { field: "AssociationRJServiceProvider" })?;
367                        writer.write_u8(match reason {
368                            AssociationRJServiceProviderASCEReason::NoReasonGiven => {
369                                0x01
370                            }
371                            AssociationRJServiceProviderASCEReason::ProtocolVersionNotSupported => {
372                                0x02
373                            }
374                        }).context(WriteFieldSnafu { field: "AssociationRJServiceProvider (2)" })?;
375                    }
376                    AssociationRJSource::ServiceProviderPresentation(reason) => {
377                        writer.write_u8(0x03).context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason" })?;
378                        writer.write_u8(match reason {
379                            AssociationRJServiceProviderPresentationReason::TemporaryCongestion => {
380                                0x01
381                            }
382                            AssociationRJServiceProviderPresentationReason::LocalLimitExceeded => {
383                                0x02
384                            }
385                            AssociationRJServiceProviderPresentationReason::Reserved(data) => {
386                                *data
387                            }
388                        }).context(WriteFieldSnafu { field: "AssociationRJServiceProviderPresentationReason (2)" })?;
389                    }
390                }
391
392                Ok(())
393            }).context(WriteChunkSnafu { name: "AssociationRJ" })?;
394
395            Ok(())
396        }
397        Pdu::PData { data } => {
398            // 1 - PDU-type - 04H
399            writer
400                .write_u8(0x04)
401                .context(WriteFieldSnafu { field: "PDU-type" })?;
402
403            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
404            // this value when received.
405            writer
406                .write_u8(0x00)
407                .context(WriteReservedSnafu { bytes: 1_u32 })?;
408
409            write_chunk_u32(writer, |writer| {
410                // 7-xxx - Presentation-data-value Item(s) - This variable data field shall contain
411                // one or more Presentation-data-value Items(s). For a complete description of the
412                // use of this field see Section 9.3.5.1
413
414                for presentation_data_value in data {
415                    write_chunk_u32(writer, |writer| {
416                        // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd
417                        // integers between 1 and 255, encoded as an unsigned binary number. For a
418                        // complete description of the use of this field see Section 7.1.1.13.
419                        writer.push(presentation_data_value.presentation_context_id);
420
421                        // 6-xxx - Presentation-data-value - This Presentation-data-value field
422                        // shall contain DICOM message information (command and/or data set) with a
423                        // message control header. For a complete description of the use of this
424                        // field see Annex E.
425
426                        // The Message Control Header shall be made of one byte with the least
427                        // significant bit (bit 0) taking one of the following values:
428                        // - If bit 0 is set to 1, the following fragment shall contain Message
429                        //   Command information.
430                        // - If bit 0 is set to 0, the following fragment shall contain Message Data
431                        //   Set information.
432                        // The next least significant bit (bit 1) shall be defined by the following
433                        // rules: If bit 1 is set to 1, the following fragment shall contain the
434                        // last fragment of a Message Data Set or of a Message Command.
435                        // - If bit 1 is set to 0, the following fragment does not contain the last
436                        //   fragment of a Message Data Set or of a Message Command.
437                        let mut message_header = 0x00;
438                        if let PDataValueType::Command = presentation_data_value.value_type {
439                            message_header |= 0x01;
440                        }
441                        if presentation_data_value.is_last {
442                            message_header |= 0x02;
443                        }
444                        writer.push(message_header);
445
446                        // Message fragment
447                        writer.extend(&presentation_data_value.data);
448
449                        Ok(())
450                    })
451                    .context(WriteChunkSnafu {
452                        name: "Presentation-data-value item",
453                    })?;
454                }
455
456                Ok(())
457            })
458            .context(WriteChunkSnafu { name: "PData" })
459        }
460        Pdu::ReleaseRQ => {
461            // 1 - PDU-type - 05H
462            writer
463                .write_u8(0x05)
464                .context(WriteFieldSnafu { field: "PDU-type" })?;
465
466            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
467            // this value when received.
468            writer
469                .write_u8(0x00)
470                .context(WriteReservedSnafu { bytes: 1_u32 })?;
471
472            write_chunk_u32(writer, |writer| {
473                writer.extend([0u8; 4]);
474                Ok(())
475            })
476            .context(WriteChunkSnafu { name: "ReleaseRQ" })?;
477
478            Ok(())
479        }
480        Pdu::ReleaseRP => {
481            // 1 - PDU-type - 06H
482            writer
483                .write_u8(0x06)
484                .context(WriteFieldSnafu { field: "PDU-type" })?;
485
486            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
487            // this value when received.
488            writer
489                .write_u8(0x00)
490                .context(WriteReservedSnafu { bytes: 1_u32 })?;
491
492            write_chunk_u32(writer, |writer| {
493                writer.extend([0u8; 4]);
494                Ok(())
495            })
496            .context(WriteChunkSnafu { name: "ReleaseRP" })?;
497
498            Ok(())
499        }
500        Pdu::AbortRQ { source } => {
501            // 1 - PDU-type - 07H
502            writer
503                .write_u8(0x07)
504                .context(WriteFieldSnafu { field: "PDU-type" })?;
505
506            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
507            // this value when received.
508            writer
509                .write_u8(0x00)
510                .context(WriteReservedSnafu { bytes: 1_u32 })?;
511
512            write_chunk_u32(writer, |writer| {
513                // 7 - Reserved - This reserved field shall be sent with a value 00H but not tested
514                // to this value when received.
515                writer.push(0);
516                // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested
517                // to this value when received.
518                writer.push(0);
519
520                // 9 - Source - This Source field shall contain an integer value encoded as an
521                // unsigned binary number. One of the following values shall be used:
522                // - 0 - DICOM UL service-user (initiated abort)
523                // - 1 - reserved
524                // - 2 - DICOM UL service-provider (initiated abort)
525                // 10 - Reason/Diag - This field shall contain an integer value encoded as an
526                // unsigned binary number. If the Source field has the value (2) "DICOM UL
527                // service-provider", it shall take one of the following:
528                // - 0 - reason-not-specified1 - unrecognized-PDU
529                // - 2 - unexpected-PDU
530                // - 3 - reserved
531                // - 4 - unrecognized-PDU parameter
532                // - 5 - unexpected-PDU parameter
533                // - 6 - invalid-PDU-parameter value
534                // If the Source field has the value (0) "DICOM UL service-user", this reason field
535                // shall not be significant. It shall be sent with a value 00H but not tested to
536                // this value when received.
537                let source_word = match source {
538                    AbortRQSource::ServiceUser => [0x00; 2],
539                    AbortRQSource::Reserved => [0x01, 0x00],
540                    AbortRQSource::ServiceProvider(reason) => match reason {
541                        AbortRQServiceProviderReason::ReasonNotSpecified => [0x02, 0x00],
542                        AbortRQServiceProviderReason::UnrecognizedPdu => [0x02, 0x01],
543                        AbortRQServiceProviderReason::UnexpectedPdu => [0x02, 0x02],
544                        AbortRQServiceProviderReason::Reserved => [0x02, 0x03],
545                        AbortRQServiceProviderReason::UnrecognizedPduParameter => [0x02, 0x04],
546                        AbortRQServiceProviderReason::UnexpectedPduParameter => [0x02, 0x05],
547                        AbortRQServiceProviderReason::InvalidPduParameter => [0x02, 0x06],
548                    },
549                };
550                writer.extend(source_word);
551
552                Ok(())
553            })
554            .context(WriteChunkSnafu { name: "AbortRQ" })?;
555
556            Ok(())
557        }
558        Pdu::Unknown { pdu_type, data } => {
559            // 1 - PDU-type - XXH
560            writer
561                .write_u8(*pdu_type)
562                .context(WriteFieldSnafu { field: "PDU-type" })?;
563
564            // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to
565            // this value when received.
566            writer
567                .write_u8(0x00)
568                .context(WriteReservedSnafu { bytes: 1_u32 })?;
569
570            write_chunk_u32(writer, |writer| {
571                writer.extend(data);
572                Ok(())
573            })
574            .context(WriteChunkSnafu { name: "Unknown" })?;
575
576            Ok(())
577        }
578    }
579}
580
581fn write_pdu_variable_application_context_name(
582    writer: &mut dyn Write,
583    application_context_name: &str,
584    codec: &dyn TextCodec,
585) -> Result<()> {
586    // Application Context Item Structure
587    // 1 - Item-type - 10H
588    writer
589        .write_u8(0x10)
590        .context(WriteFieldSnafu { field: "Item-type" })?;
591
592    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
593    // tested to this value when received.
594    writer
595        .write_u8(0x00)
596        .context(WriteReservedSnafu { bytes: 1_u32 })?;
597
598    write_chunk_u16(writer, |writer| {
599        // 5-xxx - Application-context-name -A valid Application-context-name shall
600        // be encoded as defined in Annex F. For a description of the use of this
601        // field see Section 7.1.1.2. Application-context-names are structured as
602        // UIDs as defined in PS3.5 (see Annex A for an overview of this concept).
603        // DICOM Application-context-names are registered in PS3.7.
604        writer
605            .write_all(
606                &codec
607                    .encode(application_context_name)
608                    .context(EncodeFieldSnafu {
609                        field: "Application-context-name",
610                    })?,
611            )
612            .context(WriteFieldSnafu {
613                field: "Application-context-name",
614            })
615    })
616    .context(WriteChunkSnafu {
617        name: "Application Context Item",
618    })?;
619
620    Ok(())
621}
622
623fn write_pdu_variable_presentation_context_proposed(
624    writer: &mut dyn Write,
625    presentation_context: &PresentationContextProposed,
626    codec: &dyn TextCodec,
627) -> Result<()> {
628    // Presentation Context Item Structure
629    // 1 - tem-type - 20H
630    writer
631        .write_u8(0x20)
632        .context(WriteFieldSnafu { field: "Item-type" })?;
633
634    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
635    // tested to this value when received.
636    writer
637        .write_u8(0x00)
638        .context(WriteReservedSnafu { bytes: 1_u32 })?;
639
640    write_chunk_u16(writer, |writer| {
641        // 5 - Presentation-context-ID - Presentation-context-ID values shall be
642        // odd integers between 1 and 255, encoded as an unsigned binary number.
643        // For a complete description of the use of this field see Section
644        // 7.1.1.13.
645        writer
646            .write_u8(presentation_context.id)
647            .context(WriteFieldSnafu {
648                field: "Presentation-context-ID",
649            })?;
650
651        // 6 - Reserved - This reserved field shall be sent with a value 00H but
652        // not tested to this value when received.
653        writer
654            .write_u8(0x00)
655            .context(WriteReservedSnafu { bytes: 1_u32 })?;
656
657        // 7 - Reserved - This reserved field shall be sent with a value 00H but
658        // not tested to this value when received
659        writer
660            .write_u8(0x00)
661            .context(WriteReservedSnafu { bytes: 1_u32 })?;
662
663        // 8 - Reserved - This reserved field shall be sent with a value 00H but
664        // not tested to this value when received.
665        writer
666            .write_u8(0x00)
667            .context(WriteReservedSnafu { bytes: 1_u32 })?;
668
669        // 9-xxx - Abstract/Transfer Syntax Sub-Items - This variable field
670        // shall contain the following sub-items: one Abstract Syntax and one or
671        // more Transfer Syntax(es). For a complete description of the use and
672        // encoding of these sub-items see Section 9.3.2.2.1 and Section
673        // 9.3.2.2.2.
674
675        // Abstract Syntax Sub-Item Structure
676        // 1 - Item-type 30H
677        writer
678            .write_u8(0x30)
679            .context(WriteFieldSnafu { field: "Item-type" })?;
680
681        // 2 - Reserved - This reserved field shall be sent with a value 00H
682        // but not tested to this value when
683        // received.
684        writer
685            .write_u8(0x00)
686            .context(WriteReservedSnafu { bytes: 1_u32 })?;
687
688        write_chunk_u16(writer, |writer| {
689            // 5-xxx - Abstract-syntax-name - This variable field shall
690            // contain
691            // the Abstract-syntax-name related to the proposed presentation
692            // context. A valid Abstract-syntax-name shall be encoded as
693            // defined in Annex F. For a
694            // description of the use of this field see
695            // Section 7.1.1.13. Abstract-syntax-names are structured as
696            // UIDs as defined in PS3.5
697            // (see Annex B for an overview of this concept).
698            // DICOM Abstract-syntax-names are registered in PS3.4.
699            writer
700                .write_all(
701                    &codec
702                        .encode(&presentation_context.abstract_syntax)
703                        .context(EncodeFieldSnafu {
704                            field: "Abstract-syntax-name",
705                        })?,
706                )
707                .context(WriteFieldSnafu {
708                    field: "Abstract-syntax-name",
709                })
710        })
711        .context(WriteChunkSnafu {
712            name: "Abstract Syntax Item",
713        })?;
714
715        for transfer_syntax in &presentation_context.transfer_syntaxes {
716            // Transfer Syntax Sub-Item Structure
717            // 1 - Item-type - 40H
718            writer.write_u8(0x40).context(WriteFieldSnafu {
719                field: "Presentation-context Item-type",
720            })?;
721
722            // 2 - Reserved - This reserved field shall be sent with a value 00H
723            // but not tested to this value when received.
724            writer
725                .write_u8(0x00)
726                .context(WriteReservedSnafu { bytes: 1_u32 })?;
727
728            write_chunk_u16(writer, |writer| {
729                // 5-xxx - Transfer-syntax-name(s) - This variable field shall
730                // contain the Transfer-syntax-name proposed for this
731                // presentation context. A valid Transfer-syntax-name shall be
732                // encoded as defined in Annex F. For a description of the use
733                // of this field see Section 7.1.1.13. Transfer-syntax-names are
734                // structured as UIDs as defined in PS3.5 (see Annex B for an
735                // overview of this concept). DICOM Transfer-syntax-names are
736                // registered in PS3.5.
737                writer
738                    .write_all(&codec.encode(transfer_syntax).context(EncodeFieldSnafu {
739                        field: "Transfer-syntax-name",
740                    })?)
741                    .context(WriteFieldSnafu {
742                        field: "Transfer-syntax-name",
743                    })
744            })
745            .context(WriteChunkSnafu {
746                name: "Transfer Syntax Sub-Item",
747            })?;
748        }
749
750        Ok(())
751    })
752    .context(WriteChunkSnafu {
753        name: "Presentation Context Item",
754    })?;
755
756    Ok(())
757}
758
759fn write_pdu_variable_presentation_context_result(
760    writer: &mut dyn Write,
761    presentation_context: &PresentationContextResult,
762    codec: &dyn TextCodec,
763) -> Result<()> {
764    // 1 - Item-type - 21H
765    writer
766        .write_u8(0x21)
767        .context(WriteFieldSnafu { field: "Item-type" })?;
768
769    // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
770    // value when received.
771    writer
772        .write_u8(0x00)
773        .context(WriteReservedSnafu { bytes: 1_u32 })?;
774
775    write_chunk_u16(writer, |writer| {
776        // 5 - Presentation-context-ID - Presentation-context-ID values shall be odd integers
777        // between 1 and 255, encoded as an unsigned binary number. For a complete description of
778        // the use of this field see Section 7.1.1.13.
779        writer
780            .write_u8(presentation_context.id)
781            .context(WriteFieldSnafu {
782                field: "Presentation-context-ID",
783            })?;
784
785        // 6 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
786        // value when received.
787        writer
788            .write_u8(0x00)
789            .context(WriteReservedSnafu { bytes: 1_u32 })?;
790
791        // 7 - Result/Reason - This Result/Reason field shall contain an integer value encoded as an
792        // unsigned binary number. One of the following values shall be used:
793        //   0 - acceptance
794        //   1 - user-rejection
795        //   2 - no-reason (provider rejection)
796        //   3 - abstract-syntax-not-supported (provider rejection)
797        //   4 - transfer-syntaxes-not-supported (provider rejection)
798        writer
799            .write_u8(match &presentation_context.reason {
800                PresentationContextResultReason::Acceptance => 0,
801                PresentationContextResultReason::UserRejection => 1,
802                PresentationContextResultReason::NoReason => 2,
803                PresentationContextResultReason::AbstractSyntaxNotSupported => 3,
804                PresentationContextResultReason::TransferSyntaxesNotSupported => 4,
805            })
806            .context(WriteFieldSnafu {
807                field: "Presentation Context Result/Reason",
808            })?;
809
810        // 8 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
811        // value when received.
812        writer
813            .write_u8(0x00)
814            .context(WriteReservedSnafu { bytes: 1_u32 })?;
815
816        // 9-xxx - Transfer syntax sub-item - This variable field shall contain one Transfer Syntax
817        // Sub-Item. When the Result/Reason field has a value other than acceptance (0), this field
818        // shall not be significant and its value shall not be tested when received. For a complete
819        // description of the use and encoding of this item see Section 9.3.3.2.1.
820
821        // 1 - Item-type - 40H
822        writer
823            .write_u8(0x40)
824            .context(WriteFieldSnafu { field: "Item-type" })?;
825
826        // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
827        // value when received.
828        writer
829            .write_u8(0x40)
830            .context(WriteReservedSnafu { bytes: 1_u32 })?;
831
832        write_chunk_u16(writer, |writer| {
833            // 5-xxx - Transfer-syntax-name - This variable field shall contain the
834            // Transfer-syntax-name proposed for this presentation context. A valid
835            // Transfer-syntax-name shall be encoded as defined in Annex F. For a description of the
836            // use of this field see Section 7.1.1.14. Transfer-syntax-names are structured as UIDs
837            // as defined in PS3.5 (see Annex B for an overview of this concept). DICOM
838            // Transfer-syntax-names are registered in PS3.5.
839            writer
840                .write_all(
841                    &codec
842                        .encode(&presentation_context.transfer_syntax)
843                        .context(EncodeFieldSnafu {
844                            field: "Transfer-syntax-name",
845                        })?,
846                )
847                .context(WriteFieldSnafu {
848                    field: "Transfer-syntax-name",
849                })?;
850
851            Ok(())
852        })
853        .context(WriteChunkSnafu {
854            name: "Transfer Syntax sub-item",
855        })?;
856
857        Ok(())
858    })
859    .context(WriteChunkSnafu {
860        name: "Presentation-context",
861    })
862}
863
864fn write_pdu_variable_user_variables(
865    writer: &mut dyn Write,
866    user_variables: &[UserVariableItem],
867    codec: &dyn TextCodec,
868) -> Result<()> {
869    if user_variables.is_empty() {
870        return Ok(());
871    }
872
873    // 1 - Item-type - 50H
874    writer
875        .write_u8(0x50)
876        .context(WriteFieldSnafu { field: "Item-type" })?;
877
878    // 2 - Reserved - This reserved field shall be sent with a value 00H but not tested to this
879    // value when received.
880    writer
881        .write_u8(0x00)
882        .context(WriteReservedSnafu { bytes: 1_u32 })?;
883
884    write_chunk_u16(writer, |writer| {
885        // 5-xxx - User-data - This variable field shall contain User-data sub-items as defined by
886        // the DICOM Application Entity. The structure and content of these sub-items is defined in
887        // Annex D.
888        for user_variable in user_variables {
889            match user_variable {
890                UserVariableItem::MaxLength(max_length) => {
891                    // 1 - Item-type - 51H
892                    writer
893                        .write_u8(0x51)
894                        .context(WriteFieldSnafu { field: "Item-type" })?;
895
896                    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
897                    // tested to this value when received.
898                    writer
899                        .write_u8(0x00)
900                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
901
902                    write_chunk_u16(writer, |writer| {
903                        // 5-8 - Maximum-length-received - This parameter allows the
904                        // association-requestor to restrict the maximum length of the variable
905                        // field of the P-DATA-TF PDUs sent by the acceptor on the association once
906                        // established. This length value is indicated as a number of bytes encoded
907                        // as an unsigned binary number. The value of (0) indicates that no maximum
908                        // length is specified. This maximum length value shall never be exceeded by
909                        // the PDU length values used in the PDU-length field of the P-DATA-TF PDUs
910                        // received by the association-requestor. Otherwise, it shall be a protocol
911                        // error.
912                        writer
913                            .write_u32::<BigEndian>(*max_length)
914                            .context(WriteFieldSnafu {
915                                field: "Maximum-length-received",
916                            })
917                    })
918                    .context(WriteChunkSnafu {
919                        name: "Maximum-length-received",
920                    })?;
921                }
922                UserVariableItem::ImplementationVersionName(implementation_version_name) => {
923                    // 1 - Item-type - 55H
924                    writer
925                        .write_u8(0x55)
926                        .context(WriteFieldSnafu { field: "Item-type" })?;
927
928                    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
929                    // tested to this value when received.
930                    writer
931                        .write_u8(0x00)
932                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
933
934                    write_chunk_u16(writer, |writer| {
935                        // 5 - xxx - Implementation-version-name - This variable field shall contain
936                        // the Implementation-version-name of the Association-acceptor as defined in
937                        // Section D.3.3.2. It shall be encoded as a string of 1 to 16 ISO 646:1990
938                        // (basic G0 set) characters.
939                        writer
940                            .write_all(&codec.encode(implementation_version_name).context(
941                                EncodeFieldSnafu {
942                                    field: "Implementation-version-name",
943                                },
944                            )?)
945                            .context(WriteFieldSnafu {
946                                field: "Implementation-version-name",
947                            })
948                    })
949                    .context(WriteChunkSnafu {
950                        name: "Implementation-version-name",
951                    })?;
952                }
953                UserVariableItem::ImplementationClassUID(implementation_class_uid) => {
954                    // 1 - Item-type - 52H
955                    writer
956                        .write_u8(0x52)
957                        .context(WriteFieldSnafu { field: "Item-type" })?;
958
959                    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
960                    // tested to this value when received.
961                    writer
962                        .write_u8(0x00)
963                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
964
965                    write_chunk_u16(writer, |writer| {
966                        //5 - xxx - Implementation-class-uid - This variable field shall contain
967                        // the Implementation-class-uid of the Association-acceptor as defined in
968                        // Section D.3.3.2. The Implementation-class-uid field is structured as a
969                        // UID as defined in PS3.5.
970                        writer
971                            .write_all(&codec.encode(implementation_class_uid).context(
972                                EncodeFieldSnafu {
973                                    field: "Implementation-class-uid",
974                                },
975                            )?)
976                            .context(WriteFieldSnafu {
977                                field: "Implementation-class-uid",
978                            })
979                    })
980                    .context(WriteChunkSnafu {
981                        name: "Implementation-class-uid",
982                    })?;
983                }
984                UserVariableItem::SopClassExtendedNegotiationSubItem(sop_class_uid, data) => {
985                    // 1 - Item-type - 56H
986                    writer
987                        .write_u8(0x56)
988                        .context(WriteFieldSnafu { field: "Item-type" })?;
989                    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
990                    // tested to this value when received.
991                    writer
992                        .write_u8(0x00)
993                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
994
995                    write_chunk_u16(writer, |writer| {
996                        write_chunk_u16(writer, |writer| {
997                            //  7-xxx - The SOP Class or Meta SOP Class identifier encoded as a UID
998                            //  as defined in Section 9 “Unique Identifiers (UIDs)” in PS3.5.
999                            writer
1000                                .write_all(&codec.encode(sop_class_uid).context(
1001                                    EncodeFieldSnafu {
1002                                        field: "SOP-class-uid",
1003                                    },
1004                                )?)
1005                                .context(WriteFieldSnafu {
1006                                    field: "SOP-class-uid",
1007                                })
1008                        })
1009                        .context(WriteChunkSnafu {
1010                            name: "SOP-class-uid",
1011                        })?;
1012
1013                        write_chunk_u16(writer, |writer| {
1014                            // xxx-xxx Service-class-application-information - This field shall contain
1015                            // the application information specific to the Service Class specification
1016                            // identified by the SOP-class-uid. The semantics and value of this field is
1017                            // defined in the identified Service Class specification.
1018                            writer.write_all(data).context(WriteFieldSnafu {
1019                                field: "Service-class-application-information",
1020                            })
1021                        })
1022                        .context(WriteChunkSnafu {
1023                            name: "Service-class-application-information",
1024                        })
1025                    })
1026                    .context(WriteChunkSnafu { name: "Sub-item" })?;
1027                }
1028                UserVariableItem::UserIdentityItem(user_identity) => {
1029                    // 1 - Item-type - 58H
1030                    writer
1031                        .write_u8(0x58)
1032                        .context(WriteFieldSnafu { field: "Item-type" })?;
1033
1034                    // 2 - Reserved - This reserved field shall be sent with a value 00H but not
1035                    // tested to this value when received.
1036                    writer
1037                        .write_u8(0x00)
1038                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
1039
1040                    // 3-4 - Item-length
1041                    write_chunk_u16(writer, |writer| {
1042                        // 5 - User-Identity-Type
1043                        writer
1044                            .write_u8(user_identity.identity_type().to_u8())
1045                            .context(WriteFieldSnafu {
1046                                field: "User-Identity-Type",
1047                            })?;
1048
1049                        // 6 - Positive-response-requested
1050                        let positive_response_requested_out: u8 =
1051                            if user_identity.positive_response_requested() {
1052                                1
1053                            } else {
1054                                0
1055                            };
1056                        writer.write_u8(positive_response_requested_out).context(
1057                            WriteFieldSnafu {
1058                                field: "Positive-response-requested",
1059                            },
1060                        )?;
1061
1062                        // 7-8 - Primary-field-length
1063                        write_chunk_u16(writer, |writer| {
1064                            // 9-n - Primary-field
1065                            writer
1066                                .write_all(user_identity.primary_field().as_slice())
1067                                .context(WriteFieldSnafu {
1068                                    field: "Primary-field",
1069                                })
1070                        })
1071                        .context(WriteChunkSnafu {
1072                            name: "Primary-field",
1073                        })?;
1074
1075                        // n+1-n+2 - Secondary-field-length
1076                        write_chunk_u16(writer, |writer| {
1077                            // n+3-m - Secondary-field
1078                            writer
1079                                .write_all(user_identity.secondary_field().as_slice())
1080                                .context(WriteFieldSnafu {
1081                                    field: "Secondary-field",
1082                                })
1083                        })
1084                        .context(WriteChunkSnafu {
1085                            name: "Secondary-field",
1086                        })
1087                    })
1088                    .context(WriteChunkSnafu {
1089                        name: "Item-length",
1090                    })?;
1091                }
1092                UserVariableItem::Unknown(item_type, data) => {
1093                    writer
1094                        .write_u8(*item_type)
1095                        .context(WriteFieldSnafu { field: "Item-type" })?;
1096
1097                    writer
1098                        .write_u8(0x00)
1099                        .context(WriteReservedSnafu { bytes: 1_u32 })?;
1100
1101                    write_chunk_u16(writer, |writer| {
1102                        writer.write_all(data).context(WriteFieldSnafu {
1103                            field: "Unknown Data",
1104                        })
1105                    })
1106                    .context(WriteChunkSnafu { name: "Unknown" })?;
1107                }
1108            }
1109        }
1110
1111        Ok(())
1112    })
1113    .context(WriteChunkSnafu { name: "User-data" })
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119
1120    #[test]
1121    fn can_write_chunks_with_preceding_u32_length() -> Result<()> {
1122        let mut bytes = vec![0u8; 0];
1123        write_chunk_u32(&mut bytes, |writer| {
1124            writer
1125                .write_u8(0x02)
1126                .context(WriteFieldSnafu { field: "Field1" })?;
1127            write_chunk_u32(writer, |writer| {
1128                writer
1129                    .write_u8(0x03)
1130                    .context(WriteFieldSnafu { field: "Field2" })?;
1131                Ok(())
1132            })
1133            .context(WriteChunkSnafu { name: "Chunk2" })
1134        })
1135        .context(WriteChunkSnafu { name: "Chunk1" })?;
1136
1137        assert_eq!(bytes.len(), 10);
1138        assert_eq!(bytes, &[0, 0, 0, 6, 2, 0, 0, 0, 1, 3]);
1139
1140        Ok(())
1141    }
1142
1143    #[test]
1144    fn can_write_chunks_with_preceding_u16_length() -> Result<()> {
1145        let mut bytes = vec![0u8; 0];
1146        write_chunk_u16(&mut bytes, |writer| {
1147            writer
1148                .write_u8(0x02)
1149                .context(WriteFieldSnafu { field: "Field1" })?;
1150            write_chunk_u16(writer, |writer| {
1151                writer
1152                    .write_u8(0x03)
1153                    .context(WriteFieldSnafu { field: "Field2" })?;
1154                Ok(())
1155            })
1156            .context(WriteChunkSnafu { name: "Chunk2" })
1157        })
1158        .context(WriteChunkSnafu { name: "Chunk1" })?;
1159
1160        assert_eq!(bytes.len(), 6);
1161        assert_eq!(bytes, &[0, 4, 2, 0, 1, 3]);
1162
1163        Ok(())
1164    }
1165
1166    #[test]
1167    fn write_abort_rq() {
1168        let mut out = vec![];
1169
1170        // abort by request of SCU
1171        let pdu = Pdu::AbortRQ {
1172            source: AbortRQSource::ServiceUser,
1173        };
1174        write_pdu(&mut out, &pdu).unwrap();
1175        assert_eq!(
1176            &out,
1177            &[
1178                // code 7 + reserved byte
1179                0x07, 0x00, //
1180                // PDU length: 4 bytes
1181                0x00, 0x00, 0x00, 0x04, //
1182                // reserved 2 bytes + source: service user (0) + reason (0)
1183                0x00, 0x00, 0x00, 0x00,
1184            ]
1185        );
1186        out.clear();
1187
1188        // Reserved
1189        let pdu = Pdu::AbortRQ {
1190            source: AbortRQSource::Reserved,
1191        };
1192        write_pdu(&mut out, &pdu).unwrap();
1193        assert_eq!(
1194            &out,
1195            &[
1196                // code 7 + reserved byte
1197                0x07, 0x00, //
1198                // PDU length: 4 bytes
1199                0x00, 0x00, 0x00, 0x04, //
1200                // reserved 2 bytes + source: reserved (1) + reason (0)
1201                0x00, 0x00, 0x01, 0x00,
1202            ]
1203        );
1204        out.clear();
1205
1206        // abort by request of SCP
1207        let pdu = Pdu::AbortRQ {
1208            source: AbortRQSource::ServiceProvider(
1209                AbortRQServiceProviderReason::InvalidPduParameter,
1210            ),
1211        };
1212        write_pdu(&mut out, &pdu).unwrap();
1213        assert_eq!(
1214            &out,
1215            &[
1216                // code 7 + reserved byte
1217                0x07, 0x00, //
1218                // PDU length: 4 bytes
1219                0x00, 0x00, 0x00, 0x04, //
1220                // reserved 2 bytes
1221                0x00, 0x00, //
1222                // source: service provider (2), invalid parameter value (6)
1223                0x02, 0x06,
1224            ]
1225        );
1226    }
1227}