Merge pull request #279 from image-rs/seeking-multiple-times
Seeking multiple times
This commit is contained in:
6 files changed
+265
-18
No files matched your search
@@ -0,0 +1,169 @@
|
|||||||
|
use crate::{tags::IfdPointer, TiffError, TiffFormatError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// The IFD structure of a TIFF file should be limited to a forest of entries, and a tree when we
|
||||||
|
/// only consider having a primary IFD and its children. There is up to one primary child of a node
|
||||||
|
/// that is the `next` entry in an IFD itself. Since we offer iteration over this chain we want to
|
||||||
|
/// detect when a new edge would introduce a cycle, such that callers can be guaranteed to
|
||||||
|
/// terminate iteration at some point even in malicious images.
|
||||||
|
///
|
||||||
|
/// However, we are not necessarily visiting the IFDs in their topological order.
|
||||||
|
///
|
||||||
|
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
|
||||||
|
/// Then, when we insert a new edge we check if they belong to the same component.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct IfdCycles {
|
||||||
|
/// The root of each component union.
|
||||||
|
component_union: HashMap<ComponentId, ComponentId>,
|
||||||
|
links: HashMap<IfdPointer, u64>,
|
||||||
|
chains: HashMap<IfdPointer, ComponentId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
struct ComponentId(u64);
|
||||||
|
|
||||||
|
impl IfdCycles {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
IfdCycles::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_next(
|
||||||
|
&mut self,
|
||||||
|
from: IfdPointer,
|
||||||
|
to: Option<IfdPointer>,
|
||||||
|
) -> Result<bool, TiffError> {
|
||||||
|
let to_offset = to.map_or(0, |p| p.0);
|
||||||
|
|
||||||
|
// For some reason we got
|
||||||
|
match self.links.get(&from) {
|
||||||
|
Some(existing) if *existing == to_offset => return Ok(false),
|
||||||
|
// We got here twice with two different reads of the same IFD. That is .. unusual and
|
||||||
|
// we interpret it as a cycle.
|
||||||
|
Some(_) => return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)),
|
||||||
|
None => self.links.insert(from, to_offset),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.ensure_node(from);
|
||||||
|
|
||||||
|
if let Some(to) = to {
|
||||||
|
self.ensure_node(to);
|
||||||
|
|
||||||
|
let parent = self.nominal_component(from);
|
||||||
|
let child = self.nominal_component(to);
|
||||||
|
|
||||||
|
if parent == child {
|
||||||
|
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component_union.insert(child, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_node(&mut self, ifd: IfdPointer) {
|
||||||
|
self.chains.entry(ifd).or_insert_with(|| {
|
||||||
|
let id = ComponentId(self.component_union.len() as u64);
|
||||||
|
self.component_union.insert(id, id);
|
||||||
|
id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nominal_component(&mut self, node: IfdPointer) -> ComponentId {
|
||||||
|
let id = self.chains[&node];
|
||||||
|
|
||||||
|
let nomimal = {
|
||||||
|
let mut iter = id;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let parent = self.component_union[&iter];
|
||||||
|
|
||||||
|
if parent == iter {
|
||||||
|
break parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
iter = parent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if nomimal != id {
|
||||||
|
// Compress.
|
||||||
|
let mut iter = id;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let parent = self.component_union[&iter];
|
||||||
|
|
||||||
|
if parent == iter {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component_union.insert(iter, nomimal);
|
||||||
|
iter = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nomimal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycles_are_detected() {
|
||||||
|
let mut cycles = IfdCycles::new();
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x800)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x800), Some(IfdPointer(0x20)))
|
||||||
|
.expect_err("cycle must be detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reflective_cycle() {
|
||||||
|
let mut cycles = IfdCycles::new();
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x20)))
|
||||||
|
.expect_err("self-referential cycle must be detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn late_cycle() {
|
||||||
|
let mut cycles = IfdCycles::new();
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x60)))
|
||||||
|
.expect_err("non-existing link is valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn odd_cycle() {
|
||||||
|
let mut cycles = IfdCycles::new();
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
|
||||||
|
.expect("non-existing link is valid");
|
||||||
|
|
||||||
|
cycles
|
||||||
|
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x80)))
|
||||||
|
.expect_err("non-existing link is valid");
|
||||||
|
}
|
||||||
+31
-18
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::BTreeMap;
|
||||||
use std::io::{self, Read, Seek};
|
use std::io::{self, Read, Seek};
|
||||||
|
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
@@ -14,6 +14,7 @@ use half::f16;
|
|||||||
use self::image::Image;
|
use self::image::Image;
|
||||||
use self::stream::{ByteOrder, EndianReader};
|
use self::stream::{ByteOrder, EndianReader};
|
||||||
|
|
||||||
|
mod cycles;
|
||||||
pub mod ifd;
|
pub mod ifd;
|
||||||
mod image;
|
mod image;
|
||||||
mod stream;
|
mod stream;
|
||||||
@@ -263,9 +264,12 @@ where
|
|||||||
reader: EndianReader<R>,
|
reader: EndianReader<R>,
|
||||||
bigtiff: bool,
|
bigtiff: bool,
|
||||||
limits: Limits,
|
limits: Limits,
|
||||||
|
current_ifd: Option<IfdPointer>,
|
||||||
next_ifd: Option<IfdPointer>,
|
next_ifd: Option<IfdPointer>,
|
||||||
|
/// The IFDs we visited already in this chain of IFDs.
|
||||||
ifd_offsets: Vec<IfdPointer>,
|
ifd_offsets: Vec<IfdPointer>,
|
||||||
seen_ifds: HashSet<IfdPointer>,
|
/// Map from the ifd into the `ifd_offsets` ordered list.
|
||||||
|
seen_ifds: cycles::IfdCycles,
|
||||||
image: Image,
|
image: Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,9 +499,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
}
|
}
|
||||||
.map(IfdPointer);
|
.map(IfdPointer);
|
||||||
|
|
||||||
let mut seen_ifds = HashSet::new();
|
let current_ifd = *next_ifd.as_ref().unwrap();
|
||||||
seen_ifds.insert(*next_ifd.as_ref().unwrap());
|
let ifd_offsets = vec![current_ifd];
|
||||||
let ifd_offsets = vec![*next_ifd.as_ref().unwrap()];
|
|
||||||
|
|
||||||
let mut decoder = Decoder {
|
let mut decoder = Decoder {
|
||||||
reader,
|
reader,
|
||||||
@@ -505,7 +508,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
limits: Default::default(),
|
limits: Default::default(),
|
||||||
next_ifd,
|
next_ifd,
|
||||||
ifd_offsets,
|
ifd_offsets,
|
||||||
seen_ifds,
|
current_ifd: None,
|
||||||
|
seen_ifds: cycles::IfdCycles::new(),
|
||||||
image: Image {
|
image: Image {
|
||||||
ifd: None,
|
ifd: None,
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -542,6 +546,11 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self.image().colortype()
|
self.image().colortype()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The offset of the directory representing the current image.
|
||||||
|
pub fn ifd_pointer(&mut self) -> Option<IfdPointer> {
|
||||||
|
self.current_ifd
|
||||||
|
}
|
||||||
|
|
||||||
fn image(&self) -> &Image {
|
fn image(&self) -> &Image {
|
||||||
&self.image
|
&self.image
|
||||||
}
|
}
|
||||||
@@ -552,6 +561,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
if ifd_index >= self.ifd_offsets.len() {
|
if ifd_index >= self.ifd_offsets.len() {
|
||||||
// We possibly need to load in the next IFD
|
// We possibly need to load in the next IFD
|
||||||
if self.next_ifd.is_none() {
|
if self.next_ifd.is_none() {
|
||||||
|
self.current_ifd = None;
|
||||||
|
|
||||||
return Err(TiffError::FormatError(
|
return Err(TiffError::FormatError(
|
||||||
TiffFormatError::ImageFileDirectoryNotFound,
|
TiffFormatError::ImageFileDirectoryNotFound,
|
||||||
));
|
));
|
||||||
@@ -574,6 +585,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
// If the index is within the list of ifds then we can load the selected image/IFD
|
// If the index is within the list of ifds then we can load the selected image/IFD
|
||||||
if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) {
|
if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) {
|
||||||
let ifd = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?;
|
let ifd = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?;
|
||||||
|
self.next_ifd = ifd.next();
|
||||||
|
self.current_ifd = Some(*ifd_offset);
|
||||||
self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?;
|
self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -591,20 +604,20 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ifd = Self::read_ifd(
|
let ifd_offset = self.next_ifd.take().unwrap();
|
||||||
&mut self.reader,
|
let ifd = Self::read_ifd(&mut self.reader, self.bigtiff, ifd_offset)?;
|
||||||
self.bigtiff,
|
|
||||||
self.next_ifd.take().unwrap(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(next) = ifd.next() {
|
// Ensure this walk does not get us into a cycle.
|
||||||
if !self.seen_ifds.insert(next) {
|
self.seen_ifds.insert_next(ifd_offset, ifd.next())?;
|
||||||
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
|
|
||||||
}
|
// Extend the list of known IFD offsets in this chain, if needed.
|
||||||
self.next_ifd = Some(next);
|
if self.ifd_offsets.last().copied() == self.current_ifd {
|
||||||
self.ifd_offsets.push(next);
|
self.ifd_offsets.push(ifd_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.current_ifd = Some(ifd_offset);
|
||||||
|
self.next_ifd = ifd.next();
|
||||||
|
|
||||||
Ok(ifd)
|
Ok(ifd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +635,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self.next_ifd.is_some()
|
self.next_ifd.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the byte_order
|
/// Returns the byte_order of the file.
|
||||||
pub fn byte_order(&self) -> ByteOrder {
|
pub fn byte_order(&self) -> ByteOrder {
|
||||||
self.reader.byte_order
|
self.reader.byte_order
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,4 +158,24 @@ mod tests {
|
|||||||
u16::MAX.into()
|
u16::MAX.into()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iteration_order() {
|
||||||
|
let mut dir = Directory::empty(IfdPointer(0));
|
||||||
|
assert_eq!(dir.len(), 0);
|
||||||
|
|
||||||
|
let fake_entry = Entry::new_u64(crate::tags::Type::BYTE, 0, [0; 8]);
|
||||||
|
dir.extend((0..32).map(|i| {
|
||||||
|
let tag = Tag::Unknown(i);
|
||||||
|
let entry = fake_entry.clone();
|
||||||
|
(tag, entry)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let iter_order: Vec<u16> = dir.iter().map(|(tag, _e)| tag.to_u16()).collect();
|
||||||
|
assert_eq!(
|
||||||
|
iter_order,
|
||||||
|
(0..32).collect::<Vec<_>>(),
|
||||||
|
"Tags must be in ascending order according to the specification"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
subsubifd.tif:
|
||||||
|
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
||||||
|
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
||||||
|
license: CC0
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use tiff::decoder::Decoder;
|
||||||
|
|
||||||
|
// This file has the following IFD structure:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// IFD -> IFD -> IFD
|
||||||
|
// |-> IFD
|
||||||
|
// |-> IFD -> IFD
|
||||||
|
// | \-> IFD -> IFD
|
||||||
|
// \-> IFD
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// where the main chain of IFD has 3 images, the second of which has several sub-IFDs and some have
|
||||||
|
// more nested IFDs.
|
||||||
|
const TEST_IMAGE_SUBIFD: &str = "./tests/subsubifds.tif";
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_seek_chain() {
|
||||||
|
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
|
||||||
|
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
|
||||||
|
|
||||||
|
// Remember the offsets in the first iteration.
|
||||||
|
let offset0 = decoder.ifd_pointer().expect("First IFD pointer not found");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
let offset1 = decoder.ifd_pointer().expect("Second IFD pointer not found");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
let offset2 = decoder.ifd_pointer().expect("Third IFD pointer not found");
|
||||||
|
assert!(!decoder.more_images());
|
||||||
|
|
||||||
|
// Ensure we can seek, to do this again.
|
||||||
|
decoder
|
||||||
|
.seek_to_image(0)
|
||||||
|
.expect("Failed to seek to first image");
|
||||||
|
assert_eq!(Some(offset0), decoder.ifd_pointer());
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(Some(offset1), decoder.ifd_pointer());
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(Some(offset2), decoder.ifd_pointer());
|
||||||
|
assert!(!decoder.more_images());
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in new issue
Block a user