use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use dicom_core::{Tag, VR};
use dicom_dictionary_std::tags;
use dicom_encoding::transfer_syntax::{TransferSyntax, TransferSyntaxIndex};
use dicom_object::{file::ReadPreamble, DefaultDicomObject, OpenFileOptions};
use dicom_parser::dataset::{lazy_read::LazyDataSetReader, LazyDataToken};
use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
use crate::{Error, Result};
const MAX_FILE_META_BYTES: u32 = 1024 * 1024;
const MAX_FILE_META_ELEMENTS: usize = 128;
pub const MAX_METADATA_ELEMENT_BYTES: u32 = 16 * 1024 * 1024;
pub const MAX_METADATA_VALUE_BYTES: u64 = 128 * 1024 * 1024;
const MAX_METADATA_TOKENS: usize = 2_000_000;
pub const MAX_METADATA_SEQUENCE_DEPTH: usize = 64;
const MAX_TRANSFER_SYNTAX_UID_BYTES: u32 = 128;
pub fn open_metadata_object(path: &Path) -> Result<DefaultDicomObject> {
let mut file = std::fs::File::open(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
let transfer_syntax_uid = preflight_file_meta(&mut file, path)?;
let transfer_syntax = TransferSyntaxRegistry
.get(&transfer_syntax_uid)
.ok_or_else(|| {
invalid_metadata(
path,
format!("unsupported transfer syntax {transfer_syntax_uid}"),
)
})?;
preflight_data_set(&mut file, path, transfer_syntax)?;
file.seek(SeekFrom::Start(0)).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
OpenFileOptions::new()
.read_until(tags::FLOAT_PIXEL_DATA)
.read_preamble(ReadPreamble::Always)
.from_reader(file)
.map_err(|source| Error::DicomRead {
path: path.to_path_buf(),
source: Box::new(source),
})
}
fn preflight_file_meta(file: &mut std::fs::File, path: &Path) -> Result<String> {
file.seek(SeekFrom::Start(128))
.map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
let mut magic = [0_u8; 4];
read_metadata_exact(file, path, &mut magic, "DICOM magic code")?;
if &magic != b"DICM" {
return Err(invalid_metadata(path, "missing DICOM file preamble"));
}
let group_length_header = read_explicit_header(file, path)?;
if group_length_header.tag != tags::FILE_META_INFORMATION_GROUP_LENGTH
|| group_length_header.vr != VR::UL
|| group_length_header.value_len != 4
{
return Err(invalid_metadata(
path,
"invalid File Meta Information Group Length element",
));
}
let mut length_bytes = [0_u8; 4];
read_metadata_exact(file, path, &mut length_bytes, "file meta group length")?;
let group_length = u32::from_le_bytes(length_bytes);
if group_length > MAX_FILE_META_BYTES {
return Err(invalid_metadata(
path,
format!(
"file meta group is {group_length} bytes, exceeding the {MAX_FILE_META_BYTES}-byte limit"
),
));
}
let mut remaining = u64::from(group_length);
let mut element_count = 0_usize;
let mut transfer_syntax_uid = None;
while remaining > 0 {
element_count = element_count.saturating_add(1);
if element_count > MAX_FILE_META_ELEMENTS {
return Err(invalid_metadata(
path,
format!("file meta group exceeds the {MAX_FILE_META_ELEMENTS}-element limit"),
));
}
let header = read_explicit_header(file, path)?;
let encoded_len = header
.header_len
.checked_add(u64::from(header.value_len))
.ok_or_else(|| invalid_metadata(path, "file meta element length overflows"))?;
if header.tag.group() != 0x0002 || encoded_len > remaining {
return Err(invalid_metadata(
path,
format!(
"file meta element {} exceeds the declared group boundary",
header.tag
),
));
}
if header.tag == tags::TRANSFER_SYNTAX_UID {
if header.value_len == 0 || header.value_len > MAX_TRANSFER_SYNTAX_UID_BYTES {
return Err(invalid_metadata(
path,
"transfer syntax UID has an invalid declared length",
));
}
let mut value = vec![0_u8; header.value_len as usize];
read_metadata_exact(file, path, &mut value, "transfer syntax UID")?;
let value = String::from_utf8(value).map_err(|_| {
invalid_metadata(path, "transfer syntax UID is not valid ASCII/UTF-8")
})?;
transfer_syntax_uid = Some(
value
.trim_end_matches(|character: char| {
character.is_whitespace() || character == '\0'
})
.to_string(),
);
} else {
file.seek(SeekFrom::Current(i64::from(header.value_len)))
.map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
}
remaining -= encoded_len;
}
let position = file.stream_position().map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
let file_len = file
.metadata()
.map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?
.len();
if position > file_len {
return Err(invalid_metadata(
path,
"file meta group extends beyond the end of the file",
));
}
transfer_syntax_uid
.filter(|uid| !uid.is_empty())
.ok_or_else(|| invalid_metadata(path, "file meta group has no transfer syntax UID"))
}
struct ExplicitHeader {
tag: Tag,
vr: VR,
value_len: u32,
header_len: u64,
}
fn read_explicit_header(file: &mut std::fs::File, path: &Path) -> Result<ExplicitHeader> {
let mut base = [0_u8; 8];
read_metadata_exact(file, path, &mut base, "file meta element header")?;
let tag = Tag(
u16::from_le_bytes([base[0], base[1]]),
u16::from_le_bytes([base[2], base[3]]),
);
let vr = VR::from_binary([base[4], base[5]])
.ok_or_else(|| invalid_metadata(path, format!("invalid VR in file meta element {tag}")))?;
let uses_u32_length = matches!(
vr,
VR::OB
| VR::OD
| VR::OF
| VR::OL
| VR::OV
| VR::OW
| VR::SQ
| VR::UC
| VR::UN
| VR::UR
| VR::UT
);
let (value_len, header_len) = if uses_u32_length {
if base[6..8] != [0, 0] {
return Err(invalid_metadata(
path,
format!("invalid reserved bytes in file meta element {tag}"),
));
}
let mut length = [0_u8; 4];
read_metadata_exact(file, path, &mut length, "file meta element length")?;
(u32::from_le_bytes(length), 12)
} else {
(u32::from(u16::from_le_bytes([base[6], base[7]])), 8)
};
Ok(ExplicitHeader {
tag,
vr,
value_len,
header_len,
})
}
fn preflight_data_set(
file: &mut (impl Read + Seek),
path: &Path,
transfer_syntax: &TransferSyntax,
) -> Result<()> {
let mut reader = LazyDataSetReader::new_with_ts(BufReader::new(file), transfer_syntax)
.map_err(|error| {
invalid_metadata(
path,
format!("could not initialize metadata parser: {error}"),
)
})?;
let mut token_count = 0_usize;
let mut sequence_depth = 0_usize;
let mut declared_value_bytes = 0_u64;
while let Some(token) = reader.advance() {
token_count = token_count.saturating_add(1);
if token_count > MAX_METADATA_TOKENS {
return Err(invalid_metadata(
path,
format!("metadata exceeds the {MAX_METADATA_TOKENS}-token limit"),
));
}
let token = token.map_err(|error| {
invalid_metadata(path, format!("could not preflight metadata: {error}"))
})?;
match token {
LazyDataToken::ElementHeader(header) => {
if is_pixel_value(header.tag) {
return Ok(());
}
if header.len.0 > MAX_METADATA_ELEMENT_BYTES {
return Err(invalid_metadata(
path,
format!(
"metadata element value limit is {MAX_METADATA_ELEMENT_BYTES} bytes, but {} declares {} bytes",
header.tag, header.len.0
),
));
}
declared_value_bytes = declared_value_bytes
.checked_add(u64::from(header.len.0))
.ok_or_else(|| invalid_metadata(path, "metadata byte count overflows"))?;
if declared_value_bytes > MAX_METADATA_VALUE_BYTES {
return Err(invalid_metadata(
path,
format!(
"metadata declares more than the {MAX_METADATA_VALUE_BYTES}-byte cumulative value limit"
),
));
}
}
LazyDataToken::SequenceStart { tag, .. } => {
if is_pixel_value(tag) {
return Ok(());
}
sequence_depth = sequence_depth.saturating_add(1);
if sequence_depth > MAX_METADATA_SEQUENCE_DEPTH {
return Err(invalid_metadata(
path,
format!(
"metadata sequence nesting exceeds the {MAX_METADATA_SEQUENCE_DEPTH}-level limit"
),
));
}
}
LazyDataToken::PixelSequenceStart => return Ok(()),
LazyDataToken::SequenceEnd => {
sequence_depth = sequence_depth.saturating_sub(1);
}
lazy @ (LazyDataToken::LazyValue { .. } | LazyDataToken::LazyItemValue { .. }) => {
lazy.skip().map_err(|error| {
invalid_metadata(path, format!("could not skip metadata value: {error}"))
})?;
}
LazyDataToken::ItemStart { .. } | LazyDataToken::ItemEnd => {}
_ => {
return Err(invalid_metadata(
path,
"metadata parser returned an unsupported token",
));
}
}
}
if sequence_depth != 0 {
return Err(invalid_metadata(
path,
"metadata ended inside an unterminated sequence",
));
}
Ok(())
}
fn is_pixel_value(tag: Tag) -> bool {
matches!(
tag,
tags::FLOAT_PIXEL_DATA | tags::DOUBLE_FLOAT_PIXEL_DATA | tags::PIXEL_DATA
)
}
fn read_metadata_exact(
file: &mut std::fs::File,
path: &Path,
bytes: &mut [u8],
context: &str,
) -> Result<()> {
file.read_exact(bytes)
.map_err(|error| invalid_metadata(path, format!("could not read {context}: {error}")))
}
fn invalid_metadata(path: &Path, reason: impl std::fmt::Display) -> Error {
Error::InvalidInput(format!(
"DICOM metadata preflight failed for {}: {reason}",
path.display()
))
}
#[cfg(test)]
mod tests;