From f890d22657afc49f1c912066fb2947368fc2b918 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:57:59 +0100 Subject: [PATCH 1/7] Add EntryBytesReader: impl Read + Seek --- src/decoder/ifd.rs | 2 +- src/decoder/mod.rs | 6 ++ src/decoder/value.rs | 196 +++++++++++++++++++++++++++++++++++++++++++ src/encoder/mod.rs | 3 +- 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/decoder/value.rs diff --git a/src/decoder/ifd.rs b/src/decoder/ifd.rs index 217ad26..a17d645 100644 --- a/src/decoder/ifd.rs +++ b/src/decoder/ifd.rs @@ -393,7 +393,7 @@ impl Entry { self.count } - pub(crate) fn offset(&self) -> &[u8] { + pub(crate) fn offset_raw(&self) -> &[u8; 8] { &self.offset } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 53a64ba..0240126 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -20,8 +20,10 @@ mod image; mod logluv; mod stream; mod tag_reader; +mod value; pub use self::buffer::{DecodingSampleBuffer, DecodingSampleSlice, DecodingSampleType}; +pub use self::value::EntryBytesReader; #[deprecated = "Renamed to `DecodingSampleBuffer`"] pub type DecodingResult = DecodingSampleBuffer; @@ -943,6 +945,10 @@ impl Decoder { Ok(val) } + pub fn read_entry(&mut self, entry: ifd::Entry) -> TiffResult> { + EntryBytesReader::from_entry(&mut self.value_reader, entry) + } + /// Reads a TIFF IFA offset/value field #[inline] pub fn read_offset_u64(&mut self) -> Result<[u8; 8], io::Error> { diff --git a/src/decoder/value.rs b/src/decoder/value.rs new file mode 100644 index 0000000..0673f2b --- /dev/null +++ b/src/decoder/value.rs @@ -0,0 +1,196 @@ +use std::io::{self, Cursor, Read, Seek}; + +use crate::{ + decoder::{ifd::Entry, stream::EndianReader, ValueReader}, + tags::{ByteOrder, IfdPointer, Type}, + TiffResult, +}; + +pub struct EntryBytesReader<'lt, R> { + inner: &'lt mut EndianReader, + variant: Variant, +} + +const OFFSET_BUFFER_LEN: usize = 8; + +enum Variant { + Immediate { + /// An in-value reader if the offset field contains the data. Note that we rotate it to the + /// back of this buffer, not the front, so that `Cursor` already contains the right end. + /// We do not let you seek back beyond our start. + data: Cursor<[u8; OFFSET_BUFFER_LEN]>, + value_bytes: u8, + }, + InFile { + // the length relative to the base so we can seek relative without querying the position. + value_bytes: u64, + remainder: u64, + base: IfdPointer, + }, +} + +impl<'lt, R> EntryBytesReader<'lt, R> { + pub(super) fn from_entry(inner: &'lt mut ValueReader, entry: Entry) -> TiffResult + where + R: Read + Seek, + { + let offset: [_; OFFSET_BUFFER_LEN] = *entry.offset_raw(); + let limit = if inner.bigtiff { 8 } else { 4 }; + assert!(limit as usize <= OFFSET_BUFFER_LEN); + + let value_bytes = entry.field_type().value_bytes(entry.count())?; + + if value_bytes <= limit { + // We move the data to the back of the array whereas it is in the front right now. + let mut value = offset; + value.rotate_left(value_bytes as usize); + let mut cursor = Cursor::new(value); + cursor.set_position(OFFSET_BUFFER_LEN as u64 - value_bytes); + + Ok(EntryBytesReader { + inner: &mut inner.reader, + variant: Variant::Immediate { + data: cursor, + value_bytes: value_bytes as u8, + }, + }) + } else { + // remainder > limit >= 0 so this is an unreachable panic. + let pointer = if inner.bigtiff { + let mut offset = offset; + + inner + .reader + .byte_order + .convert(Type::IFD8, &mut offset[..], ByteOrder::native()); + + IfdPointer(u64::from_ne_bytes(offset)) + } else { + let mut offset = *offset[..4].as_array::<4>().unwrap(); + + inner + .reader + .byte_order + .convert(Type::IFD, &mut offset[..], ByteOrder::native()); + + let offset = u32::from_ne_bytes(offset); + IfdPointer(u64::from(offset)) + }; + + inner.reader.goto_offset(pointer.0)?; + + Ok(EntryBytesReader { + inner: &mut inner.reader, + variant: Variant::InFile { + value_bytes, + remainder: value_bytes, + base: pointer, + }, + }) + } + } +} + +impl Read for EntryBytesReader<'_, R> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match &mut self.variant { + Variant::Immediate { data, .. } => { + return data.read(buf); + } + Variant::InFile { remainder, .. } => { + let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); + let avail = buf.len().min(bound); + let read = self.inner.inner().read(&mut buf[..avail])?; + + // On a well-behaved `Read`: read <= avail <= bound <=(mathematical) remainder + let consume = read as u64; + *remainder -= consume; + Ok(read) + } + } + } + + fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { + match &mut self.variant { + Variant::Immediate { data, .. } => { + return data.read_exact(buf); + } + Variant::InFile { remainder, .. } => { + let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); + + // Fail early if we would be sure to over-read. + if buf.len() > bound { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Tiff value shorter than the requested buffer size", + )); + } + + // since buf.len() <= bound <=(mathematical) remainder + let consume = buf.len() as u64; + self.inner.inner().read_exact(buf)?; + *remainder -= consume; + + Ok(()) + } + } + } +} + +impl Seek for EntryBytesReader<'_, R> { + fn seek(&mut self, offset: io::SeekFrom) -> io::Result { + let (value_bytes, remainder) = match &self.variant { + Variant::Immediate { data, value_bytes } => { + let remainder = data.get_ref().len() as u64 - data.position(); + (*value_bytes as u64, remainder) + } + &Variant::InFile { + value_bytes, + remainder, + base: _, + } => (value_bytes, remainder), + }; + + // Normalize to an offset from the start + let offset_from_start = match offset { + io::SeekFrom::Start(n) => Some(n), + io::SeekFrom::Current(n) => { + let front = value_bytes - remainder; + front.checked_add_signed(n) + } + io::SeekFrom::End(n) => value_bytes.checked_add_signed(n), + }; + + let Some((offset, remainder)) = offset_from_start.and_then(|n| { + let remainder = value_bytes.checked_sub(n)?; + Some((n, remainder)) + }) else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "seek out of bounds of entry value", + )); + }; + + match &mut self.variant { + Variant::Immediate { data, .. } => { + let start = OFFSET_BUFFER_LEN as u64 - value_bytes; + data.set_position(start + offset); + Ok(offset) + } + &mut Variant::InFile { base, .. } => { + // We checked this against overflow on construction. Ignore the offset we get. + self.inner + .inner() + .seek(io::SeekFrom::Start(base.0 + offset))?; + + self.variant = Variant::InFile { + value_bytes, + remainder, + base, + }; + + Ok(offset) + } + } + } +} diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index ed28b8e..48412e0 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -488,7 +488,8 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { self.writer.write_u16(entry.field_type().to_u16())?; let count = K::convert_offset(entry.count())?; count.write(self.writer)?; - self.writer.write_bytes(&entry.offset()[..offset_bytes])?; + self.writer + .write_bytes(&entry.offset_raw()[..offset_bytes])?; } Ok(offset) From a2996ab34e02db409e91ac9e748e25a3673da02f Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:20:38 +0200 Subject: [PATCH 2/7] Basic test for EntryByteReader --- src/decoder/value.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/tags.rs | 7 +++++++ 2 files changed, 51 insertions(+) diff --git a/src/decoder/value.rs b/src/decoder/value.rs index 0673f2b..71a8895 100644 --- a/src/decoder/value.rs +++ b/src/decoder/value.rs @@ -194,3 +194,47 @@ impl Seek for EntryBytesReader<'_, R> { } } } + +#[cfg(test)] +mod tests { + use crate::{decoder::Decoder, tags::Tag}; + use std::io::{Read as _, Seek as _, SeekFrom}; + + #[test] + fn can_seek_tag_bytes() -> Result<(), crate::error::TiffError> { + let file = std::fs::File::open(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/images/int8_rgb.tif" + )) + .unwrap(); + + let mut decoder = Decoder::open(file).unwrap(); + decoder.next_directory().unwrap(); + + let strips = decoder + .current_ifd() + .find_entry(Tag::StripByteCounts) + .expect("some strip byte count is present"); + + let mut buffer = crate::tags::ValueBuffer::default(); + + decoder + .current_ifd() + .find_tag_buf(Tag::StripByteCounts, &mut buffer) + .expect("some strip byte count is present"); + + let mut reader = decoder.read_entry(strips)?; + + let mut data = vec![]; + reader.read_to_end(&mut data)?; + assert_eq!(buffer.as_bytes(), data); + + reader.seek(SeekFrom::Start(0))?; + reader.read_to_end(&mut data)?; + + let (first, second) = data.split_at(data.len() / 2); + assert_eq!(first, second); + + Ok(()) + } +} diff --git a/src/tags.rs b/src/tags.rs index 85412f5..8a3f481 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -567,6 +567,13 @@ impl ValueBuffer { } } +/// Create an empty value buffer with `Byte` type. This type choice is arbitrary. +impl Default for ValueBuffer { + fn default() -> Self { + ValueBuffer::empty(Type::BYTE) + } +} + /// Byte order of the TIFF file. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ByteOrder { From 8a1b65fe68d366fb2c4214b075506375e7d8f73a Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:26:34 +0200 Subject: [PATCH 3/7] Expand testing --- src/decoder/value.rs | 53 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/decoder/value.rs b/src/decoder/value.rs index 71a8895..6c8d781 100644 --- a/src/decoder/value.rs +++ b/src/decoder/value.rs @@ -66,7 +66,7 @@ impl<'lt, R> EntryBytesReader<'lt, R> { IfdPointer(u64::from_ne_bytes(offset)) } else { - let mut offset = *offset[..4].as_array::<4>().unwrap(); + let mut offset = *offset.first_chunk::<4>().unwrap(); inner .reader @@ -95,7 +95,7 @@ impl Read for EntryBytesReader<'_, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { match &mut self.variant { Variant::Immediate { data, .. } => { - return data.read(buf); + data.read(buf) } Variant::InFile { remainder, .. } => { let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); @@ -113,7 +113,7 @@ impl Read for EntryBytesReader<'_, R> { fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { match &mut self.variant { Variant::Immediate { data, .. } => { - return data.read_exact(buf); + data.read_exact(buf) } Variant::InFile { remainder, .. } => { let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); @@ -197,7 +197,7 @@ impl Seek for EntryBytesReader<'_, R> { #[cfg(test)] mod tests { - use crate::{decoder::Decoder, tags::Tag}; + use crate::{decoder::Decoder, encoder, tags::Tag}; use std::io::{Read as _, Seek as _, SeekFrom}; #[test] @@ -237,4 +237,49 @@ mod tests { Ok(()) } + + #[test] + fn seek_from_end() -> Result<(), crate::error::TiffError> { + const ARTIST: &str = "99 little bugs in the code"; + + let data = { + let mut data = std::io::Cursor::new(vec![]); + let mut encoder = encoder::TiffEncoder::new(&mut data)?; + + let mut dir = encoder.image_directory()?; + dir.write_tag(Tag::Artist, ARTIST)?; + dir.finish()?; + + data.into_inner() + }; + + let file = std::io::Cursor::new(data); + let mut decoder = Decoder::open(file).unwrap(); + decoder.next_directory().unwrap(); + + let artist = decoder + .current_ifd() + .find_entry(Tag::Artist) + .expect("some artist is present"); + + let mut reader = decoder.read_entry(artist)?; + + let mut value = vec![]; + reader.read_to_end(&mut value)?; + + let (&nul, value) = value.split_last().unwrap(); + assert_eq!(value, ARTIST.as_bytes()); + assert_eq!(nul, b'\0'); + + assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1); + assert!(reader.seek(SeekFrom::End(1)).is_err()); + + reader.seek(SeekFrom::End(-5))?; + let mut is_code = [0u8; 4]; + reader.read_exact(&mut is_code)?; + + assert_eq!(is_code, *value.last_chunk::<4>().unwrap()); + + Ok(()) + } } From f93a6380e63dc0104b0366b7d86e914d27a06a9c Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:56:30 +0200 Subject: [PATCH 4/7] Extra utils for reading tags --- src/decoder/mod.rs | 31 +++++++++++++++++ src/decoder/value.rs | 82 ++++++++++++++++++++++++++++++++------------ 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 0240126..ea16145 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -852,6 +852,15 @@ impl Decoder { self.value_reader.reader.byte_order } + /// Check if this file is a BigTIFF or a standard TIFF. + pub fn variant(&self) -> TiffVariant { + if self.value_reader.bigtiff { + TiffVariant::BigTiff + } else { + TiffVariant::Standard + } + } + #[inline] pub fn read_ifd_offset(&mut self) -> Result { if self.value_reader.bigtiff { @@ -945,10 +954,32 @@ impl Decoder { Ok(val) } + /// Create a byte reader for the value of an arbitrary IFD entry. + /// + /// Small values are stored inline inside the offset field of the entry. The threshold for this + /// depends on the [`Self::variant`] of the file. Interpret entries that have not been + /// extracted from an IFD of the current decoder with care. + /// + /// The reader returned form this method reads raw bytes. Values retrieved in this way are not + /// yet converted to the host endianess. + /// + /// Returns an error if the entry count exceeds the possible offset values or if the offset + /// of the entry would overflow the offset in the underlying stream. Otherwise returns a reader + /// that yields the bytes of the value. pub fn read_entry(&mut self, entry: ifd::Entry) -> TiffResult> { EntryBytesReader::from_entry(&mut self.value_reader, entry) } + /// Utility to create a reader like [`Self::read_entry`], from the current IFD. + /// + /// Returns an `Ok(None)` if the tag is not found in the current directory. + pub fn read_tag(&mut self, tag: Tag) -> TiffResult>> { + match self.current_ifd().find_entry(tag) { + None => Ok(None), + Some(entry) => self.read_entry(entry).map(Some), + } + } + /// Reads a TIFF IFA offset/value field #[inline] pub fn read_offset_u64(&mut self) -> Result<[u8; 8], io::Error> { diff --git a/src/decoder/value.rs b/src/decoder/value.rs index 6c8d781..7434d71 100644 --- a/src/decoder/value.rs +++ b/src/decoder/value.rs @@ -94,9 +94,7 @@ impl<'lt, R> EntryBytesReader<'lt, R> { impl Read for EntryBytesReader<'_, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { match &mut self.variant { - Variant::Immediate { data, .. } => { - data.read(buf) - } + Variant::Immediate { data, .. } => data.read(buf), Variant::InFile { remainder, .. } => { let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); let avail = buf.len().min(bound); @@ -112,9 +110,7 @@ impl Read for EntryBytesReader<'_, R> { fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { match &mut self.variant { - Variant::Immediate { data, .. } => { - data.read_exact(buf) - } + Variant::Immediate { data, .. } => data.read_exact(buf), Variant::InFile { remainder, .. } => { let bound = usize::try_from(*remainder).unwrap_or(usize::MAX); @@ -197,7 +193,7 @@ impl Seek for EntryBytesReader<'_, R> { #[cfg(test)] mod tests { - use crate::{decoder::Decoder, encoder, tags::Tag}; + use crate::{decoder::Decoder, encoder, tags}; use std::io::{Read as _, Seek as _, SeekFrom}; #[test] @@ -211,22 +207,21 @@ mod tests { let mut decoder = Decoder::open(file).unwrap(); decoder.next_directory().unwrap(); - let strips = decoder - .current_ifd() - .find_entry(Tag::StripByteCounts) - .expect("some strip byte count is present"); - let mut buffer = crate::tags::ValueBuffer::default(); decoder .current_ifd() - .find_tag_buf(Tag::StripByteCounts, &mut buffer) + .find_tag_buf(tags::Tag::StripByteCounts, &mut buffer) .expect("some strip byte count is present"); - let mut reader = decoder.read_entry(strips)?; + let mut reader = decoder + .read_tag(tags::Tag::StripByteCounts)? + .expect("some strip byte count is present"); let mut data = vec![]; reader.read_to_end(&mut data)?; + + // `ValueBuffer` also stores its data in the file endianess until converted. assert_eq!(buffer.as_bytes(), data); reader.seek(SeekFrom::Start(0))?; @@ -247,7 +242,7 @@ mod tests { let mut encoder = encoder::TiffEncoder::new(&mut data)?; let mut dir = encoder.image_directory()?; - dir.write_tag(Tag::Artist, ARTIST)?; + dir.write_tag(tags::Tag::Artist, ARTIST)?; dir.finish()?; data.into_inner() @@ -257,12 +252,9 @@ mod tests { let mut decoder = Decoder::open(file).unwrap(); decoder.next_directory().unwrap(); - let artist = decoder - .current_ifd() - .find_entry(Tag::Artist) - .expect("some artist is present"); - - let mut reader = decoder.read_entry(artist)?; + let mut reader = decoder + .read_tag(tags::Tag::Artist)? + .expect("the artist is present?"); let mut value = vec![]; reader.read_to_end(&mut value)?; @@ -274,12 +266,58 @@ mod tests { assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1); assert!(reader.seek(SeekFrom::End(1)).is_err()); - reader.seek(SeekFrom::End(-5))?; + // Check relative seeks by comparing against the "code" + let code_offset = reader.seek(SeekFrom::End(-5))?; let mut is_code = [0u8; 4]; reader.read_exact(&mut is_code)?; + assert_eq!(is_code, *value.last_chunk::<4>().unwrap()); + assert_eq!(reader.seek(SeekFrom::Start(0))?, 0); + assert_eq!(reader.seek(SeekFrom::Current(0))?, 0); + assert!(reader.seek(SeekFrom::Current(-1)).is_err()); + + reader.seek(SeekFrom::Start(code_offset))?; + reader.read_exact(&mut is_code)?; assert_eq!(is_code, *value.last_chunk::<4>().unwrap()); Ok(()) } + + /// Check that the reader from an `offset` field also works. + #[test] + fn read_from_offset() -> Result<(), crate::error::TiffError> { + let data = { + let mut data = std::io::Cursor::new(vec![]); + let mut encoder = encoder::TiffEncoder::new(&mut data)?; + + let mut dir = encoder.image_directory()?; + dir.write_tag( + tags::Tag::PlanarConfiguration, + tags::PlanarConfiguration::Planar, + )?; + dir.finish()?; + + data.into_inner() + }; + + let file = std::io::Cursor::new(data); + let mut decoder = Decoder::open(file).unwrap(); + decoder.next_directory().unwrap(); + let bo = decoder.byte_order(); + + let mut reader = decoder + .read_tag(tags::Tag::PlanarConfiguration)? + .expect("some artist is present"); + + let mut buf = [0; 2]; + reader.read_exact(&mut buf)?; + + bo.convert(tags::Type::SHORT, &mut buf, tags::ByteOrder::BigEndian); + assert_eq!(buf, [0, 2]); + + // Check relative seeks, this item is only two bytes long. + assert!(reader.seek(SeekFrom::End(-3)).is_err()); + + Ok(()) + } } From 5b8a2832c3c56f3c8dde4104e122d0d21fea3ba2 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:33:46 +0200 Subject: [PATCH 5/7] Add design note --- src/decoder/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index ea16145..32b478c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -966,6 +966,14 @@ impl Decoder { /// Returns an error if the entry count exceeds the possible offset values or if the offset /// of the entry would overflow the offset in the underlying stream. Otherwise returns a reader /// that yields the bytes of the value. + /// + /// ## Design Notes + /// + /// This method does not appear on [`IfdDecoder`] as we assume the reader type may be important + /// to you. It also allows forward compatibility to additional trait implementations without + /// demanding them from all types as `dyn` would. There is also no `M×N` instantiation problem + /// with this method: you get one type per reader type and monomorphic methods from the traits + /// it implements. pub fn read_entry(&mut self, entry: ifd::Entry) -> TiffResult> { EntryBytesReader::from_entry(&mut self.value_reader, entry) } From 006f0b0fa4cc40e17174ef1d59523deef2d25792 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:53:49 +0200 Subject: [PATCH 6/7] Directly implement Seek::stream_position --- src/decoder/value.rs | 47 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/decoder/value.rs b/src/decoder/value.rs index 7434d71..16c27a6 100644 --- a/src/decoder/value.rs +++ b/src/decoder/value.rs @@ -25,7 +25,7 @@ enum Variant { // the length relative to the base so we can seek relative without querying the position. value_bytes: u64, remainder: u64, - base: IfdPointer, + base: u64, }, } @@ -84,7 +84,7 @@ impl<'lt, R> EntryBytesReader<'lt, R> { variant: Variant::InFile { value_bytes, remainder: value_bytes, - base: pointer, + base: pointer.0, }, }) } @@ -177,7 +177,7 @@ impl Seek for EntryBytesReader<'_, R> { // We checked this against overflow on construction. Ignore the offset we get. self.inner .inner() - .seek(io::SeekFrom::Start(base.0 + offset))?; + .seek(io::SeekFrom::Start(base + offset))?; self.variant = Variant::InFile { value_bytes, @@ -189,6 +189,34 @@ impl Seek for EntryBytesReader<'_, R> { } } } + + fn stream_position(&mut self) -> io::Result { + Ok(match &self.variant { + Variant::Immediate { data, value_bytes } => { + // Recall the value is at the end of the buffer. + let from_end = OFFSET_BUFFER_LEN as u64 - data.position(); + u64::from(*value_bytes) - from_end + } + // Note: we might think about using the inner `stream_position` here. However that + // exposes something interesting: If the internal stream is modified concurrently from + // another instance (e.g. a shared `File`) then we may lose track of it anyways. + // Especially the `Read` implementation is not bounded on `Seek` so it could not + // acquire the position. And it should not since that is just a different form of race + // where we risk the position being modified by a different thread just as well. + // + // So in the interest of least surprise we just assume that we can track the position + // accurately here and that the inner reader uses for instance `read_at` consistently. + // We own the value (or mutable reference) it after all. + // + // Also this one MUST be compatible with `seek(SeekFrom::Current(0))` as per trait + // defintion—so at least keep those consistent if changing anything. + &Variant::InFile { + remainder, + value_bytes, + .. + } => value_bytes - remainder, + }) + } } #[cfg(test)] @@ -214,6 +242,8 @@ mod tests { .find_tag_buf(tags::Tag::StripByteCounts, &mut buffer) .expect("some strip byte count is present"); + let bytes_of_value = buffer.as_bytes().len() as u64; + let mut reader = decoder .read_tag(tags::Tag::StripByteCounts)? .expect("some strip byte count is present"); @@ -222,18 +252,22 @@ mod tests { reader.read_to_end(&mut data)?; // `ValueBuffer` also stores its data in the file endianess until converted. + assert_eq!(reader.stream_position()?, bytes_of_value); assert_eq!(buffer.as_bytes(), data); reader.seek(SeekFrom::Start(0))?; + assert_eq!(reader.stream_position()?, 0); reader.read_to_end(&mut data)?; + assert_eq!(reader.stream_position()?, bytes_of_value); - let (first, second) = data.split_at(data.len() / 2); + let (first, second) = data.split_at(bytes_of_value as usize); assert_eq!(first, second); Ok(()) } #[test] + #[allow(clippy::seek_from_current)] // We want to test that functionality. fn seek_from_end() -> Result<(), crate::error::TiffError> { const ARTIST: &str = "99 little bugs in the code"; @@ -265,6 +299,7 @@ mod tests { assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1); assert!(reader.seek(SeekFrom::End(1)).is_err()); + assert_eq!(reader.stream_position()?, ARTIST.len() as u64 + 1); // Check relative seeks by comparing against the "code" let code_offset = reader.seek(SeekFrom::End(-5))?; @@ -273,11 +308,15 @@ mod tests { assert_eq!(is_code, *value.last_chunk::<4>().unwrap()); assert_eq!(reader.seek(SeekFrom::Start(0))?, 0); + assert_eq!(reader.stream_position()?, 0); assert_eq!(reader.seek(SeekFrom::Current(0))?, 0); + assert_eq!(reader.stream_position()?, 0); assert!(reader.seek(SeekFrom::Current(-1)).is_err()); + assert_eq!(reader.stream_position()?, 0); reader.seek(SeekFrom::Start(code_offset))?; reader.read_exact(&mut is_code)?; + assert_eq!(reader.stream_position()?, ARTIST.len() as u64); assert_eq!(is_code, *value.last_chunk::<4>().unwrap()); Ok(()) From 52b9cc7a19797663687adc662ae5bcd3b43133b4 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:35:19 +0200 Subject: [PATCH 7/7] Rename EntryBytesReader module --- src/decoder/{value.rs => entry_reader.rs} | 0 src/decoder/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/decoder/{value.rs => entry_reader.rs} (100%) diff --git a/src/decoder/value.rs b/src/decoder/entry_reader.rs similarity index 100% rename from src/decoder/value.rs rename to src/decoder/entry_reader.rs diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 32b478c..7ab7ee9 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -15,15 +15,15 @@ use self::stream::EndianReader; mod buffer; mod cycles; +mod entry_reader; pub mod ifd; mod image; mod logluv; mod stream; mod tag_reader; -mod value; pub use self::buffer::{DecodingSampleBuffer, DecodingSampleSlice, DecodingSampleType}; -pub use self::value::EntryBytesReader; +pub use self::entry_reader::EntryBytesReader; #[deprecated = "Renamed to `DecodingSampleBuffer`"] pub type DecodingResult = DecodingSampleBuffer;