├── .gitignore ├── examples ├── rev_lines.rs └── raw_rev_lines.rs ├── Cargo.toml ├── README.md ├── benches ├── iai.rs └── criterion.rs ├── .github └── workflows │ └── actions.yml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/rev_lines.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use rev_lines::RevLines; 4 | 5 | fn main() -> Result<(), Box> { 6 | let file = Cursor::new("Just\na\nfew\nlines\n"); 7 | let rev_lines = RevLines::new(file); 8 | 9 | for line in rev_lines { 10 | println!("{:?}", line); 11 | } 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /examples/raw_rev_lines.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use rev_lines::RawRevLines; 4 | 5 | fn main() -> Result<(), Box> { 6 | let file = Cursor::new(vec![ 7 | b'A', b'B', b'C', b'D', b'E', b'F', b'\n', // some valid UTF-8 in this line 8 | b'X', 252, 253, 254, b'Y', b'\n', // invalid UTF-8 in this line 9 | b'G', b'H', b'I', b'J', b'K', b'\n', // some more valid UTF-8 at the end 10 | ]); 11 | let rev_lines = RawRevLines::new(file); 12 | 13 | for line in rev_lines { 14 | // String::from_utf8_lossy would be another use case 15 | match String::from_utf8(line?) { 16 | Ok(line) => println!("{}", line), 17 | Err(e) => println!("Error: {}", e), 18 | } 19 | } 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rev_lines" 3 | version = "0.3.0" 4 | description = "Rust Iterator for reading files line by line with a buffer in reverse" 5 | repository = "https://github.com/mikeycgto/rev_lines" 6 | documentation = "https://docs.rs/rev_lines" 7 | license = "MIT" 8 | authors = ["Michael Coyne "] 9 | keywords = ["lines", "reverse", "reader", "buffer", "iterator"] 10 | autobenches = false 11 | edition = "2021" 12 | 13 | [dependencies] 14 | thiserror = "1.0.40" 15 | 16 | [dev-dependencies] 17 | iai = { git = "https://github.com/sigaloid/iai", rev = "6c83e942" } 18 | criterion = "0.5" 19 | 20 | [[bench]] 21 | name = "iai" 22 | path = "benches/iai.rs" 23 | harness = false 24 | 25 | [[bench]] 26 | name = "criterion" 27 | path = "benches/criterion.rs" 28 | harness = false 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rev_lines 2 | 3 | [![rev-lines](https://github.com/mjc-gh/rev_lines/actions/workflows/actions.yml/badge.svg)](https://github.com/mjc-gh/rev_lines/actions/workflows/actions.yml) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 5 | [![crates.io](https://img.shields.io/crates/v/rev-lines.svg)](https://crates.io/crates/rev_lines) 6 | 7 | This library provides a small Rust Iterator for reading files line by 8 | line with a buffer in reverse 9 | 10 | ## Documentation 11 | 12 | Documentation is available on [Docs.rs](https://docs.rs/rev_lines). 13 | 14 | ## Example 15 | 16 | ```rust 17 | use std::fs::File; 18 | 19 | use rev_lines::RevLines; 20 | 21 | let file = File::open("README.md").unwrap(); 22 | let rev_lines = RevLines::new(file); 23 | 24 | for line in rev_lines { 25 | println!("{:?}", line); 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /benches/iai.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use rev_lines::RawRevLines; 4 | 5 | const KB: usize = 1024; 6 | const FILE_LENGTH: usize = 20 * KB; 7 | 8 | fn input(file_length: usize, lines_length: u32) -> Vec { 9 | let mut count = 0; 10 | std::iter::from_fn(move || { 11 | count += 1; 12 | 13 | if count % lines_length == 0 { 14 | Some(b'\n') 15 | } else { 16 | Some(b'a') 17 | } 18 | }) 19 | .take(file_length) 20 | .collect() 21 | } 22 | 23 | fn raw_rev_lines_next_line_length_20_buffer_capacity_4096() { 24 | let reader = Cursor::new(input(FILE_LENGTH, 20)); 25 | let mut rev_lines = RawRevLines::with_capacity(4096, reader); 26 | while let Some(_) = rev_lines.next() {} 27 | } 28 | 29 | fn raw_rev_lines_next_line_length_160_buffer_capacity_4096() { 30 | let reader = Cursor::new(input(FILE_LENGTH, 160)); 31 | let mut rev_lines = RawRevLines::with_capacity(4096, reader); 32 | while let Some(_) = rev_lines.next() {} 33 | } 34 | 35 | iai::main!( 36 | raw_rev_lines_next_line_length_20_buffer_capacity_4096, 37 | raw_rev_lines_next_line_length_160_buffer_capacity_4096 38 | ); 39 | -------------------------------------------------------------------------------- /benches/criterion.rs: -------------------------------------------------------------------------------- 1 | extern crate criterion; 2 | use std::io::Cursor; 3 | 4 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 5 | 6 | extern crate rev_lines; 7 | use rev_lines::RawRevLines; 8 | 9 | fn input(file_length: usize, lines_length: u32) -> Vec { 10 | let mut count = 0; 11 | std::iter::from_fn(move || { 12 | count += 1; 13 | 14 | if count % lines_length == 0 { 15 | Some(b'\n') 16 | } else { 17 | Some(b'a') 18 | } 19 | }) 20 | .take(file_length) 21 | .collect() 22 | } 23 | 24 | pub fn criterion_benchmark(c: &mut Criterion) { 25 | for (file_length, line_length, buffer_capacity) in [ 26 | (1000000, 100, 20), 27 | (1000000, 100, 50), 28 | (1000000, 100, 100), 29 | (1000000, 5, 4096), 30 | (1000000, 20, 4096), 31 | (1000000, 50, 4096), 32 | (1000000, 80, 4096), 33 | (1000000, 1000, 4096), 34 | ] { 35 | c.bench_function( 36 | &format!("RawRevLines file_length={file_length} line_length={line_length}, buffer_capacity={buffer_capacity}"), 37 | |b| { 38 | b.iter(|| { 39 | let reader = Cursor::new(input(black_box(file_length), black_box(line_length))); 40 | let mut rev_lines = RawRevLines::with_capacity(buffer_capacity, reader); 41 | while let Some(_) = rev_lines.next() {} 42 | }) 43 | }, 44 | ); 45 | } 46 | } 47 | 48 | criterion_group!(benches, criterion_benchmark); 49 | criterion_main!(benches); 50 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: rev-lines 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | 20 | - name: Run cargo check 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | test: 26 | name: Test Suite 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout sources 30 | uses: actions/checkout@v2 31 | 32 | - name: Install stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | 39 | - name: Run cargo test 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: test 43 | 44 | lints: 45 | name: Lints 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout sources 49 | uses: actions/checkout@v2 50 | 51 | - name: Install stable toolchain 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | profile: minimal 55 | toolchain: stable 56 | override: true 57 | components: rustfmt, clippy 58 | 59 | - name: Run cargo fmt 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: fmt 63 | args: --all -- --check 64 | 65 | - name: Run cargo clippy 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | args: -- -D warnings 70 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ### RevLines 2 | //! 3 | //! This library provides a small Rust Iterator for reading files or 4 | //! any `BufReader` line by line with buffering in reverse. 5 | //! 6 | //! #### Example 7 | //! 8 | //! ``` 9 | //! use std::fs::File; 10 | //! 11 | //! use rev_lines::RevLines; 12 | //! 13 | //! let file = File::open("README.md").unwrap(); 14 | //! let rev_lines = RevLines::new(file); 15 | //! 16 | //! for line in rev_lines { 17 | //! println!("{:?}", line); 18 | //! } 19 | //! ``` 20 | //! 21 | //! If a line with invalid UTF-8 is encountered, the iterator will return `None` next, and stop iterating. 22 | //! 23 | //! This method uses logic borrowed from [uutils/coreutils tail](https://github.com/uutils/coreutils/blob/f2166fed0ad055d363aedff6223701001af090d3/src/tail/tail.rs#L399-L402) 24 | 25 | use std::cmp::min; 26 | use std::io::{self, BufReader, Read, Seek, SeekFrom}; 27 | 28 | use thiserror::Error; 29 | 30 | static DEFAULT_SIZE: usize = 4096; 31 | 32 | static LF_BYTE: u8 = b'\n'; 33 | static CR_BYTE: u8 = b'\r'; 34 | 35 | /// `RevLines` struct 36 | pub struct RawRevLines { 37 | reader: BufReader, 38 | reader_cursor: u64, 39 | buffer: Vec, 40 | buffer_end: usize, 41 | read_len: usize, 42 | was_last_byte_line_feed: bool, 43 | } 44 | 45 | impl RawRevLines { 46 | /// Create a new `RawRevLines` struct from a Reader. 47 | /// Internal buffering for iteration will default to 4096 bytes at a time. 48 | pub fn new(reader: R) -> RawRevLines { 49 | RawRevLines::with_capacity(DEFAULT_SIZE, reader) 50 | } 51 | 52 | /// Create a new `RawRevLines` struct from a Reader`. 53 | /// Internal buffering for iteration will use `cap` bytes at a time. 54 | pub fn with_capacity(cap: usize, reader: R) -> RawRevLines { 55 | RawRevLines { 56 | reader: BufReader::new(reader), 57 | reader_cursor: u64::MAX, 58 | buffer: vec![0; cap], 59 | buffer_end: 0, 60 | read_len: 0, 61 | was_last_byte_line_feed: false, 62 | } 63 | } 64 | 65 | fn init_reader(&mut self) -> io::Result<()> { 66 | // Move cursor to the end of the file and store the cursor position 67 | self.reader_cursor = self.reader.seek(SeekFrom::End(0))?; 68 | // Next read will be the full buffer size or the remaining bytes in the file 69 | self.read_len = min(self.buffer.len(), self.reader_cursor as usize); 70 | // Move cursor just before the next bytes to read 71 | self.reader.seek_relative(-(self.read_len as i64))?; 72 | // Update the cursor position 73 | self.reader_cursor -= self.read_len as u64; 74 | 75 | self.read_to_buffer()?; 76 | 77 | // Handle any trailing new line characters for the reader 78 | // so the first next call does not return Some("") 79 | if self.buffer_end > 0 { 80 | if let Some(last_byte) = self.buffer.get(self.buffer_end - 1) { 81 | if *last_byte == LF_BYTE { 82 | self.buffer_end -= 1; 83 | self.was_last_byte_line_feed = true; 84 | } 85 | } 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | fn read_to_buffer(&mut self) -> io::Result<()> { 92 | // Read the next bytes into the buffer, self.read_len was already prepared for that 93 | self.reader.read_exact(&mut self.buffer[0..self.read_len])?; 94 | // Specify which part of the buffer is valid 95 | self.buffer_end = self.read_len; 96 | 97 | // Determine what the next read length will be 98 | let next_read_len = min(self.buffer.len(), self.reader_cursor as usize); 99 | // Move the cursor just in front of the next read 100 | self.reader 101 | .seek_relative(-((self.read_len + next_read_len) as i64))?; 102 | // Update cursor position 103 | self.reader_cursor -= next_read_len as u64; 104 | 105 | // Store the next read length, it'll be used in the next call 106 | self.read_len = next_read_len; 107 | 108 | Ok(()) 109 | } 110 | 111 | fn next_line(&mut self) -> io::Result>> { 112 | // Reader cursor will only ever be u64::MAX if the reader has not been initialized 113 | // If by some chance the reader is initialized with a file of length u64::MAX this will still work, 114 | // as some read length value is subtracted from the cursor position right away 115 | if self.reader_cursor == u64::MAX { 116 | self.init_reader()?; 117 | } 118 | 119 | // For most sane scenarios, where size of the buffer is greater than the length of the line, 120 | // the result will only contain one and at most two elements, making the flattening trivial. 121 | // At the same time, instead of pushing one element at a time, it allows us to copy a subslice of the buffer, 122 | // which is very performant on modern architectures. 123 | let mut result: Vec> = Vec::new(); 124 | 125 | 'outer: loop { 126 | // Current buffer was read to completion, read new contents 127 | if self.buffer_end == 0 { 128 | // Read the of minimum between the desired 129 | // buffer size or remaining length of the reader 130 | self.read_to_buffer()?; 131 | } 132 | 133 | // If buffer_end is still 0, it means the reader is empty 134 | if self.buffer_end == 0 { 135 | if result.is_empty() { 136 | return Ok(None); 137 | } else { 138 | break; 139 | } 140 | } 141 | 142 | let mut buffer_length = self.buffer_end; 143 | 144 | for ch in self.buffer[..self.buffer_end].iter().rev() { 145 | self.buffer_end -= 1; 146 | // Found a new line character to break on 147 | if *ch == LF_BYTE { 148 | result.push(self.buffer[self.buffer_end + 1..buffer_length].to_vec()); 149 | self.was_last_byte_line_feed = true; 150 | break 'outer; 151 | } 152 | // If previous byte was line feed, skip carriage return 153 | if *ch == CR_BYTE && self.was_last_byte_line_feed { 154 | buffer_length -= 1; 155 | } 156 | self.was_last_byte_line_feed = false; 157 | } 158 | 159 | result.push(self.buffer[..buffer_length].to_vec()); 160 | } 161 | 162 | Ok(Some(result.into_iter().rev().flatten().collect())) 163 | } 164 | } 165 | 166 | impl Iterator for RawRevLines { 167 | type Item = io::Result>; 168 | 169 | fn next(&mut self) -> Option>> { 170 | self.next_line().transpose() 171 | } 172 | } 173 | 174 | #[derive(Debug, Error)] 175 | pub enum RevLinesError { 176 | #[error(transparent)] 177 | Io(#[from] std::io::Error), 178 | #[error(transparent)] 179 | InvalidUtf8(#[from] std::string::FromUtf8Error), 180 | } 181 | 182 | pub struct RevLines(RawRevLines); 183 | 184 | impl RevLines { 185 | /// Create a new `RawRevLines` struct from a Reader. 186 | /// Internal buffering for iteration will default to 4096 bytes at a time. 187 | pub fn new(reader: R) -> RevLines { 188 | RevLines(RawRevLines::new(reader)) 189 | } 190 | 191 | /// Create a new `RawRevLines` struct from a Reader`. 192 | /// Internal buffering for iteration will use `cap` bytes at a time. 193 | pub fn with_capacity(cap: usize, reader: R) -> RevLines { 194 | RevLines(RawRevLines::with_capacity(cap, reader)) 195 | } 196 | } 197 | 198 | impl Iterator for RevLines { 199 | type Item = Result; 200 | 201 | fn next(&mut self) -> Option> { 202 | let line = match self.0.next_line().transpose()? { 203 | Ok(line) => line, 204 | Err(error) => return Some(Err(RevLinesError::Io(error))), 205 | }; 206 | 207 | Some(String::from_utf8(line).map_err(RevLinesError::InvalidUtf8)) 208 | } 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use std::io::{BufReader, Cursor}; 214 | 215 | use crate::{RawRevLines, RevLines}; 216 | 217 | type TestResult = Result<(), Box>; 218 | 219 | #[test] 220 | fn raw_handles_empty_files() -> TestResult { 221 | let file = Cursor::new(Vec::new()); 222 | let mut rev_lines = RawRevLines::new(file); 223 | 224 | assert!(rev_lines.next().transpose()?.is_none()); 225 | 226 | Ok(()) 227 | } 228 | 229 | #[test] 230 | fn raw_handles_file_with_one_line() -> TestResult { 231 | let text = b"ABCD\n".to_vec(); 232 | for cap in 1..(text.len() + 1) { 233 | let file = Cursor::new(&text); 234 | let mut rev_lines = RawRevLines::with_capacity(cap, file); 235 | 236 | assert_eq!(rev_lines.next().transpose()?, Some(b"ABCD".to_vec())); 237 | assert_eq!(rev_lines.next().transpose()?, None); 238 | } 239 | 240 | Ok(()) 241 | } 242 | 243 | #[test] 244 | fn raw_handles_file_with_multi_lines() -> TestResult { 245 | let text = b"ABCDEF\nGHIJK\nLMNOPQRST\nUVWXYZ\n".to_vec(); 246 | for cap in 5..(text.len() + 1) { 247 | let file = Cursor::new(b"ABCDEF\nGHIJK\nLMNOPQRST\nUVWXYZ\n".to_vec()); 248 | let mut rev_lines = RawRevLines::with_capacity(cap, file); 249 | 250 | assert_eq!(rev_lines.next().transpose()?, Some(b"UVWXYZ".to_vec())); 251 | assert_eq!(rev_lines.next().transpose()?, Some(b"LMNOPQRST".to_vec())); 252 | assert_eq!(rev_lines.next().transpose()?, Some(b"GHIJK".to_vec())); 253 | assert_eq!(rev_lines.next().transpose()?, Some(b"ABCDEF".to_vec())); 254 | assert_eq!(rev_lines.next().transpose()?, None); 255 | } 256 | 257 | Ok(()) 258 | } 259 | 260 | #[test] 261 | fn raw_handles_windows_file_with_multi_lines() -> TestResult { 262 | let text = b"ABCDEF\r\nGHIJK\r\nLMNOP\rQRST\r\nUVWXYZ\r\n".to_vec(); 263 | for cap in 1..(text.len() + 1) { 264 | let file = Cursor::new(&text); 265 | let mut rev_lines = RawRevLines::with_capacity(cap, file); 266 | 267 | assert_eq!(rev_lines.next().transpose()?, Some(b"UVWXYZ".to_vec())); 268 | assert_eq!(rev_lines.next().transpose()?, Some(b"LMNOP\rQRST".to_vec())); // bare CR not stripped 269 | assert_eq!(rev_lines.next().transpose()?, Some(b"GHIJK".to_vec())); 270 | assert_eq!(rev_lines.next().transpose()?, Some(b"ABCDEF".to_vec())); 271 | assert_eq!(rev_lines.next().transpose()?, None); 272 | } 273 | 274 | Ok(()) 275 | } 276 | 277 | #[test] 278 | fn raw_handles_file_with_blank_lines() -> TestResult { 279 | let file = Cursor::new(b"ABCD\n\nXYZ\n\n\n".to_vec()); 280 | let mut rev_lines = RawRevLines::new(file); 281 | 282 | assert_eq!(rev_lines.next().transpose()?, Some(b"".to_vec())); 283 | assert_eq!(rev_lines.next().transpose()?, Some(b"".to_vec())); 284 | assert_eq!(rev_lines.next().transpose()?, Some(b"XYZ".to_vec())); 285 | assert_eq!(rev_lines.next().transpose()?, Some(b"".to_vec())); 286 | assert_eq!(rev_lines.next().transpose()?, Some(b"ABCD".to_vec())); 287 | assert_eq!(rev_lines.next().transpose()?, None); 288 | 289 | Ok(()) 290 | } 291 | 292 | #[test] 293 | fn raw_handles_file_with_invalid_utf8() -> TestResult { 294 | let file = BufReader::new(Cursor::new(vec![ 295 | b'A', b'B', b'C', b'D', b'E', b'F', b'\n', // some valid UTF-8 in this line 296 | b'X', 252, 253, 254, b'Y', b'\n', // invalid UTF-8 in this line 297 | b'G', b'H', b'I', b'J', b'K', b'\n', // some more valid UTF-8 at the end 298 | ])); 299 | let mut rev_lines = RawRevLines::new(file); 300 | assert_eq!(rev_lines.next().transpose()?, Some(b"GHIJK".to_vec())); 301 | assert_eq!( 302 | rev_lines.next().transpose()?, 303 | Some(vec![b'X', 252, 253, 254, b'Y']) 304 | ); 305 | assert_eq!(rev_lines.next().transpose()?, Some(b"ABCDEF".to_vec())); 306 | assert_eq!(rev_lines.next().transpose()?, None); 307 | 308 | Ok(()) 309 | } 310 | 311 | #[test] 312 | fn it_handles_empty_files() -> TestResult { 313 | let file = Cursor::new(Vec::new()); 314 | let mut rev_lines = RevLines::new(file); 315 | 316 | assert!(rev_lines.next().transpose()?.is_none()); 317 | 318 | Ok(()) 319 | } 320 | 321 | #[test] 322 | fn it_handles_file_with_one_line() -> TestResult { 323 | let file = Cursor::new(b"ABCD\n".to_vec()); 324 | let mut rev_lines = RevLines::new(file); 325 | 326 | assert_eq!(rev_lines.next().transpose()?, Some("ABCD".to_string())); 327 | assert_eq!(rev_lines.next().transpose()?, None); 328 | 329 | Ok(()) 330 | } 331 | 332 | #[test] 333 | fn it_handles_file_with_multi_lines() -> TestResult { 334 | let file = Cursor::new(b"ABCDEF\nGHIJK\nLMNOPQRST\nUVWXYZ\n".to_vec()); 335 | let mut rev_lines = RevLines::new(file); 336 | 337 | assert_eq!(rev_lines.next().transpose()?, Some("UVWXYZ".to_string())); 338 | assert_eq!(rev_lines.next().transpose()?, Some("LMNOPQRST".to_string())); 339 | assert_eq!(rev_lines.next().transpose()?, Some("GHIJK".to_string())); 340 | assert_eq!(rev_lines.next().transpose()?, Some("ABCDEF".to_string())); 341 | assert_eq!(rev_lines.next().transpose()?, None); 342 | 343 | Ok(()) 344 | } 345 | 346 | #[test] 347 | fn it_handles_file_with_blank_lines() -> TestResult { 348 | let file = Cursor::new(b"ABCD\n\nXYZ\n\n\n".to_vec()); 349 | let mut rev_lines = RevLines::new(file); 350 | 351 | assert_eq!(rev_lines.next().transpose()?, Some("".to_string())); 352 | assert_eq!(rev_lines.next().transpose()?, Some("".to_string())); 353 | assert_eq!(rev_lines.next().transpose()?, Some("XYZ".to_string())); 354 | assert_eq!(rev_lines.next().transpose()?, Some("".to_string())); 355 | assert_eq!(rev_lines.next().transpose()?, Some("ABCD".to_string())); 356 | assert_eq!(rev_lines.next().transpose()?, None); 357 | 358 | Ok(()) 359 | } 360 | 361 | #[test] 362 | fn it_handles_file_with_multi_lines_and_with_capacity() -> TestResult { 363 | let file = Cursor::new(b"ABCDEF\nGHIJK\nLMNOPQRST\nUVWXYZ\n".to_vec()); 364 | let mut rev_lines = RevLines::with_capacity(5, file); 365 | 366 | assert_eq!(rev_lines.next().transpose()?, Some("UVWXYZ".to_string())); 367 | assert_eq!(rev_lines.next().transpose()?, Some("LMNOPQRST".to_string())); 368 | assert_eq!(rev_lines.next().transpose()?, Some("GHIJK".to_string())); 369 | assert_eq!(rev_lines.next().transpose()?, Some("ABCDEF".to_string())); 370 | assert_eq!(rev_lines.next().transpose()?, None); 371 | 372 | Ok(()) 373 | } 374 | 375 | #[test] 376 | fn it_handles_file_with_invalid_utf8() -> TestResult { 377 | let file = BufReader::new(Cursor::new(vec![ 378 | b'A', b'B', b'C', b'D', b'E', b'F', b'\n', // some valid UTF-8 in this line 379 | b'X', 252, 253, 254, b'Y', b'\n', // invalid UTF-8 in this line 380 | b'G', b'H', b'I', b'J', b'K', b'\n', // some more valid UTF-8 at the end 381 | ])); 382 | let mut rev_lines = RevLines::new(file); 383 | assert_eq!(rev_lines.next().transpose()?, Some("GHIJK".to_string())); 384 | assert!(rev_lines.next().transpose().is_err()); 385 | assert_eq!(rev_lines.next().transpose()?, Some("ABCDEF".to_string())); 386 | assert_eq!(rev_lines.next().transpose()?, None); 387 | 388 | Ok(()) 389 | } 390 | } 391 | --------------------------------------------------------------------------------