Skip to main content

uu_csplit/
csplit.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5// spell-checker:ignore rustdoc
6#![allow(rustdoc::private_intra_doc_links)]
7
8use std::borrow::Borrow;
9use std::cmp::Ordering;
10use std::ffi::OsString;
11use std::io::{self, BufReader, ErrorKind};
12use std::{
13    fs::{File, remove_file},
14    io::{BufRead, BufWriter, Write},
15};
16
17use clap::{Arg, ArgAction, ArgMatches, Command};
18use regex::Regex;
19use uucore::display::Quotable;
20use uucore::error::{FromIo, UResult};
21use uucore::format_usage;
22
23mod csplit_error;
24mod patterns;
25mod split_name;
26
27use crate::csplit_error::CsplitError;
28use crate::split_name::SplitName;
29
30use uucore::translate;
31
32mod options {
33    pub const SUFFIX_FORMAT: &str = "suffix-format";
34    pub const SUPPRESS_MATCHED: &str = "suppress-matched";
35    pub const DIGITS: &str = "digits";
36    pub const PREFIX: &str = "prefix";
37    pub const KEEP_FILES: &str = "keep-files";
38    pub const QUIET: &str = "quiet";
39    pub const ELIDE_EMPTY_FILES: &str = "elide-empty-files";
40    pub const FILE: &str = "file";
41    pub const PATTERN: &str = "pattern";
42}
43
44/// Command line options for csplit.
45pub struct CsplitOptions {
46    split_name: SplitName,
47    keep_files: bool,
48    quiet: bool,
49    elide_empty_files: bool,
50    suppress_matched: bool,
51}
52
53impl CsplitOptions {
54    fn new(matches: &ArgMatches) -> Result<Self, CsplitError> {
55        let keep_files = matches.get_flag(options::KEEP_FILES);
56        let quiet = matches.get_flag(options::QUIET);
57        let elide_empty_files = matches.get_flag(options::ELIDE_EMPTY_FILES);
58        let suppress_matched = matches.get_flag(options::SUPPRESS_MATCHED);
59
60        Ok(Self {
61            split_name: SplitName::new(
62                matches.get_one::<String>(options::PREFIX).cloned(),
63                matches.get_one::<String>(options::SUFFIX_FORMAT).cloned(),
64                matches.get_one::<String>(options::DIGITS).cloned(),
65            )?,
66            keep_files,
67            quiet,
68            elide_empty_files,
69            suppress_matched,
70        })
71    }
72}
73
74pub struct LinesWithNewlines<T: BufRead> {
75    inner: T,
76}
77
78impl<T: BufRead> LinesWithNewlines<T> {
79    fn new(s: T) -> Self {
80        Self { inner: s }
81    }
82}
83
84impl<T: BufRead> Iterator for LinesWithNewlines<T> {
85    type Item = io::Result<String>;
86
87    fn next(&mut self) -> Option<Self::Item> {
88        fn ret(v: Vec<u8>) -> io::Result<String> {
89            String::from_utf8(v).map_err(|_| {
90                io::Error::new(ErrorKind::InvalidData, translate!("csplit-stream-not-utf8"))
91            })
92        }
93
94        let mut v = Vec::new();
95        match self.inner.read_until(b'\n', &mut v) {
96            Ok(0) => None,
97            Ok(_) => Some(ret(v)),
98            Err(e) => Some(Err(e)),
99        }
100    }
101}
102
103/// Splits a file into severals according to the command line patterns.
104///
105/// # Errors
106///
107/// - [`io::Error`] if there is some problem reading/writing from/to a file.
108/// - [`CsplitError::LineOutOfRange`] if the line number pattern is larger than the number of input
109///   lines.
110/// - [`CsplitError::LineOutOfRangeOnRepetition`], like previous but after applying the pattern
111///   more than once.
112/// - [`CsplitError::MatchNotFound`] if no line matched a regular expression.
113/// - [`CsplitError::MatchNotFoundOnRepetition`], like previous but after applying the pattern
114///   more than once.
115pub fn csplit<T>(options: &CsplitOptions, patterns: &[&str], input: T) -> Result<(), CsplitError>
116where
117    T: BufRead,
118{
119    let enumerated_input_lines = LinesWithNewlines::new(input)
120        .map(|line| line.map_err_context(|| translate!("csplit-read-error")))
121        .enumerate();
122    let mut input_iter = InputSplitter::new(enumerated_input_lines);
123    let mut split_writer = SplitWriter::new(options);
124    let patterns_vec: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?;
125    let all_up_to_line = patterns_vec
126        .iter()
127        .all(|p| matches!(p, patterns::Pattern::UpToLine(_, _)));
128    let ret = do_csplit(&mut split_writer, patterns_vec, &mut input_iter);
129
130    // consume the rest, unless there was an error
131    let ret = if ret.is_ok() {
132        input_iter.rewind_buffer();
133        if let Some((_, line)) = input_iter.next() {
134            // There is remaining input: create a final split and copy remainder
135            split_writer.new_writer()?;
136            split_writer.writeln(&line?)?;
137            for (_, line) in input_iter {
138                split_writer.writeln(&line?)?;
139            }
140            split_writer.finish_split()
141        } else if all_up_to_line && options.suppress_matched {
142            // GNU semantics for integer patterns with --suppress-matched:
143            // even if no remaining input, create a final (possibly empty) split
144            split_writer.new_writer()?;
145            split_writer.finish_split()
146        } else {
147            Ok(())
148        }
149    } else {
150        ret
151    };
152    // delete files on error by default
153    if ret.is_err() && !options.keep_files {
154        split_writer.delete_all_splits()?;
155    }
156    ret
157}
158
159fn do_csplit<I>(
160    split_writer: &mut SplitWriter,
161    patterns: Vec<patterns::Pattern>,
162    input_iter: &mut InputSplitter<I>,
163) -> Result<(), CsplitError>
164where
165    I: Iterator<Item = (usize, UResult<String>)>,
166{
167    // split the file based on patterns
168    for pattern in patterns {
169        let pattern_as_str = pattern.to_string();
170        let is_skip = matches!(pattern, patterns::Pattern::SkipToMatch(_, _, _));
171        match pattern {
172            patterns::Pattern::UpToLine(n, ex) => {
173                let mut up_to_line = n;
174                for (_, ith) in ex.iter() {
175                    split_writer.new_writer()?;
176                    match split_writer.do_to_line(&pattern_as_str, up_to_line, input_iter) {
177                        // the error happened when applying the pattern more than once
178                        Err(CsplitError::LineOutOfRange(_)) if ith != 1 => {
179                            return Err(CsplitError::LineOutOfRangeOnRepetition(
180                                pattern_as_str,
181                                ith - 1,
182                            ));
183                        }
184                        Err(err) => return Err(err),
185                        // continue the splitting process
186                        Ok(()) => (),
187                    }
188                    up_to_line += n;
189                }
190            }
191            patterns::Pattern::UpToMatch(regex, offset, ex)
192            | patterns::Pattern::SkipToMatch(regex, offset, ex) => {
193                for (max, ith) in ex.iter() {
194                    if is_skip {
195                        // when skipping a part of the input, no writer is created
196                        split_writer.as_dev_null();
197                    } else {
198                        split_writer.new_writer()?;
199                    }
200                    match (
201                        split_writer.do_to_match(&pattern_as_str, &regex, offset, input_iter),
202                        max,
203                    ) {
204                        // in case of ::pattern::ExecutePattern::Always, then it's fine not to find a
205                        // matching line
206                        (Err(CsplitError::MatchNotFound(_)), None) => {
207                            return Ok(());
208                        }
209                        // the error happened when applying the pattern more than once
210                        (Err(CsplitError::MatchNotFound(_)), Some(m)) if m != 1 && ith != 1 => {
211                            return Err(CsplitError::MatchNotFoundOnRepetition(
212                                pattern_as_str,
213                                ith - 1,
214                            ));
215                        }
216                        (Err(err), _) => return Err(err),
217                        // continue the splitting process
218                        (Ok(()), _) => (),
219                    }
220                }
221            }
222        }
223    }
224    Ok(())
225}
226
227/// Write a portion of the input file into a split which filename is based on an incrementing
228/// counter.
229struct SplitWriter<'a> {
230    /// the options set through the command line
231    options: &'a CsplitOptions,
232    /// a split counter
233    counter: usize,
234    /// the writer to the current split
235    current_writer: Option<BufWriter<File>>,
236    /// the size in bytes of the current split
237    size: usize,
238    /// flag to indicate that no content should be written to a split
239    dev_null: bool,
240}
241
242impl Drop for SplitWriter<'_> {
243    fn drop(&mut self) {
244        if self.options.elide_empty_files && self.size == 0 {
245            let file_name = self.options.split_name.get(self.counter);
246            // In the case of `echo a | csplit -z - %a%1`, the file
247            // `xx00` does not exist because the positive offset
248            // advanced past the end of the input. Since there is no
249            // file to remove in that case, `remove_file` would return
250            // an error, so we just ignore it.
251            let _ = remove_file(file_name);
252        }
253    }
254}
255
256impl SplitWriter<'_> {
257    fn new(options: &CsplitOptions) -> SplitWriter<'_> {
258        SplitWriter {
259            options,
260            counter: 0,
261            current_writer: None,
262            size: 0,
263            dev_null: false,
264        }
265    }
266
267    /// Creates a new split and returns its filename.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if creating the split file fails.
272    fn new_writer(&mut self) -> Result<(), CsplitError> {
273        let file_name = self.options.split_name.get(self.counter);
274        let file = File::create(&file_name)
275            .map_err_context(|| file_name.clone())
276            .map_err(CsplitError::from)?;
277        self.current_writer = Some(BufWriter::new(file));
278        self.counter += 1;
279        self.size = 0;
280        self.dev_null = false;
281        Ok(())
282    }
283
284    /// The current split will not keep any of the read input lines.
285    fn as_dev_null(&mut self) {
286        self.dev_null = true;
287    }
288
289    /// Writes the line to the current split.
290    /// If `self.dev_null` is true, then the line is discarded.
291    ///
292    /// # Errors
293    ///
294    /// Some [`io::Error`] may occur when attempting to write the line.
295    fn writeln(&mut self, line: &str) -> io::Result<()> {
296        if !self.dev_null {
297            if let Some(ref mut current_writer) = self.current_writer {
298                let bytes = line.as_bytes();
299                current_writer.write_all(bytes)?;
300                self.size += bytes.len();
301            } else {
302                panic!("{}", translate!("csplit-write-split-not-created"))
303            }
304        }
305        Ok(())
306    }
307
308    /// Perform some operations after completing a split, i.e., either remove it
309    /// if the [`options::ELIDE_EMPTY_FILES`] option is enabled, or print how much bytes were written
310    /// to it if [`options::QUIET`] is disabled.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if flushing the writer fails.
315    fn finish_split(&mut self) -> Result<(), CsplitError> {
316        if !self.dev_null {
317            // Flush the writer to ensure all data is written and errors are detected
318            if let Some(ref mut writer) = self.current_writer {
319                let file_name = self.options.split_name.get(self.counter - 1);
320                writer
321                    .flush()
322                    .map_err_context(|| file_name.clone())
323                    .map_err(CsplitError::from)?;
324            }
325            if self.options.elide_empty_files && self.size == 0 {
326                self.counter -= 1;
327            } else if !self.options.quiet {
328                println!("{}", self.size);
329            }
330        }
331        Ok(())
332    }
333
334    /// Removes all the split files that were created.
335    ///
336    /// # Errors
337    ///
338    /// Returns an [`io::Error`] if there was a problem removing a split.
339    fn delete_all_splits(&self) -> io::Result<()> {
340        let mut ret = Ok(());
341        for ith in 0..self.counter {
342            let file_name = self.options.split_name.get(ith);
343            if let Err(err) = remove_file(file_name) {
344                ret = Err(err);
345            }
346        }
347        ret
348    }
349
350    /// Split the input stream up to the line number `n`.
351    ///
352    /// If the line number `n` is smaller than the current position in the input, then an empty
353    /// split is created.
354    ///
355    /// # Errors
356    ///
357    /// In addition to errors reading/writing from/to a file, if the line number
358    /// `n` is greater than the total available lines, then a
359    /// [`CsplitError::LineOutOfRange`] error is returned.
360    fn do_to_line<I>(
361        &mut self,
362        pattern_as_str: &str,
363        n: usize,
364        input_iter: &mut InputSplitter<I>,
365    ) -> Result<(), CsplitError>
366    where
367        I: Iterator<Item = (usize, UResult<String>)>,
368    {
369        input_iter.rewind_buffer();
370        input_iter.set_size_of_buffer(1);
371
372        let mut ret = Err(CsplitError::LineOutOfRange(pattern_as_str.to_string()));
373        while let Some((ln, line)) = input_iter.next() {
374            let line = line?;
375            match n.cmp(&(&ln + 1)) {
376                Ordering::Less => {
377                    assert!(
378                        input_iter.add_line_to_buffer(ln, line).is_none(),
379                        "the buffer is big enough to contain 1 line"
380                    );
381                    ret = Ok(());
382                    break;
383                }
384                Ordering::Equal => {
385                    assert!(
386                        self.options.suppress_matched
387                            || input_iter.add_line_to_buffer(ln, line).is_none(),
388                        "the buffer is big enough to contain 1 line"
389                    );
390                    ret = Ok(());
391                    break;
392                }
393                Ordering::Greater => (),
394            }
395            self.writeln(&line)?;
396        }
397        self.finish_split()?;
398        ret
399    }
400
401    /// Read lines up to the line matching a [`Regex`]. With a non-zero offset,
402    /// the block of relevant lines can be extended (if positive), or reduced
403    /// (if negative).
404    ///
405    /// # Errors
406    ///
407    /// In addition to errors reading/writing from/to a file, the following errors may be returned:
408    /// - if no line matched, an [`CsplitError::MatchNotFound`].
409    /// - if there are not enough lines to accommodate the offset, an
410    ///   [`CsplitError::LineOutOfRange`].
411    #[allow(clippy::cognitive_complexity)]
412    fn do_to_match<I>(
413        &mut self,
414        pattern_as_str: &str,
415        regex: &Regex,
416        mut offset: i32,
417        input_iter: &mut InputSplitter<I>,
418    ) -> Result<(), CsplitError>
419    where
420        I: Iterator<Item = (usize, UResult<String>)>,
421    {
422        if offset >= 0 {
423            // The offset is zero or positive, no need for a buffer on the lines read.
424            // NOTE: drain the buffer of input_iter, no match should be done within.
425            for line in input_iter.drain_buffer() {
426                self.writeln(&line)?;
427            }
428            // retain the matching line
429            input_iter.set_size_of_buffer(1);
430
431            while let Some((ln, line)) = input_iter.next() {
432                let line = line?;
433                let l = line
434                    .strip_suffix("\r\n")
435                    .unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line));
436                if regex.is_match(l) {
437                    let mut next_line_suppress_matched = false;
438                    match (self.options.suppress_matched, offset) {
439                        // no offset, add the line to the next split
440                        (false, 0) => {
441                            assert!(
442                                input_iter.add_line_to_buffer(ln, line).is_none(),
443                                "the buffer is big enough to contain 1 line"
444                            );
445                        }
446                        // a positive offset, some more lines need to be added to the current split
447                        (false, _) => self.writeln(&line)?,
448                        // suppress matched option true, but there is a positive offset, so the line is printed
449                        (true, 1..) => {
450                            next_line_suppress_matched = true;
451                            self.writeln(&line)?;
452                        }
453                        _ => (),
454                    }
455                    offset -= 1;
456
457                    // write the extra lines required by the offset
458                    while offset > 0 {
459                        if let Some((_, line)) = input_iter.next() {
460                            self.writeln(&line?)?;
461                        } else {
462                            self.finish_split()?;
463                            return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string()));
464                        }
465                        offset -= 1;
466                    }
467                    self.finish_split()?;
468
469                    // if we have to suppress one line after we take the next and do nothing
470                    if next_line_suppress_matched {
471                        input_iter.next();
472                    }
473                    return Ok(());
474                }
475                self.writeln(&line)?;
476            }
477        } else {
478            // With a negative offset we use a buffer to keep the lines within the offset.
479            // NOTE: do not drain the buffer of input_iter, in case of an LineOutOfRange error
480            // but do not rewind it either since no match should be done within.
481            // The consequence is that the buffer may already be full with lines from a previous
482            // split, which is taken care of when calling `shrink_buffer_to_size`.
483            let offset_usize = -offset as usize;
484            input_iter.set_size_of_buffer(offset_usize);
485            while let Some((ln, line)) = input_iter.next() {
486                let line = line?;
487                let l = line
488                    .strip_suffix("\r\n")
489                    .unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line));
490                if regex.is_match(l) {
491                    for line in input_iter.shrink_buffer_to_size() {
492                        self.writeln(&line)?;
493                    }
494                    if self.options.suppress_matched {
495                        // since offset_usize is for sure greater than 0
496                        // the first element of the buffer should be removed and this
497                        // line inserted to be coherent with GNU implementation
498                        input_iter.add_line_to_buffer(ln, line);
499                    } else {
500                        // add 1 to the buffer size to make place for the matched line
501                        input_iter.set_size_of_buffer(offset_usize + 1);
502                        assert!(
503                            input_iter.add_line_to_buffer(ln, line).is_none(),
504                            "should be big enough to hold every lines"
505                        );
506                    }
507
508                    self.finish_split()?;
509                    if input_iter.buffer_len() < offset_usize {
510                        return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string()));
511                    }
512                    return Ok(());
513                }
514                if let Some(line) = input_iter.add_line_to_buffer(ln, line) {
515                    self.writeln(&line)?;
516                }
517            }
518            // no match, drain the buffer into the current split
519            for line in input_iter.drain_buffer() {
520                self.writeln(&line)?;
521            }
522        }
523
524        self.finish_split()?;
525        Err(CsplitError::MatchNotFound(pattern_as_str.to_string()))
526    }
527}
528
529/// An iterator which can output items from a buffer filled externally.
530/// This is used to pass matching lines to the next split and to support patterns with a negative offset.
531struct InputSplitter<I>
532where
533    I: Iterator<Item = (usize, UResult<String>)>,
534{
535    iter: I,
536    buffer: Vec<<I as Iterator>::Item>,
537    /// the number of elements the buffer may hold
538    size: usize,
539    /// flag to indicate content off the buffer should be returned instead of off the wrapped
540    /// iterator
541    rewind: bool,
542}
543
544impl<I> InputSplitter<I>
545where
546    I: Iterator<Item = (usize, UResult<String>)>,
547{
548    fn new(iter: I) -> Self {
549        Self {
550            iter,
551            buffer: Vec::new(),
552            rewind: false,
553            size: 1,
554        }
555    }
556
557    /// Rewind the iteration by outputting the buffer's content.
558    fn rewind_buffer(&mut self) {
559        self.rewind = true;
560    }
561
562    /// Shrink the buffer so that its length is equal to the set size, returning an iterator for
563    /// the elements that were too much.
564    fn shrink_buffer_to_size(&mut self) -> impl Iterator<Item = String> + '_ {
565        let shrink_offset = if self.buffer.len() > self.size {
566            self.buffer.len() - self.size
567        } else {
568            0
569        };
570        self.buffer
571            .drain(..shrink_offset)
572            .map(|(_, line)| line.unwrap())
573    }
574
575    /// Drain the content of the buffer.
576    fn drain_buffer(&mut self) -> impl Iterator<Item = String> + '_ {
577        self.buffer.drain(..).map(|(_, line)| line.unwrap())
578    }
579
580    /// Set the maximum number of lines to keep.
581    fn set_size_of_buffer(&mut self, size: usize) {
582        self.size = size;
583    }
584
585    /// Add a line to the buffer. If the buffer has `self.size` elements, then its head is removed and
586    /// the new line is pushed to the buffer. The removed head is then available in the returned
587    /// option.
588    fn add_line_to_buffer(&mut self, ln: usize, line: String) -> Option<String> {
589        if self.rewind {
590            self.buffer.insert(0, (ln, Ok(line)));
591            None
592        } else if self.buffer.len() >= self.size {
593            let (_, head_line) = self.buffer.remove(0);
594            self.buffer.push((ln, Ok(line)));
595            Some(head_line.unwrap())
596        } else {
597            self.buffer.push((ln, Ok(line)));
598            None
599        }
600    }
601
602    /// Returns the number of lines stored in the buffer
603    fn buffer_len(&self) -> usize {
604        self.buffer.len()
605    }
606}
607
608impl<I> Iterator for InputSplitter<I>
609where
610    I: Iterator<Item = (usize, UResult<String>)>,
611{
612    type Item = <I as Iterator>::Item;
613
614    fn next(&mut self) -> Option<Self::Item> {
615        if self.rewind {
616            if !self.buffer.is_empty() {
617                return Some(self.buffer.remove(0));
618            }
619            self.rewind = false;
620        }
621        self.iter.next()
622    }
623}
624
625#[uucore::main]
626pub fn uumain(args: impl uucore::Args) -> UResult<()> {
627    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
628
629    // get the file to split
630    let file_name = matches.get_one::<OsString>(options::FILE).unwrap();
631
632    // get the patterns to split on
633    let patterns: Vec<_> = matches
634        .get_many::<String>(options::PATTERN)
635        .unwrap()
636        .map(Borrow::borrow)
637        .collect();
638    let options = CsplitOptions::new(&matches)?;
639    if file_name == "-" {
640        let stdin = io::stdin();
641        Ok(csplit(&options, &patterns, stdin.lock())?)
642    } else {
643        let file = File::open(file_name)
644            .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?;
645        Ok(csplit(&options, &patterns, BufReader::new(file))?)
646    }
647}
648
649pub fn uu_app() -> Command {
650    Command::new("csplit")
651        .version(uucore::crate_version!())
652        .help_template(uucore::localized_help_template(uucore::util_name()))
653        .about(translate!("csplit-about"))
654        .override_usage(format_usage(&translate!("csplit-usage")))
655        .args_override_self(true)
656        .infer_long_args(true)
657        .arg(
658            Arg::new(options::SUFFIX_FORMAT)
659                .short('b')
660                .long(options::SUFFIX_FORMAT)
661                .value_name("FORMAT")
662                .help(translate!("csplit-help-suffix-format")),
663        )
664        .arg(
665            Arg::new(options::PREFIX)
666                .short('f')
667                .long(options::PREFIX)
668                .value_name("PREFIX")
669                .help(translate!("csplit-help-prefix")),
670        )
671        .arg(
672            Arg::new(options::KEEP_FILES)
673                .short('k')
674                .long(options::KEEP_FILES)
675                .help(translate!("csplit-help-keep-files"))
676                .action(ArgAction::SetTrue),
677        )
678        .arg(
679            Arg::new(options::SUPPRESS_MATCHED)
680                .long(options::SUPPRESS_MATCHED)
681                .help(translate!("csplit-help-suppress-matched"))
682                .action(ArgAction::SetTrue),
683        )
684        .arg(
685            Arg::new(options::DIGITS)
686                .short('n')
687                .long(options::DIGITS)
688                .value_name("DIGITS")
689                .help(translate!("csplit-help-digits")),
690        )
691        .arg(
692            Arg::new(options::QUIET)
693                .short('q')
694                .long(options::QUIET)
695                .visible_short_alias('s')
696                .visible_alias("silent")
697                .help(translate!("csplit-help-quiet"))
698                .action(ArgAction::SetTrue),
699        )
700        .arg(
701            Arg::new(options::ELIDE_EMPTY_FILES)
702                .short('z')
703                .long(options::ELIDE_EMPTY_FILES)
704                .help(translate!("csplit-help-elide-empty-files"))
705                .action(ArgAction::SetTrue),
706        )
707        .arg(
708            Arg::new(options::FILE)
709                .hide(true)
710                .required(true)
711                .value_hint(clap::ValueHint::FilePath)
712                .value_parser(clap::value_parser!(OsString)),
713        )
714        .arg(
715            Arg::new(options::PATTERN)
716                .hide(true)
717                .action(ArgAction::Append)
718                .required(true),
719        )
720        .after_help(translate!("csplit-after-help"))
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    #[allow(clippy::cognitive_complexity)]
729    fn input_splitter() {
730        let input = vec![
731            Ok(String::from("aaa")),
732            Ok(String::from("bbb")),
733            Ok(String::from("ccc")),
734            Ok(String::from("ddd")),
735        ];
736        let mut input_splitter = InputSplitter::new(input.into_iter().enumerate());
737
738        input_splitter.set_size_of_buffer(2);
739        assert_eq!(input_splitter.buffer_len(), 0);
740
741        match input_splitter.next() {
742            Some((0, Ok(line))) => {
743                assert_eq!(line, String::from("aaa"));
744                assert_eq!(input_splitter.add_line_to_buffer(0, line), None);
745                assert_eq!(input_splitter.buffer_len(), 1);
746            }
747            item => panic!("wrong item: {item:?}"),
748        }
749
750        match input_splitter.next() {
751            Some((1, Ok(line))) => {
752                assert_eq!(line, String::from("bbb"));
753                assert_eq!(input_splitter.add_line_to_buffer(1, line), None);
754                assert_eq!(input_splitter.buffer_len(), 2);
755            }
756            item => panic!("wrong item: {item:?}"),
757        }
758
759        match input_splitter.next() {
760            Some((2, Ok(line))) => {
761                assert_eq!(line, String::from("ccc"));
762                assert_eq!(
763                    input_splitter.add_line_to_buffer(2, line),
764                    Some(String::from("aaa"))
765                );
766                assert_eq!(input_splitter.buffer_len(), 2);
767            }
768            item => panic!("wrong item: {item:?}"),
769        }
770
771        input_splitter.rewind_buffer();
772
773        match input_splitter.next() {
774            Some((1, Ok(line))) => {
775                assert_eq!(line, String::from("bbb"));
776                assert_eq!(input_splitter.buffer_len(), 1);
777            }
778            item => panic!("wrong item: {item:?}"),
779        }
780
781        match input_splitter.next() {
782            Some((2, Ok(line))) => {
783                assert_eq!(line, String::from("ccc"));
784                assert_eq!(input_splitter.buffer_len(), 0);
785            }
786            item => panic!("wrong item: {item:?}"),
787        }
788
789        match input_splitter.next() {
790            Some((3, Ok(line))) => {
791                assert_eq!(line, String::from("ddd"));
792                assert_eq!(input_splitter.buffer_len(), 0);
793            }
794            item => panic!("wrong item: {item:?}"),
795        }
796
797        assert!(input_splitter.next().is_none());
798    }
799
800    #[test]
801    #[allow(clippy::cognitive_complexity)]
802    fn input_splitter_interrupt_rewind() {
803        let input = vec![
804            Ok(String::from("aaa")),
805            Ok(String::from("bbb")),
806            Ok(String::from("ccc")),
807            Ok(String::from("ddd")),
808        ];
809        let mut input_splitter = InputSplitter::new(input.into_iter().enumerate());
810
811        input_splitter.set_size_of_buffer(3);
812        assert_eq!(input_splitter.buffer_len(), 0);
813
814        match input_splitter.next() {
815            Some((0, Ok(line))) => {
816                assert_eq!(line, String::from("aaa"));
817                assert_eq!(input_splitter.add_line_to_buffer(0, line), None);
818                assert_eq!(input_splitter.buffer_len(), 1);
819            }
820            item => panic!("wrong item: {item:?}"),
821        }
822
823        match input_splitter.next() {
824            Some((1, Ok(line))) => {
825                assert_eq!(line, String::from("bbb"));
826                assert_eq!(input_splitter.add_line_to_buffer(1, line), None);
827                assert_eq!(input_splitter.buffer_len(), 2);
828            }
829            item => panic!("wrong item: {item:?}"),
830        }
831
832        match input_splitter.next() {
833            Some((2, Ok(line))) => {
834                assert_eq!(line, String::from("ccc"));
835                assert_eq!(input_splitter.add_line_to_buffer(2, line), None);
836                assert_eq!(input_splitter.buffer_len(), 3);
837            }
838            item => panic!("wrong item: {item:?}"),
839        }
840
841        input_splitter.rewind_buffer();
842
843        match input_splitter.next() {
844            Some((0, Ok(line))) => {
845                assert_eq!(line, String::from("aaa"));
846                assert_eq!(input_splitter.add_line_to_buffer(0, line), None);
847                assert_eq!(input_splitter.buffer_len(), 3);
848            }
849            item => panic!("wrong item: {item:?}"),
850        }
851
852        match input_splitter.next() {
853            Some((0, Ok(line))) => {
854                assert_eq!(line, String::from("aaa"));
855                assert_eq!(input_splitter.buffer_len(), 2);
856            }
857            item => panic!("wrong item: {item:?}"),
858        }
859
860        match input_splitter.next() {
861            Some((1, Ok(line))) => {
862                assert_eq!(line, String::from("bbb"));
863                assert_eq!(input_splitter.buffer_len(), 1);
864            }
865            item => panic!("wrong item: {item:?}"),
866        }
867
868        match input_splitter.next() {
869            Some((2, Ok(line))) => {
870                assert_eq!(line, String::from("ccc"));
871                assert_eq!(input_splitter.buffer_len(), 0);
872            }
873            item => panic!("wrong item: {item:?}"),
874        }
875
876        match input_splitter.next() {
877            Some((3, Ok(line))) => {
878                assert_eq!(line, String::from("ddd"));
879                assert_eq!(input_splitter.buffer_len(), 0);
880            }
881            item => panic!("wrong item: {item:?}"),
882        }
883
884        assert!(input_splitter.next().is_none());
885    }
886}