1use crate::error::ServerError;
7use std::fs::File;
8use std::io::{BufReader, Read};
9use std::path::Path;
10
11#[derive(Debug)]
25pub struct ChunkStream {
26 reader: BufReader<File>,
27 chunk_size: usize,
28 exhausted: bool,
29}
30
31impl ChunkStream {
32 pub fn from_file(
51 path: &Path,
52 chunk_size: usize,
53 ) -> Result<Self, ServerError> {
54 let file = File::open(path)?;
55 Ok(Self {
56 reader: BufReader::new(file),
57 chunk_size: chunk_size.max(1),
58 exhausted: false,
59 })
60 }
61}
62
63impl Iterator for ChunkStream {
64 type Item = Result<Vec<u8>, ServerError>;
65
66 fn next(&mut self) -> Option<Self::Item> {
67 read_next_chunk(
68 &mut self.reader,
69 self.chunk_size,
70 &mut self.exhausted,
71 )
72 }
73}
74
75fn read_next_chunk<R: Read>(
76 reader: &mut R,
77 chunk_size: usize,
78 exhausted: &mut bool,
79) -> Option<Result<Vec<u8>, ServerError>> {
80 if *exhausted {
81 return None;
82 }
83
84 let mut buf = vec![0_u8; chunk_size];
85 match reader.read(&mut buf) {
86 Ok(0) => {
87 *exhausted = true;
88 None
89 }
90 Ok(n) => {
91 buf.truncate(n);
92 Some(Ok(buf))
93 }
94 Err(err) => {
95 *exhausted = true;
96 Some(Err(ServerError::Io(err)))
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::io;
105 use tempfile::TempDir;
106
107 struct ErrReader;
108
109 impl Read for ErrReader {
110 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
111 Err(io::Error::other("boom"))
112 }
113 }
114
115 #[test]
116 fn helper_maps_read_errors_and_marks_exhausted() {
117 let mut exhausted = false;
118 let mut reader = ErrReader;
119 let item = read_next_chunk(&mut reader, 8, &mut exhausted);
120 assert!(matches!(item, Some(Err(ServerError::Io(_)))));
121 assert!(exhausted);
122 }
123
124 #[test]
125 fn helper_returns_none_when_already_exhausted() {
126 let mut exhausted = true;
127 let mut reader = io::empty();
128 assert!(
129 read_next_chunk(&mut reader, 4, &mut exhausted).is_none()
130 );
131 }
132
133 #[test]
134 fn streams_file_in_chunks() {
135 let tmp = TempDir::new().expect("tmp");
136 let file = tmp.path().join("data.txt");
137 std::fs::write(&file, b"abcdefgh").expect("write");
138
139 let chunks: Result<Vec<Vec<u8>>, _> =
140 ChunkStream::from_file(&file, 3).expect("open").collect();
141
142 assert_eq!(
143 chunks.expect("chunks"),
144 vec![b"abc".to_vec(), b"def".to_vec(), b"gh".to_vec()]
145 );
146 }
147
148 #[test]
149 fn missing_file_returns_io_error() {
150 let tmp = TempDir::new().expect("tmp");
151 let missing = tmp.path().join("does-not-exist.txt");
152 let result = ChunkStream::from_file(&missing, 4);
153 assert!(matches!(result, Err(ServerError::Io(_))));
154 }
155
156 #[test]
157 fn returns_none_after_stream_is_exhausted() {
158 let tmp = TempDir::new().expect("tmp");
159 let file = tmp.path().join("single-byte.txt");
160 std::fs::write(&file, b"x").expect("write");
161 let mut stream =
162 ChunkStream::from_file(&file, 1).expect("stream open");
163
164 assert!(
165 matches!(stream.next(), Some(Ok(chunk)) if chunk == b"x")
166 );
167 assert!(stream.next().is_none());
168 assert!(stream.next().is_none());
169 }
170}