Skip to main content

netget/server/dicom/
pdu.rs

1//! DICOM upper layer (PS3.8 ยง9): A-ASSOCIATE-RQ/AC/RJ, P-DATA-TF with presentation data
2//! values, A-RELEASE, A-ABORT; and DIMSE command sets (PS3.7), which are always Implicit VR
3//! Little Endian.
4use super::dataset::{self, IMPLICIT_LE};
5use anyhow::{bail, ensure, Context, Result};
6use serde_json::{json, Map, Value};
7use tokio::io::{AsyncRead, AsyncReadExt};
8
9pub const APPLICATION_CONTEXT: &str = "1.2.840.10008.3.1.1.1";
10pub const VERIFICATION: &str = "1.2.840.10008.1.1";
11pub const PATIENT_ROOT_FIND: &str = "1.2.840.10008.5.1.4.1.2.1.1";
12pub const STUDY_ROOT_FIND: &str = "1.2.840.10008.5.1.4.1.2.2.1";
13pub const STORAGE_PREFIX: &str = "1.2.840.10008.5.1.4.1.1.";
14pub const IMPLEMENTATION_CLASS: &str = "1.2.826.0.1.3680043.10.1408.1";
15pub const IMPLEMENTATION_VERSION: &str = "NETGET_1";
16/// Largest PDU NetGet accepts and announces (its maximum receive length).
17pub const MAX_PDU: u32 = 1 << 20;
18
19pub const C_STORE_RQ: u16 = 0x0001;
20pub const C_FIND_RQ: u16 = 0x0020;
21pub const C_ECHO_RQ: u16 = 0x0030;
22pub const C_CANCEL_RQ: u16 = 0x0FFF;
23pub const NO_DATASET: u16 = 0x0101;
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct PresentationContext {
27    pub id: u8,
28    pub abstract_syntax: String,
29    pub transfer_syntaxes: Vec<String>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct AssociateRq {
34    pub called: String,
35    pub calling: String,
36    pub contexts: Vec<PresentationContext>,
37    pub max_pdu: u32,
38    pub implementation: Option<String>,
39}
40
41#[derive(Debug, Clone, PartialEq)]
42pub enum Pdu {
43    AssociateRq(AssociateRq),
44    /// (context id, result, transfer syntax), peer max PDU.
45    AssociateAc(Vec<(u8, u8, String)>, u32),
46    AssociateRj {
47        result: u8,
48        source: u8,
49        reason: u8,
50    },
51    /// (context id, flags, bytes).
52    Data(Vec<(u8, u8, Vec<u8>)>),
53    ReleaseRq,
54    ReleaseRp,
55    Abort {
56        source: u8,
57        reason: u8,
58    },
59}
60
61fn ae(raw: &[u8]) -> String {
62    String::from_utf8_lossy(raw).trim().to_owned()
63}
64
65fn uid(raw: &[u8]) -> Result<String> {
66    let s = std::str::from_utf8(raw)?
67        .trim_end_matches(['\0', ' '])
68        .to_owned();
69    ensure!(
70        !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_digit() || b == b'.'),
71        "invalid UID"
72    );
73    Ok(s)
74}
75
76pub async fn read<R: AsyncRead + Unpin>(r: &mut R) -> Result<Option<Pdu>> {
77    let mut head = [0u8; 6];
78    match r.read_exact(&mut head).await {
79        Ok(_) => {}
80        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
81        Err(e) => return Err(e.into()),
82    }
83    let len = u32::from_be_bytes([head[2], head[3], head[4], head[5]]);
84    ensure!(len <= MAX_PDU + 6, "PDU of {len} bytes exceeds the maximum");
85    let mut body = vec![0u8; len as usize];
86    r.read_exact(&mut body).await?;
87    Ok(Some(decode(head[0], &body)?))
88}
89
90fn items(b: &[u8]) -> Result<Vec<(u8, &[u8])>> {
91    let mut out = Vec::new();
92    let mut i = 0;
93    while i < b.len() {
94        ensure!(i + 4 <= b.len(), "truncated item");
95        let t = b[i];
96        let l = u16::from_be_bytes([b[i + 2], b[i + 3]]) as usize;
97        let body = b
98            .get(i + 4..i + 4 + l)
99            .context("item longer than its PDU")?;
100        out.push((t, body));
101        i += 4 + l;
102    }
103    Ok(out)
104}
105
106pub fn decode(kind: u8, b: &[u8]) -> Result<Pdu> {
107    Ok(match kind {
108        0x01 | 0x02 => {
109            ensure!(b.len() >= 68, "short association PDU");
110            let mut contexts = Vec::new();
111            let mut accepted = Vec::new();
112            let mut max_pdu = 0;
113            let mut implementation = None;
114            let mut app_ok = false;
115            for (t, body) in items(&b[68..])? {
116                match t {
117                    0x10 => {
118                        app_ok = std::str::from_utf8(body)
119                            .map(|s| s.trim_end_matches('\0') == APPLICATION_CONTEXT)
120                            .unwrap_or(false)
121                    }
122                    0x20 if kind == 0x01 => {
123                        ensure!(body.len() >= 4, "short presentation context");
124                        let mut abs = None;
125                        let mut ts = Vec::new();
126                        for (st, sb) in items(&body[4..])? {
127                            match st {
128                                0x30 => abs = Some(uid(sb)?),
129                                0x40 => ts.push(uid(sb)?),
130                                _ => {}
131                            }
132                        }
133                        contexts.push(PresentationContext {
134                            id: body[0],
135                            abstract_syntax: abs
136                                .context("presentation context without abstract syntax")?,
137                            transfer_syntaxes: ts,
138                        });
139                    }
140                    0x21 if kind == 0x02 => {
141                        ensure!(body.len() >= 4, "short presentation context");
142                        let ts = items(&body[4..])?
143                            .into_iter()
144                            .find(|(st, _)| *st == 0x40)
145                            .map(|(_, sb)| uid(sb))
146                            .transpose()?
147                            .unwrap_or_default();
148                        accepted.push((body[0], body[2], ts));
149                    }
150                    0x50 => {
151                        for (st, sb) in items(body)? {
152                            match st {
153                                0x51 if sb.len() == 4 => {
154                                    max_pdu = u32::from_be_bytes([sb[0], sb[1], sb[2], sb[3]])
155                                }
156                                0x52 => implementation = uid(sb).ok(),
157                                _ => {}
158                            }
159                        }
160                    }
161                    _ => {}
162                }
163            }
164            ensure!(app_ok, "unsupported application context");
165            if kind == 0x01 {
166                ensure!(contexts.len() <= 128, "too many presentation contexts");
167                Pdu::AssociateRq(AssociateRq {
168                    called: ae(&b[4..20]),
169                    calling: ae(&b[20..36]),
170                    contexts,
171                    max_pdu,
172                    implementation,
173                })
174            } else {
175                Pdu::AssociateAc(accepted, max_pdu)
176            }
177        }
178        0x03 => {
179            ensure!(b.len() >= 4, "short A-ASSOCIATE-RJ");
180            Pdu::AssociateRj {
181                result: b[1],
182                source: b[2],
183                reason: b[3],
184            }
185        }
186        0x04 => {
187            let mut pdvs = Vec::new();
188            let mut i = 0;
189            while i < b.len() {
190                ensure!(i + 6 <= b.len(), "truncated PDV");
191                let l = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize;
192                ensure!(l >= 2 && i + 4 + l <= b.len(), "PDV longer than its PDU");
193                pdvs.push((b[i + 4], b[i + 5], b[i + 6..i + 4 + l].to_vec()));
194                i += 4 + l;
195            }
196            Pdu::Data(pdvs)
197        }
198        0x05 => Pdu::ReleaseRq,
199        0x06 => Pdu::ReleaseRp,
200        0x07 => {
201            ensure!(b.len() >= 4, "short A-ABORT");
202            Pdu::Abort {
203                source: b[2],
204                reason: b[3],
205            }
206        }
207        other => bail!("unknown PDU type {other:#04x}"),
208    })
209}
210
211fn item(t: u8, body: &[u8]) -> Vec<u8> {
212    let mut v = vec![t, 0];
213    v.extend_from_slice(&(body.len() as u16).to_be_bytes());
214    v.extend_from_slice(body);
215    v
216}
217
218fn ae_field(s: &str) -> [u8; 16] {
219    let mut f = [b' '; 16];
220    for (i, b) in s.bytes().take(16).enumerate() {
221        f[i] = b;
222    }
223    f
224}
225
226fn user_info(max_pdu: u32) -> Vec<u8> {
227    let mut ui = item(0x51, &max_pdu.to_be_bytes());
228    ui.extend(item(0x52, IMPLEMENTATION_CLASS.as_bytes()));
229    ui.extend(item(0x55, IMPLEMENTATION_VERSION.as_bytes()));
230    item(0x50, &ui)
231}
232
233fn frame(kind: u8, body: Vec<u8>) -> Vec<u8> {
234    let mut v = vec![kind, 0];
235    v.extend_from_slice(&(body.len() as u32).to_be_bytes());
236    v.extend(body);
237    v
238}
239
240fn assoc_head(called: &str, calling: &str) -> Vec<u8> {
241    let mut b = vec![0, 1, 0, 0];
242    b.extend_from_slice(&ae_field(called));
243    b.extend_from_slice(&ae_field(calling));
244    b.extend_from_slice(&[0u8; 32]);
245    b.extend(item(0x10, APPLICATION_CONTEXT.as_bytes()));
246    b
247}
248
249pub fn encode(p: &Pdu, called: &str, calling: &str) -> Vec<u8> {
250    match p {
251        Pdu::AssociateRq(rq) => {
252            let mut b = assoc_head(&rq.called, &rq.calling);
253            for c in &rq.contexts {
254                let mut pc = vec![c.id, 0, 0, 0];
255                pc.extend(item(0x30, c.abstract_syntax.as_bytes()));
256                for ts in &c.transfer_syntaxes {
257                    pc.extend(item(0x40, ts.as_bytes()));
258                }
259                b.extend(item(0x20, &pc));
260            }
261            b.extend(user_info(MAX_PDU));
262            frame(0x01, b)
263        }
264        Pdu::AssociateAc(results, _) => {
265            let mut b = assoc_head(called, calling);
266            for (id, result, ts) in results {
267                let mut pc = vec![*id, 0, *result, 0];
268                pc.extend(item(0x40, ts.as_bytes()));
269                b.extend(item(0x21, &pc));
270            }
271            b.extend(user_info(MAX_PDU));
272            frame(0x02, b)
273        }
274        Pdu::AssociateRj {
275            result,
276            source,
277            reason,
278        } => frame(0x03, vec![0, *result, *source, *reason]),
279        Pdu::Data(pdvs) => {
280            let mut b = Vec::new();
281            for (id, flags, bytes) in pdvs {
282                b.extend_from_slice(&((bytes.len() + 2) as u32).to_be_bytes());
283                b.push(*id);
284                b.push(*flags);
285                b.extend_from_slice(bytes);
286            }
287            frame(0x04, b)
288        }
289        Pdu::ReleaseRq => frame(0x05, vec![0; 4]),
290        Pdu::ReleaseRp => frame(0x06, vec![0; 4]),
291        Pdu::Abort { source, reason } => frame(0x07, vec![0, 0, *source, *reason]),
292    }
293}
294
295/// P-DATA-TF PDUs carrying one message part (command or dataset), fragmented to fit the
296/// peer's maximum PDU length.
297pub fn data_pdus(context: u8, command: bool, bytes: &[u8], peer_max: u32) -> Vec<Vec<u8>> {
298    let max = if peer_max == 0 {
299        MAX_PDU
300    } else {
301        peer_max.min(MAX_PDU)
302    } as usize;
303    let chunk = max.saturating_sub(6).max(256);
304    let mut out = Vec::new();
305    let pieces: Vec<&[u8]> = if bytes.is_empty() {
306        vec![&[][..]]
307    } else {
308        bytes.chunks(chunk).collect()
309    };
310    let n = pieces.len();
311    for (i, piece) in pieces.into_iter().enumerate() {
312        let flags = (command as u8) | if i + 1 == n { 0x02 } else { 0 };
313        out.push(encode(
314            &Pdu::Data(vec![(context, flags, piece.to_vec())]),
315            "",
316            "",
317        ));
318    }
319    out
320}
321
322/// A command set (group 0000) with its group length, Implicit VR Little Endian.
323pub fn command(fields: &[(u32, Value)]) -> Vec<u8> {
324    let mut m = Map::new();
325    for (tag, v) in fields {
326        m.insert(dataset::tag_key(*tag), v.clone());
327    }
328    let body = dataset::encode(&m, IMPLICIT_LE).expect("command sets encode");
329    let mut out = Vec::new();
330    out.extend_from_slice(&0u16.to_le_bytes());
331    out.extend_from_slice(&0u16.to_le_bytes());
332    out.extend_from_slice(&4u32.to_le_bytes());
333    out.extend_from_slice(&(body.len() as u32).to_le_bytes());
334    out.extend(body);
335    out
336}
337
338pub fn us(v: u16) -> Value {
339    json!({"vr": "US", "Value": [v]})
340}
341
342pub fn ui(v: &str) -> Value {
343    json!({"vr": "UI", "Value": [v]})
344}
345
346/// A received DIMSE message: its command set and, when the command says one follows, the
347/// dataset bytes, both reassembled from fragments.
348#[derive(Default, Debug)]
349pub struct Assembly {
350    pub context: u8,
351    pub command: Vec<u8>,
352    pub command_done: bool,
353    pub dataset: Vec<u8>,
354    pub dataset_done: bool,
355}
356
357pub struct Message {
358    pub context: u8,
359    pub command: Map<String, Value>,
360    pub dataset: Option<Vec<u8>>,
361}
362
363impl Assembly {
364    /// Feed PDVs; returns a message once the command and (if announced) the dataset are whole.
365    pub fn feed(&mut self, pdvs: Vec<(u8, u8, Vec<u8>)>) -> Result<Option<Message>> {
366        for (ctx, flags, bytes) in pdvs {
367            self.context = ctx;
368            let is_command = flags & 1 == 1;
369            let last = flags & 2 == 2;
370            if is_command {
371                ensure!(
372                    !self.command_done,
373                    "command fragment after the command ended"
374                );
375                ensure!(
376                    self.command.len() + bytes.len() <= 64 * 1024,
377                    "command set too large"
378                );
379                self.command.extend(bytes);
380                self.command_done = last;
381            } else {
382                ensure!(self.command_done, "dataset before its command");
383                ensure!(
384                    self.dataset.len() + bytes.len() <= dataset::MAX_DATASET,
385                    "dataset over 16 MiB"
386                );
387                self.dataset.extend(bytes);
388                self.dataset_done = last;
389            }
390        }
391        if !self.command_done {
392            return Ok(None);
393        }
394        let cmd = dataset::decode(&self.command, IMPLICIT_LE)?;
395        let has_dataset = cmd
396            .get("00000800")
397            .and_then(|v| v["Value"][0].as_u64())
398            .is_some_and(|t| t != NO_DATASET as u64);
399        if has_dataset && !self.dataset_done {
400            return Ok(None);
401        }
402        let done = std::mem::take(self);
403        Ok(Some(Message {
404            context: done.context,
405            command: cmd,
406            dataset: has_dataset.then_some(done.dataset),
407        }))
408    }
409}
410
411pub fn field_u16(cmd: &Map<String, Value>, tag: u32) -> Option<u16> {
412    cmd.get(&dataset::tag_key(tag))
413        .and_then(|v| v["Value"][0].as_u64())
414        .map(|v| v as u16)
415}
416
417pub fn field_str(cmd: &Map<String, Value>, tag: u32) -> Option<String> {
418    dataset::text(cmd, tag)
419}