wsi-rs 0.5.2

wsi-rs whole-slide image reader
Documentation
use dicom_core::{Tag, VR};
use dicom_object::file::ReadPreamble;

use super::*;

const MAX_FILE_META_BYTES: u32 = 1024 * 1024;
const MAX_FILE_META_ELEMENTS: usize = 128;
const MAX_METADATA_ELEMENT_BYTES: u32 = 16 * 1024 * 1024;
const MAX_METADATA_VALUE_BYTES: u64 = 128 * 1024 * 1024;
const MAX_METADATA_TOKENS: usize = 2_000_000;
const MAX_METADATA_SEQUENCE_DEPTH: usize = 64;
const MAX_TRANSFER_SYNTAX_UID_BYTES: u32 = 128;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum DicomPixelDataLocation {
    Native { value_offset: u64, value_len: u32 },
    Encapsulated,
    Missing,
}

pub(super) struct OpenedDicomMetadata {
    pub(super) object: DefaultDicomObject,
    pub(super) pixel_data: DicomPixelDataLocation,
    pub(super) source_identity: FileIdentity,
}

pub(super) fn open_metadata_object_until(
    path: &Path,
    stop_tag: Tag,
) -> Result<OpenedDicomMetadata, WsiError> {
    let mut file = File::open(path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    })?;
    let pixel_data = preflight_dicom_metadata(&mut file, path)?;
    let source_identity = FileIdentity::from_open_file(path, &file)?;
    file.seek(SeekFrom::Start(0))
        .map_err(|source| WsiError::IoWithPath {
            source: Arc::new(source),
            path: path.to_path_buf(),
        })?;
    let object = OpenFileOptions::new()
        .read_until(stop_tag)
        .read_preamble(ReadPreamble::Auto)
        .from_reader(file)
        .map_err(|source| invalid_slide(path, format!("cannot parse DICOM metadata: {source}")))?;
    Ok(OpenedDicomMetadata {
        object,
        pixel_data,
        source_identity,
    })
}

fn preflight_dicom_metadata(
    file: &mut File,
    path: &Path,
) -> Result<DicomPixelDataLocation, WsiError> {
    let transfer_syntax_uid = preflight_file_meta(file, path)?;
    let transfer_syntax = TransferSyntaxRegistry
        .get(&transfer_syntax_uid)
        .ok_or_else(|| {
            invalid_slide(
                path,
                format!("unsupported transfer syntax {transfer_syntax_uid}"),
            )
        })?;
    let pixel_data = {
        let mut reader =
            LazyDataSetReader::new_with_ts(&mut *file, transfer_syntax).map_err(|source| {
                invalid_slide(
                    path,
                    format!("cannot initialize DICOM metadata preflight: {source}"),
                )
            })?;
        let mut token_count = 0usize;
        let mut sequence_depth = 0usize;
        let mut declared_value_bytes = 0u64;
        let mut pixel_data = DicomPixelDataLocation::Missing;

        while let Some(token) = reader.advance() {
            token_count = token_count.saturating_add(1);
            if token_count > MAX_METADATA_TOKENS {
                return Err(invalid_slide(
                    path,
                    format!("DICOM metadata exceeds the {MAX_METADATA_TOKENS}-token limit"),
                ));
            }
            let token = token.map_err(|source| {
                invalid_slide(path, format!("cannot preflight DICOM metadata: {source}"))
            })?;
            match token {
                LazyDataToken::ElementHeader(header) => {
                    if header.tag == tags::PIXEL_DATA {
                        let value = reader
                            .advance()
                            .ok_or_else(|| invalid_slide(path, "DICOM Pixel Data has no value"))?
                            .map_err(|source| {
                                invalid_slide(
                                    path,
                                    format!("cannot preflight DICOM Pixel Data: {source}"),
                                )
                            })?;
                        pixel_data = match value {
                            LazyDataToken::LazyValue {
                                header: value_header,
                                decoder,
                            } => DicomPixelDataLocation::Native {
                                value_offset: decoder.position(),
                                value_len: value_header.len.0,
                            },
                            LazyDataToken::PixelSequenceStart => {
                                DicomPixelDataLocation::Encapsulated
                            }
                            _ => {
                                return Err(invalid_slide(
                                    path,
                                    "DICOM Pixel Data has an unsupported value encoding",
                                ));
                            }
                        };
                        break;
                    }
                    if header.len.0 > MAX_METADATA_ELEMENT_BYTES {
                        return Err(invalid_slide(
                            path,
                            format!(
                                "DICOM 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_slide(path, "DICOM metadata byte count overflow"))?;
                    if declared_value_bytes > MAX_METADATA_VALUE_BYTES {
                        return Err(invalid_slide(
                            path,
                            format!(
                                "DICOM metadata declares more than the {MAX_METADATA_VALUE_BYTES}-byte cumulative value limit"
                            ),
                        ));
                    }
                }
                LazyDataToken::SequenceStart { tag, .. } => {
                    if tag == tags::PIXEL_DATA {
                        pixel_data = DicomPixelDataLocation::Encapsulated;
                        break;
                    }
                    sequence_depth = sequence_depth.saturating_add(1);
                    if sequence_depth > MAX_METADATA_SEQUENCE_DEPTH {
                        return Err(invalid_slide(
                            path,
                            format!(
                                "DICOM metadata sequence nesting exceeds the {MAX_METADATA_SEQUENCE_DEPTH}-level limit"
                            ),
                        ));
                    }
                }
                LazyDataToken::PixelSequenceStart => {
                    pixel_data = DicomPixelDataLocation::Encapsulated;
                    break;
                }
                LazyDataToken::SequenceEnd => {
                    sequence_depth = sequence_depth.saturating_sub(1);
                }
                lazy @ (LazyDataToken::LazyValue { .. } | LazyDataToken::LazyItemValue { .. }) => {
                    lazy.skip().map_err(|source| {
                        invalid_slide(path, format!("cannot skip DICOM metadata value: {source}"))
                    })?;
                }
                LazyDataToken::ItemStart { .. } | LazyDataToken::ItemEnd => {}
                _ => {
                    return Err(invalid_slide(
                        path,
                        "DICOM metadata parser returned an unsupported token",
                    ));
                }
            }
        }

        if pixel_data == DicomPixelDataLocation::Missing && sequence_depth != 0 {
            return Err(invalid_slide(
                path,
                "DICOM metadata ended inside an unterminated sequence",
            ));
        }
        pixel_data
    };

    if let DicomPixelDataLocation::Native {
        value_offset,
        value_len,
    } = pixel_data
    {
        let value_end = value_offset
            .checked_add(u64::from(value_len))
            .ok_or_else(|| invalid_slide(path, "DICOM Pixel Data range overflow"))?;
        let file_len = file
            .seek(SeekFrom::End(0))
            .map_err(|source| io_error(path, source))?;
        if value_end > file_len {
            return Err(invalid_slide(
                path,
                "DICOM Pixel Data extends beyond the source file",
            ));
        }
    }
    Ok(pixel_data)
}

pub(super) fn preflight_file_meta(
    reader: &mut (impl Read + Seek),
    path: &Path,
) -> Result<String, WsiError> {
    reader
        .seek(SeekFrom::Start(0))
        .map_err(|source| io_error(path, source))?;
    let mut magic = [0u8; 4];
    read_exact(reader, path, &mut magic, "DICOM magic code")?;
    if &magic != b"DICM" {
        reader
            .seek(SeekFrom::Start(128))
            .map_err(|source| io_error(path, source))?;
        read_exact(reader, path, &mut magic, "DICOM magic code")?;
    }
    if &magic != b"DICM" {
        return Err(invalid_slide(path, "missing DICOM file magic code"));
    }

    let group_length_header = read_explicit_header(reader, 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_slide(
            path,
            "invalid File Meta Information Group Length element",
        ));
    }
    let mut length_bytes = [0u8; 4];
    read_exact(reader, 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_slide(
            path,
            format!(
                "DICOM 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 = 0usize;
    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_slide(
                path,
                format!("DICOM file meta group exceeds the {MAX_FILE_META_ELEMENTS}-element limit"),
            ));
        }
        let header = read_explicit_header(reader, path)?;
        let encoded_len = header
            .header_len
            .checked_add(u64::from(header.value_len))
            .ok_or_else(|| invalid_slide(path, "DICOM file meta element length overflow"))?;
        if header.tag.group() != 0x0002 || encoded_len > remaining {
            return Err(invalid_slide(
                path,
                format!(
                    "DICOM 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_slide(
                    path,
                    "DICOM transfer syntax UID has an invalid declared length",
                ));
            }
            let mut value = vec![0u8; header.value_len as usize];
            read_exact(reader, path, &mut value, "DICOM transfer syntax UID")?;
            let value = String::from_utf8(value)
                .map_err(|_| invalid_slide(path, "DICOM transfer syntax UID is not UTF-8"))?;
            transfer_syntax_uid = Some(
                value
                    .trim_end_matches(|character: char| {
                        character.is_whitespace() || character == '\0'
                    })
                    .to_string(),
            );
        } else {
            reader
                .seek(SeekFrom::Current(i64::from(header.value_len)))
                .map_err(|source| io_error(path, source))?;
        }
        remaining -= encoded_len;
    }

    let dataset_offset = reader
        .stream_position()
        .map_err(|source| io_error(path, source))?;
    let file_len = reader
        .seek(SeekFrom::End(0))
        .map_err(|source| io_error(path, source))?;
    if dataset_offset > file_len {
        return Err(invalid_slide(
            path,
            "DICOM file meta group extends beyond the source file",
        ));
    }
    reader
        .seek(SeekFrom::Start(dataset_offset))
        .map_err(|source| io_error(path, source))?;

    transfer_syntax_uid
        .filter(|uid| !uid.is_empty())
        .ok_or_else(|| invalid_slide(path, "DICOM file meta group has no transfer syntax UID"))
}

struct ExplicitHeader {
    tag: Tag,
    vr: VR,
    value_len: u32,
    header_len: u64,
}

fn read_explicit_header(
    reader: &mut (impl Read + Seek),
    path: &Path,
) -> Result<ExplicitHeader, WsiError> {
    let mut base = [0u8; 8];
    read_exact(reader, path, &mut base, "DICOM 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_slide(path, format!("invalid VR in DICOM 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_slide(
                path,
                format!("invalid reserved bytes in DICOM file meta element {tag}"),
            ));
        }
        let mut length = [0u8; 4];
        read_exact(reader, path, &mut length, "DICOM 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 read_exact(
    reader: &mut impl Read,
    path: &Path,
    bytes: &mut [u8],
    context: &str,
) -> Result<(), WsiError> {
    reader
        .read_exact(bytes)
        .map_err(|source| invalid_slide(path, format!("cannot read {context}: {source}")))
}

fn io_error(path: &Path, source: std::io::Error) -> WsiError {
    WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    }
}