dicom_toolkit_core/
uid.rs1use crate::error::{DcmError, DcmResult};
7use std::fmt;
8
9pub const MAX_UID_LENGTH: usize = 64;
11
12#[derive(Clone, PartialEq, Eq, Hash)]
17pub struct Uid(String);
18
19impl Uid {
20 pub fn new(s: impl Into<String>) -> DcmResult<Self> {
22 let s = s.into();
23 Self::validate(&s)?;
24 Ok(Self(s))
25 }
26
27 pub fn is_valid(s: &str) -> bool {
29 Self::validate(s).is_ok()
30 }
31
32 pub fn from_static(s: &'static str) -> Self {
38 debug_assert!(Self::validate(s).is_ok(), "invalid static UID: {s}");
39 Self(s.to_string())
40 }
41
42 #[allow(dead_code)]
44 pub(crate) fn from_valid(s: String) -> Self {
45 Self(s)
46 }
47
48 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52
53 pub fn generate(root: &str) -> DcmResult<Self> {
58 let uuid_val = uuid::Uuid::new_v4();
59 let uuid_bytes = uuid_val.as_bytes();
61 let num = u128::from_be_bytes(*uuid_bytes);
62 let suffix = num.to_string();
63
64 let uid_str = format!("{root}.{suffix}");
65 if uid_str.len() > MAX_UID_LENGTH {
66 let max_suffix = MAX_UID_LENGTH - root.len() - 1;
68 let uid_str = format!("{root}.{}", &suffix[..max_suffix]);
69 return Self::new(uid_str);
70 }
71 Self::new(uid_str)
72 }
73
74 fn validate(s: &str) -> DcmResult<()> {
76 if s.is_empty() {
77 return Err(DcmError::InvalidUid {
78 reason: "UID must not be empty".into(),
79 });
80 }
81 if s.len() > MAX_UID_LENGTH {
82 return Err(DcmError::InvalidUid {
83 reason: format!(
84 "UID exceeds maximum length of {MAX_UID_LENGTH}: got {}",
85 s.len()
86 ),
87 });
88 }
89 if s.starts_with('.') || s.ends_with('.') {
90 return Err(DcmError::InvalidUid {
91 reason: "UID must not start or end with a dot".into(),
92 });
93 }
94 if s.contains("..") {
95 return Err(DcmError::InvalidUid {
96 reason: "UID must not contain consecutive dots".into(),
97 });
98 }
99 for ch in s.chars() {
100 if !ch.is_ascii_digit() && ch != '.' {
101 return Err(DcmError::InvalidUid {
102 reason: format!("UID contains invalid character: '{ch}'"),
103 });
104 }
105 }
106 for component in s.split('.') {
108 if component.is_empty() {
109 return Err(DcmError::InvalidUid {
110 reason: "UID contains an empty component".into(),
111 });
112 }
113 if component.len() > 1 && component.starts_with('0') {
114 return Err(DcmError::InvalidUid {
115 reason: format!("UID component has leading zero: '{component}'"),
116 });
117 }
118 }
119 Ok(())
120 }
121}
122
123impl fmt::Debug for Uid {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "Uid(\"{}\")", self.0)
126 }
127}
128
129impl fmt::Display for Uid {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 f.write_str(&self.0)
132 }
133}
134
135impl AsRef<str> for Uid {
136 fn as_ref(&self) -> &str {
137 &self.0
138 }
139}
140
141impl TryFrom<String> for Uid {
142 type Error = DcmError;
143 fn try_from(s: String) -> DcmResult<Self> {
144 Self::new(s)
145 }
146}
147
148impl TryFrom<&str> for Uid {
149 type Error = DcmError;
150 fn try_from(s: &str) -> DcmResult<Self> {
151 Self::new(s)
152 }
153}
154
155pub mod sop_class {
161 pub const VERIFICATION: &str = "1.2.840.10008.1.1";
163
164 pub const CT_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.2";
166 pub const ENHANCED_CT_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.2.1";
167 pub const MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4";
168 pub const ENHANCED_MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4.1";
169 pub const ULTRASOUND_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.6.1";
170 pub const SECONDARY_CAPTURE_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.7";
171 pub const DIGITAL_XRAY_IMAGE_STORAGE_FOR_PRESENTATION: &str = "1.2.840.10008.5.1.4.1.1.1.1";
172 pub const DIGITAL_XRAY_IMAGE_STORAGE_FOR_PROCESSING: &str = "1.2.840.10008.5.1.4.1.1.1.1.1";
173 pub const DIGITAL_MAMMOGRAPHY_IMAGE_STORAGE_FOR_PRESENTATION: &str =
174 "1.2.840.10008.5.1.4.1.1.1.2";
175 pub const CR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.1";
176 pub const NM_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.20";
177 pub const PET_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.128";
178 pub const RT_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.481.1";
179 pub const RT_DOSE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.481.2";
180 pub const RT_STRUCTURE_SET_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.481.3";
181 pub const RT_PLAN_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.481.5";
182 pub const XA_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.12.1";
183 pub const VL_PHOTOGRAPHIC_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.77.1.4";
184 pub const VIDEO_ENDOSCOPIC_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.77.1.1.1";
185 pub const ENCAPSULATED_PDF_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.104.1";
186 pub const ENCAPSULATED_CDA_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.104.2";
187 pub const BASIC_TEXT_SR_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.88.11";
188 pub const ENHANCED_SR_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.88.22";
189 pub const COMPREHENSIVE_SR_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.88.33";
190 pub const SEGMENTATION_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.66.4";
191
192 pub const PATIENT_ROOT_QR_FIND: &str = "1.2.840.10008.5.1.4.1.2.1.1";
194 pub const PATIENT_ROOT_QR_MOVE: &str = "1.2.840.10008.5.1.4.1.2.1.2";
195 pub const PATIENT_ROOT_QR_GET: &str = "1.2.840.10008.5.1.4.1.2.1.3";
196 pub const STUDY_ROOT_QR_FIND: &str = "1.2.840.10008.5.1.4.1.2.2.1";
197 pub const STUDY_ROOT_QR_MOVE: &str = "1.2.840.10008.5.1.4.1.2.2.2";
198 pub const STUDY_ROOT_QR_GET: &str = "1.2.840.10008.5.1.4.1.2.2.3";
199
200 pub const MODALITY_WORKLIST_FIND: &str = "1.2.840.10008.5.1.4.31";
202
203 pub const BASIC_FILM_SESSION: &str = "1.2.840.10008.5.1.1.1";
205 pub const BASIC_FILM_BOX: &str = "1.2.840.10008.5.1.1.2";
206 pub const BASIC_GRAYSCALE_IMAGE_BOX: &str = "1.2.840.10008.5.1.1.4";
207 pub const BASIC_COLOR_IMAGE_BOX: &str = "1.2.840.10008.5.1.1.4.1";
208 pub const PRINTER: &str = "1.2.840.10008.5.1.1.16";
209
210 pub const STORAGE_COMMITMENT_PUSH_MODEL: &str = "1.2.840.10008.1.20.1";
212}
213
214pub mod transfer_syntax {
218 pub const IMPLICIT_VR_LITTLE_ENDIAN: &str = "1.2.840.10008.1.2";
219 pub const EXPLICIT_VR_LITTLE_ENDIAN: &str = "1.2.840.10008.1.2.1";
220 pub const EXPLICIT_VR_BIG_ENDIAN: &str = "1.2.840.10008.1.2.2";
221 pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: &str = "1.2.840.10008.1.2.1.99";
222
223 pub const JPEG_BASELINE: &str = "1.2.840.10008.1.2.4.50";
225 pub const JPEG_EXTENDED: &str = "1.2.840.10008.1.2.4.51";
226 pub const JPEG_LOSSLESS_NON_HIERARCHICAL: &str = "1.2.840.10008.1.2.4.57";
227 pub const JPEG_LOSSLESS_NON_HIERARCHICAL_FIRST_ORDER: &str = "1.2.840.10008.1.2.4.70";
228
229 pub const JPEG_LS_LOSSLESS: &str = "1.2.840.10008.1.2.4.80";
231 pub const JPEG_LS_LOSSY: &str = "1.2.840.10008.1.2.4.81";
232
233 pub const JPEG_2000_LOSSLESS: &str = "1.2.840.10008.1.2.4.90";
235 pub const JPEG_2000: &str = "1.2.840.10008.1.2.4.91";
236 pub const HIGH_THROUGHPUT_JPEG_2000_LOSSLESS_ONLY: &str = "1.2.840.10008.1.2.4.201";
237 pub const HIGH_THROUGHPUT_JPEG_2000_RPCL_LOSSLESS_ONLY: &str = "1.2.840.10008.1.2.4.202";
238 pub const HIGH_THROUGHPUT_JPEG_2000: &str = "1.2.840.10008.1.2.4.203";
239
240 pub const RLE_LOSSLESS: &str = "1.2.840.10008.1.2.5";
242
243 pub const ENCAPSULATED_UNCOMPRESSED: &str = "1.2.840.10008.1.2.1.98";
245}
246
247pub const DCMTK_RS_UID_ROOT: &str = "1.2.826.0.1.3680043.8.498";
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn valid_uid() {
256 assert!(Uid::new("1.2.840.10008.1.1").is_ok());
257 assert!(Uid::new("1.2.3").is_ok());
258 assert!(Uid::new("0").is_ok());
259 }
260
261 #[test]
262 fn invalid_uid_empty() {
263 assert!(Uid::new("").is_err());
264 }
265
266 #[test]
267 fn invalid_uid_too_long() {
268 let long = "1.".repeat(33);
269 assert!(Uid::new(&long[..long.len() - 1]).is_err());
270 }
271
272 #[test]
273 fn invalid_uid_leading_dot() {
274 assert!(Uid::new(".1.2.3").is_err());
275 }
276
277 #[test]
278 fn invalid_uid_trailing_dot() {
279 assert!(Uid::new("1.2.3.").is_err());
280 }
281
282 #[test]
283 fn invalid_uid_consecutive_dots() {
284 assert!(Uid::new("1.2..3").is_err());
285 }
286
287 #[test]
288 fn invalid_uid_non_numeric() {
289 assert!(Uid::new("1.2.abc").is_err());
290 }
291
292 #[test]
293 fn invalid_uid_leading_zero() {
294 assert!(Uid::new("1.02.3").is_err());
295 }
296
297 #[test]
298 fn from_static_preserves_value() {
299 let uid = Uid::from_static("1.2.840.10008.1.1");
300 assert_eq!(uid.as_str(), "1.2.840.10008.1.1");
301 }
302
303 #[test]
304 fn generate_uid() {
305 let uid = Uid::generate("1.2.3").unwrap();
306 assert!(uid.as_str().starts_with("1.2.3."));
307 assert!(uid.as_str().len() <= MAX_UID_LENGTH);
308 }
309
310 #[test]
311 fn uid_display() {
312 let uid = Uid::new("1.2.840.10008.1.1").unwrap();
313 assert_eq!(uid.to_string(), "1.2.840.10008.1.1");
314 }
315}