diff --git a/crates/psd/src/head.rs b/crates/psd/src/head.rs index 8aee82745..ba5054c69 100644 --- a/crates/psd/src/head.rs +++ b/crates/psd/src/head.rs @@ -241,6 +241,7 @@ mod tests { assert!(all.len() > 1); let mut limited = view.resources_limited(1); assert!(limited.next().unwrap().is_ok()); + assert!(limited.size_hint().1.unwrap() >= 1, "limit error is included in the upper bound"); assert!(limited.next().unwrap().is_err()); assert!(limited.next().is_none()); } diff --git a/crates/psd/src/resources.rs b/crates/psd/src/resources.rs index 809bee696..7b12f19a6 100644 --- a/crates/psd/src/resources.rs +++ b/crates/psd/src/resources.rs @@ -136,15 +136,30 @@ impl<'a> Iterator for ResourceIter<'a> { } fn size_hint(&self) -> (usize, Option) { - // The smallest record has a four-byte signature, two-byte id, one-byte empty name - // (plus its pad), and four-byte data length. This bounds iteration even for a section - // containing millions of empty records, without imposing an arbitrary format limit. - (0, Some(self.reader.remaining() / 12)) + // There can be at most one more item than the number of bytes: valid records consume + // bytes, and a malformed record or an exhausted caller limit can yield one final error + // without consuming any. Use a conservative bound so even a short truncated record has + // room for its error item. + let upper = if self.failed || self.reader.is_empty() { 0 } else { self.reader.remaining().saturating_add(1) }; + (0, Some(upper)) } } impl std::iter::FusedIterator for ResourceIter<'_> {} +#[cfg(test)] +mod view_tests { + use super::ResourceIter; + + #[test] + fn truncated_resource_size_hint_includes_error_item() { + let mut iter = ResourceIter::new(b"JUNK"); + assert!(iter.size_hint().1.unwrap() >= 1); + assert!(iter.next().unwrap().is_err()); + assert_eq!(iter.size_hint(), (0, Some(0))); + } +} + /// Fixed-point resolution info (resource 1005). /// /// `h_res_fixed` and `v_res_fixed` are always pixels per inch (16.16 fixed).