use crate::error::Result;
use std::io::Read;
#[derive(Debug, Clone, PartialEq)]
pub enum ChunkType {
Text,
Image,
Graphics,
Form,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ContentChunk {
pub chunk_type: ChunkType,
pub data: Vec<u8>,
pub position: u64,
pub size: usize,
pub page_number: u32,
}
impl ContentChunk {
pub fn new(chunk_type: ChunkType, data: Vec<u8>, position: u64, page_number: u32) -> Self {
let size = data.len();
Self {
chunk_type,
data,
position,
size,
page_number,
}
}
pub fn is_text(&self) -> bool {
self.chunk_type == ChunkType::Text
}
pub fn is_image(&self) -> bool {
self.chunk_type == ChunkType::Image
}
pub fn as_text(&self) -> Option<String> {
if self.is_text() {
Some(String::from_utf8_lossy(&self.data).to_string())
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct ChunkOptions {
pub max_chunk_size: usize,
pub split_large_objects: bool,
pub buffer_size: usize,
pub chunk_types: Vec<ChunkType>,
}
impl Default for ChunkOptions {
fn default() -> Self {
Self {
max_chunk_size: 1024 * 1024, split_large_objects: true,
buffer_size: 64 * 1024, chunk_types: vec![
ChunkType::Text,
ChunkType::Image,
ChunkType::Graphics,
ChunkType::Form,
ChunkType::Unknown,
],
}
}
}
impl ChunkOptions {
pub fn validate(&self) -> Result<()> {
if self.max_chunk_size == 0 {
return Err(crate::error::PdfError::InvalidStructure(
"max_chunk_size cannot be 0".to_string(),
));
}
if self.buffer_size == 0 {
return Err(crate::error::PdfError::InvalidStructure(
"buffer_size cannot be 0".to_string(),
));
}
Ok(())
}
}
pub struct ChunkProcessor {
options: ChunkOptions,
current_position: u64,
current_page: u32,
}
impl ChunkProcessor {
pub fn new(options: ChunkOptions) -> Self {
Self {
options,
current_position: 0,
current_page: 0,
}
}
pub fn process_content(&mut self, content: &[u8]) -> Result<Vec<ContentChunk>> {
self.options.validate()?;
let mut decoder =
ChunkDecoder::new(&self.options, self.current_position, self.current_page);
let mut chunks = decoder.feed(content)?;
chunks.extend(decoder.finish()?);
self.current_position = self
.current_position
.checked_add(content.len() as u64)
.ok_or_else(|| chunk_error("content position overflow"))?;
Ok(chunks)
}
pub fn set_page(&mut self, page_number: u32) {
self.current_page = page_number;
}
pub fn reset(&mut self) {
self.current_position = 0;
self.current_page = 0;
}
}
const MAX_PENDING_RECORD: usize = 16 * 1024 * 1024;
#[derive(Default)]
enum LexState {
#[default]
Normal,
Word(usize),
Name,
Comment,
Literal {
depth: usize,
escaped: bool,
},
Angle,
Hex,
CloseAngle,
}
struct ChunkDecoder<'a> {
options: &'a ChunkOptions,
position: u64,
page: u32,
prefix: Vec<u8>,
started: bool,
opaque: Option<ChunkType>,
record: Vec<u8>,
record_start: u64,
state: LexState,
nesting: usize,
in_text: bool,
last_type: ChunkType,
output: Option<ContentChunk>,
}
impl<'a> ChunkDecoder<'a> {
fn new(options: &'a ChunkOptions, position: u64, page: u32) -> Self {
Self {
options,
position,
page,
prefix: Vec::new(),
started: false,
opaque: options.chunk_types.is_empty().then_some(ChunkType::Unknown),
record: Vec::new(),
record_start: position,
state: LexState::Normal,
nesting: 0,
in_text: false,
last_type: ChunkType::Unknown,
output: None,
}
}
fn feed(&mut self, bytes: &[u8]) -> Result<Vec<ContentChunk>> {
let mut out = Vec::new();
for &byte in bytes {
if !self.started {
self.prefix.push(byte);
if self.prefix.len() < 8 {
continue;
}
self.start(&mut out)?;
} else {
self.byte(byte, &mut out)?;
}
}
Ok(out)
}
fn start(&mut self, out: &mut Vec<ContentChunk>) -> Result<()> {
self.started = true;
if self.prefix.starts_with(b"\xff\xd8") || self.prefix.starts_with(b"\x89PNG\r\n\x1a\n") {
self.opaque = Some(ChunkType::Image);
}
let prefix = std::mem::take(&mut self.prefix);
for byte in prefix {
self.byte(byte, out)?;
}
Ok(())
}
fn byte(&mut self, byte: u8, out: &mut Vec<ContentChunk>) -> Result<()> {
if let Some(kind) = self.opaque.clone() {
if self.options.split_large_objects || !self.options.chunk_types.contains(&kind) {
self.append(&[byte], kind, self.position, out)?;
} else {
if self.record.len() == self.options.max_chunk_size {
return Err(chunk_error(
"opaque content exceeds max_chunk_size with splitting disabled",
));
}
self.record.push(byte);
}
self.advance()?;
return Ok(());
}
if let LexState::Word(start) = self.state {
if delimiter(byte) {
let token = self.record[start..].to_vec();
self.state = LexState::Normal;
self.operator(&token, out)?;
}
}
if self.opaque.is_some() {
return self.byte(byte, out);
}
let index = self.record.len();
if self.record.len() == MAX_PENDING_RECORD {
return Err(chunk_error("unfinished content record exceeds 16 MiB"));
}
self.record.push(byte);
self.advance()?;
self.state = match std::mem::take(&mut self.state) {
LexState::Comment => {
if byte == b'\r' || byte == b'\n' {
LexState::Normal
} else {
LexState::Comment
}
}
LexState::Name => {
if delimiter(byte) {
self.normal(byte, index)?
} else {
LexState::Name
}
}
LexState::Literal { mut depth, escaped } => {
if escaped {
LexState::Literal {
depth,
escaped: false,
}
} else if byte == b'\\' {
LexState::Literal {
depth,
escaped: true,
}
} else {
if byte == b'(' {
depth += 1;
}
if byte == b')' {
depth -= 1;
}
if depth > 128 {
return Err(chunk_error("literal string nesting exceeds 128"));
}
if depth == 0 {
LexState::Normal
} else {
LexState::Literal {
depth,
escaped: false,
}
}
}
}
LexState::Angle => {
if byte == b'<' {
self.push_nesting()?;
LexState::Normal
} else if byte == b'>' {
LexState::Normal
} else {
LexState::Hex
}
}
LexState::Hex => {
if byte == b'>' {
LexState::Normal
} else {
LexState::Hex
}
}
LexState::CloseAngle => {
if byte == b'>' {
LexState::Normal
} else {
self.normal(byte, index)?
}
}
LexState::Word(start) => LexState::Word(start),
LexState::Normal => self.normal(byte, index)?,
};
Ok(())
}
fn advance(&mut self) -> Result<()> {
self.position = self
.position
.checked_add(1)
.ok_or_else(|| chunk_error("content position overflow"))?;
Ok(())
}
fn push_nesting(&mut self) -> Result<()> {
self.nesting += 1;
if self.nesting > 128 {
return Err(chunk_error("content nesting exceeds 128"));
}
Ok(())
}
fn normal(&mut self, byte: u8, index: usize) -> Result<LexState> {
Ok(match byte {
b'%' => LexState::Comment,
b'/' => LexState::Name,
b'(' => LexState::Literal {
depth: 1,
escaped: false,
},
b'<' => LexState::Angle,
b'>' => {
self.nesting = self.nesting.saturating_sub(1);
LexState::CloseAngle
}
b'[' => {
self.push_nesting()?;
LexState::Normal
}
b']' => {
self.nesting = self.nesting.saturating_sub(1);
LexState::Normal
}
b')' | b'{' | b'}' => LexState::Normal,
b if whitespace(b) => LexState::Normal,
_ => LexState::Word(index),
})
}
fn operator(&mut self, token: &[u8], out: &mut Vec<ContentChunk>) -> Result<()> {
if self.nesting != 0
|| token.is_empty()
|| matches!(token, b"true" | b"false" | b"null")
|| token
.iter()
.all(|b| b.is_ascii_digit() || b"+-.".contains(b))
{
return Ok(());
}
if token == b"BI" {
if !self.options.chunk_types.contains(&ChunkType::Unknown) {
return Err(chunk_error("inline image boundary is ambiguous for type filtering; include Unknown to preserve the remaining bytes"));
}
self.opaque = Some(ChunkType::Unknown);
return self.emit(ChunkType::Unknown, out);
}
let kind = if self.in_text
|| matches!(
token,
b"BT"
| b"ET"
| b"Tc"
| b"Tw"
| b"Tz"
| b"TL"
| b"Tf"
| b"Tr"
| b"Ts"
| b"Td"
| b"TD"
| b"Tm"
| b"T*"
| b"Tj"
| b"TJ"
| b"'"
| b"\""
) {
ChunkType::Text
} else if matches!(
token,
b"q" | b"Q"
| b"cm"
| b"w"
| b"J"
| b"j"
| b"M"
| b"d"
| b"ri"
| b"i"
| b"gs"
| b"m"
| b"l"
| b"c"
| b"v"
| b"y"
| b"h"
| b"re"
| b"S"
| b"s"
| b"f"
| b"F"
| b"f*"
| b"B"
| b"B*"
| b"b"
| b"b*"
| b"n"
| b"W"
| b"W*"
| b"CS"
| b"cs"
| b"SC"
| b"SCN"
| b"sc"
| b"scn"
| b"G"
| b"g"
| b"RG"
| b"rg"
| b"K"
| b"k"
| b"sh"
) {
ChunkType::Graphics
} else {
ChunkType::Unknown
};
if token == b"BT" {
self.in_text = true;
}
if token == b"ET" {
self.in_text = false;
}
self.emit(kind, out)
}
fn emit(&mut self, kind: ChunkType, out: &mut Vec<ContentChunk>) -> Result<()> {
let data = std::mem::take(&mut self.record);
self.append(&data, kind.clone(), self.record_start, out)?;
self.record_start = self.position;
self.last_type = kind;
Ok(())
}
fn append(
&mut self,
data: &[u8],
kind: ChunkType,
position: u64,
out: &mut Vec<ContentChunk>,
) -> Result<()> {
if data.is_empty() {
return Ok(());
}
if !self.options.chunk_types.contains(&kind) {
if let Some(chunk) = self.output.take() {
out.push(chunk);
}
return Ok(());
}
if !self.options.split_large_objects && data.len() > self.options.max_chunk_size {
return Err(chunk_error(
"content operation exceeds max_chunk_size with splitting disabled",
));
}
let mut offset = 0;
while offset < data.len() {
if self.output.as_ref().is_some_and(|c| {
c.chunk_type != kind
|| c.position + c.size as u64 != position + offset as u64
|| (!self.options.split_large_objects
&& c.size + data.len() > self.options.max_chunk_size)
}) {
out.push(self.output.take().unwrap());
}
let chunk = self.output.get_or_insert_with(|| {
ContentChunk::new(
kind.clone(),
Vec::new(),
position + offset as u64,
self.page,
)
});
let n = (self.options.max_chunk_size - chunk.size).min(data.len() - offset);
chunk.data.extend_from_slice(&data[offset..offset + n]);
chunk.size += n;
offset += n;
if chunk.size == self.options.max_chunk_size {
out.push(self.output.take().unwrap());
}
}
Ok(())
}
fn finish(mut self) -> Result<Vec<ContentChunk>> {
let mut out = Vec::new();
if !self.started {
self.start(&mut out)?;
}
if self.opaque.is_none() {
if let LexState::Word(start) = self.state {
let token = self.record[start..].to_vec();
self.operator(&token, &mut out)?;
}
}
let kind = self.opaque.clone().unwrap_or_else(|| {
if self.in_text {
ChunkType::Text
} else if self.record.iter().all(|b| whitespace(*b)) {
self.last_type.clone()
} else {
ChunkType::Unknown
}
});
self.emit(kind, &mut out)?;
if let Some(chunk) = self.output.take() {
out.push(chunk);
}
Ok(out)
}
}
fn whitespace(byte: u8) -> bool {
matches!(byte, 0 | 9 | 10 | 12 | 13 | 32)
}
fn delimiter(byte: u8) -> bool {
whitespace(byte) || b"()<>[]{}/%".contains(&byte)
}
fn chunk_error(message: &str) -> crate::error::PdfError {
crate::error::PdfError::InvalidStructure(message.to_owned())
}
pub fn process_in_chunks<R, F>(mut reader: R, options: ChunkOptions, mut callback: F) -> Result<()>
where
R: Read,
F: FnMut(ContentChunk) -> Result<()>,
{
options.validate()?;
let mut decoder = ChunkDecoder::new(&options, 0, 0);
let mut buffer = vec![0u8; options.buffer_size];
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
for chunk in decoder.feed(&buffer[..n])? {
callback(chunk)?;
}
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(crate::error::PdfError::Io(e)),
}
}
for chunk in decoder.finish()? {
callback(chunk)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_chunk() {
let chunk = ContentChunk::new(ChunkType::Text, b"Hello World".to_vec(), 1024, 0);
assert_eq!(chunk.chunk_type, ChunkType::Text);
assert_eq!(chunk.size, 11);
assert_eq!(chunk.position, 1024);
assert_eq!(chunk.page_number, 0);
assert!(chunk.is_text());
assert!(!chunk.is_image());
assert_eq!(chunk.as_text(), Some("Hello World".to_string()));
}
#[test]
fn test_chunk_options_default() {
let options = ChunkOptions::default();
assert_eq!(options.max_chunk_size, 1024 * 1024);
assert!(options.split_large_objects);
assert_eq!(options.buffer_size, 64 * 1024);
assert_eq!(options.chunk_types.len(), 5);
}
#[test]
fn test_chunk_processor() {
let options = ChunkOptions::default();
let mut processor = ChunkProcessor::new(options);
let content = b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET";
let chunks = processor.process_content(content).unwrap();
assert!(!chunks.is_empty());
assert_eq!(chunks[0].chunk_type, ChunkType::Text);
assert_eq!(chunks[0].data, content);
}
#[test]
fn test_large_content_splitting() {
let options = ChunkOptions {
max_chunk_size: 10, ..Default::default()
};
let mut processor = ChunkProcessor::new(options);
let content = b"This is a much longer content that should be split into multiple chunks";
let chunks = processor.process_content(content).unwrap();
assert!(chunks.len() > 1);
assert!(chunks.iter().all(|c| c.size <= 10));
}
#[test]
fn test_chunk_filtering() {
let options = ChunkOptions {
chunk_types: vec![ChunkType::Text], ..Default::default()
};
let mut processor = ChunkProcessor::new(options);
let text_content = b"BT (text) Tj ET";
let image_content = b"\xFF\xD8\xFF\xE0 image data";
let text_chunks = processor.process_content(text_content).unwrap();
assert_eq!(text_chunks.len(), 1);
let image_chunks = processor.process_content(image_content).unwrap();
assert_eq!(image_chunks.len(), 0); }
#[test]
fn test_process_in_chunks() {
use std::io::Cursor;
let data = b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET";
let cursor = Cursor::new(data);
let options = ChunkOptions {
buffer_size: 10,
..Default::default()
};
let mut chunks_received = Vec::new();
process_in_chunks(cursor, options, |chunk| {
chunks_received.push(chunk);
Ok(())
})
.unwrap();
assert!(!chunks_received.is_empty());
}
#[test]
fn test_page_tracking() {
let mut processor = ChunkProcessor::new(ChunkOptions::default());
processor.set_page(5);
let content = b"Page 5 content";
let chunks = processor.process_content(content).unwrap();
assert!(!chunks.is_empty());
assert_eq!(chunks[0].page_number, 5);
}
#[test]
fn test_processor_reset() {
let mut processor = ChunkProcessor::new(ChunkOptions::default());
processor.current_position = 1000;
processor.current_page = 10;
processor.reset();
assert_eq!(processor.current_position, 0);
assert_eq!(processor.current_page, 0);
}
#[test]
fn test_chunk_type_debug_clone_eq() {
let types = vec![
ChunkType::Text,
ChunkType::Image,
ChunkType::Graphics,
ChunkType::Form,
ChunkType::Unknown,
];
for chunk_type in types {
let debug_str = format!("{chunk_type:?}");
assert!(!debug_str.is_empty());
let cloned = chunk_type.clone();
assert_eq!(chunk_type, cloned);
}
}
#[test]
fn test_content_chunk_debug_clone() {
let chunk = ContentChunk {
chunk_type: ChunkType::Graphics,
data: vec![1, 2, 3, 4],
position: 512,
size: 4,
page_number: 2,
};
let debug_str = format!("{chunk:?}");
assert!(debug_str.contains("ContentChunk"));
assert!(debug_str.contains("Graphics"));
let cloned = chunk.clone();
assert_eq!(cloned.chunk_type, chunk.chunk_type);
assert_eq!(cloned.data, chunk.data);
assert_eq!(cloned.position, chunk.position);
assert_eq!(cloned.size, chunk.size);
assert_eq!(cloned.page_number, chunk.page_number);
}
#[test]
fn test_chunk_options_debug_clone() {
let options = ChunkOptions {
max_chunk_size: 2048,
split_large_objects: false,
buffer_size: 1024,
chunk_types: vec![ChunkType::Text, ChunkType::Image],
};
let debug_str = format!("{options:?}");
assert!(debug_str.contains("ChunkOptions"));
let cloned = options.clone();
assert_eq!(cloned.max_chunk_size, options.max_chunk_size);
assert_eq!(cloned.split_large_objects, options.split_large_objects);
assert_eq!(cloned.buffer_size, options.buffer_size);
assert_eq!(cloned.chunk_types, options.chunk_types);
}
#[test]
fn test_content_chunk_image_methods() {
let image_chunk = ContentChunk::new(ChunkType::Image, b"\xFF\xD8\xFF\xE0".to_vec(), 0, 0);
assert!(image_chunk.is_image());
assert!(!image_chunk.is_text());
assert_eq!(image_chunk.as_text(), None);
}
#[test]
fn test_content_chunk_non_text_as_text() {
let graphics_chunk =
ContentChunk::new(ChunkType::Graphics, b"100 200 m 300 400 l S".to_vec(), 0, 0);
assert!(!graphics_chunk.is_text());
assert!(!graphics_chunk.is_image());
assert_eq!(graphics_chunk.as_text(), None);
}
#[test]
fn test_content_chunk_size_calculation() {
let data = b"Hello, World!".to_vec();
let expected_size = data.len();
let chunk = ContentChunk::new(ChunkType::Text, data, 100, 1);
assert_eq!(chunk.size, expected_size);
assert_eq!(chunk.size, chunk.data.len());
}
#[test]
fn test_chunk_processor_position_tracking() {
let mut processor = ChunkProcessor::new(ChunkOptions::default());
let content1 = b"First chunk";
let content2 = b"Second chunk";
let chunks1 = processor.process_content(content1).unwrap();
assert_eq!(chunks1[0].position, 0);
let chunks2 = processor.process_content(content2).unwrap();
assert_eq!(chunks2[0].position, content1.len() as u64);
}
#[test]
fn test_chunk_options_all_chunk_types() {
let all_types = vec![
ChunkType::Text,
ChunkType::Image,
ChunkType::Graphics,
ChunkType::Form,
ChunkType::Unknown,
];
let options = ChunkOptions {
chunk_types: all_types,
..Default::default()
};
assert_eq!(options.chunk_types.len(), 5);
assert!(options.chunk_types.contains(&ChunkType::Text));
assert!(options.chunk_types.contains(&ChunkType::Image));
assert!(options.chunk_types.contains(&ChunkType::Graphics));
assert!(options.chunk_types.contains(&ChunkType::Form));
assert!(options.chunk_types.contains(&ChunkType::Unknown));
}
#[test]
fn test_chunk_filtering_multiple_types() {
let mut options = ChunkOptions::default();
options.chunk_types = vec![ChunkType::Text, ChunkType::Graphics];
let mut processor = ChunkProcessor::new(options);
let text_content = b"BT (text) Tj ET";
let graphics_content = b"100 200 m 300 400 l S";
let image_content = b"\xFF\xD8\xFF\xE0";
let text_chunks = processor.process_content(text_content).unwrap();
assert_eq!(text_chunks.len(), 1);
let graphics_chunks = processor.process_content(graphics_content).unwrap();
assert_eq!(graphics_chunks.len(), 1);
let image_chunks = processor.process_content(image_content).unwrap();
assert_eq!(image_chunks.len(), 0); }
#[test]
fn test_process_in_chunks_with_io_error() {
use std::io::Error;
struct ErrorReader;
impl Read for ErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(Error::other("Test error"))
}
}
let reader = ErrorReader;
let options = ChunkOptions::default();
let result = process_in_chunks(reader, options, |_chunk| Ok(()));
assert!(result.is_err());
}
#[test]
fn test_process_in_chunks_with_callback_error() {
use std::io::Cursor;
let data = b"BT (text) Tj ET";
let cursor = Cursor::new(data);
let options = ChunkOptions::default();
let result = process_in_chunks(cursor, options, |_chunk| {
Err(crate::error::PdfError::ParseError(
"Callback error".to_string(),
))
});
assert!(result.is_err());
}
#[test]
fn test_process_in_chunks_empty_data() {
use std::io::Cursor;
let data = b"";
let cursor = Cursor::new(data);
let options = ChunkOptions::default();
let mut chunks_received = Vec::new();
process_in_chunks(cursor, options, |chunk| {
chunks_received.push(chunk);
Ok(())
})
.unwrap();
assert!(chunks_received.is_empty());
}
#[test]
fn test_chunk_processor_with_zero_max_size() {
let mut options = ChunkOptions::default();
options.max_chunk_size = 0;
let mut processor = ChunkProcessor::new(options);
let content = b"Some content";
assert!(processor.process_content(content).is_err());
}
#[test]
fn test_chunk_processor_exact_chunk_size() {
let mut options = ChunkOptions::default();
options.max_chunk_size = 5;
let mut processor = ChunkProcessor::new(options);
let content = b"Hello";
let chunks = processor.process_content(content).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].size, 5);
}
#[test]
fn test_content_chunk_with_binary_data() {
let binary_data = vec![0, 1, 2, 3, 255, 254, 253];
let chunk = ContentChunk::new(ChunkType::Image, binary_data.clone(), 0, 0);
assert_eq!(chunk.data, binary_data);
assert_eq!(chunk.size, 7);
assert!(chunk.is_image());
assert_eq!(chunk.as_text(), None);
}
#[test]
fn test_content_chunk_as_text_with_utf8() {
let text_data = "Hello, 世界!".as_bytes().to_vec();
let chunk = ContentChunk::new(ChunkType::Text, text_data, 0, 0);
assert_eq!(chunk.as_text(), Some("Hello, 世界!".to_string()));
}
#[test]
fn test_content_chunk_as_text_with_invalid_utf8() {
let invalid_utf8 = vec![0xFF, 0xFE, 0xFD];
let chunk = ContentChunk::new(ChunkType::Text, invalid_utf8, 0, 0);
let text = chunk.as_text();
assert!(text.is_some());
assert!(!text.unwrap().is_empty());
}
#[test]
fn test_processor_multiple_pages() {
let mut processor = ChunkProcessor::new(ChunkOptions::default());
processor.set_page(0);
let content1 = b"Page 0 content";
let chunks1 = processor.process_content(content1).unwrap();
assert_eq!(chunks1[0].page_number, 0);
processor.set_page(1);
let content2 = b"Page 1 content";
let chunks2 = processor.process_content(content2).unwrap();
assert_eq!(chunks2[0].page_number, 1);
assert!(chunks2[0].position > chunks1[0].position);
}
#[test]
fn test_chunk_options_empty_chunk_types() {
let options = ChunkOptions {
chunk_types: vec![], ..Default::default()
};
let mut processor = ChunkProcessor::new(options);
let content = b"Any content";
let chunks = processor.process_content(content).unwrap();
assert!(chunks.is_empty()); }
#[test]
fn test_process_in_chunks_large_buffer() {
use std::io::Cursor;
let data = b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET";
let cursor = Cursor::new(data);
let options = ChunkOptions {
buffer_size: 1024, ..Default::default()
};
let mut chunks_received = Vec::new();
process_in_chunks(cursor, options, |chunk| {
chunks_received.push(chunk);
Ok(())
})
.unwrap();
assert!(!chunks_received.is_empty());
assert_eq!(chunks_received[0].data, data);
}
#[test]
fn test_chunk_options_validation() {
let mut options = ChunkOptions::default();
assert!(options.validate().is_ok());
options.max_chunk_size = 0;
assert!(options.validate().is_err());
options = ChunkOptions::default();
options.buffer_size = 0;
assert!(options.validate().is_err());
}
#[test]
fn test_process_in_chunks_with_invalid_options() {
use std::io::Cursor;
let data = b"test data";
let cursor = Cursor::new(data);
let mut options = ChunkOptions::default();
options.buffer_size = 0;
let result = process_in_chunks(cursor, options, |_| Ok(()));
assert!(result.is_err());
let cursor = Cursor::new(data);
let mut options = ChunkOptions::default();
options.max_chunk_size = 0;
let result = process_in_chunks(cursor, options, |_| Ok(()));
assert!(result.is_err());
}
}