lexigram_lib/
file_utils.rs

1// Copyright (c) 2025 Redglyph (@gmail.com). All Rights Reserved.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5use std::io::{BufRead, BufReader, BufWriter, Read, Write, Seek};
6use std::fs::{File, OpenOptions};
7use lexigram_core::CollectJoin;
8
9#[derive(Debug)]
10pub enum SrcTagError {
11    Io(std::io::Error),
12    NoTag,
13    NoClosingTag,
14}
15
16impl From<std::io::Error> for SrcTagError {
17    fn from(err: std::io::Error) -> Self {
18        SrcTagError::Io(err)
19    }
20}
21
22impl Display for SrcTagError {
23    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24        match self {
25            SrcTagError::Io(e) => e.fmt(f),
26            SrcTagError::NoTag => write!(f, "opening tag not found"),
27            SrcTagError::NoClosingTag => write!(f, "closing tag not found"),
28        }
29    }
30}
31
32impl Error for SrcTagError {
33    fn source(&self) -> Option<&(dyn Error + 'static)> {
34        match self {
35            SrcTagError::Io(e) => Some(e),
36            SrcTagError::NoTag => None,
37            SrcTagError::NoClosingTag => None,
38        }
39    }
40}
41
42/// Reads the file `filename` and finds the part included between two tags `tag`.
43/// Returns the text between the tags or `None` if they couldn't be found.
44///
45/// Each line is trimmed from any ending space characters and ends with `\n`.
46pub fn get_tagged_source(filename: &str, tag: &str) -> Result<String, SrcTagError> {
47    let file_tag = format!("[{tag}]");
48    let file = File::open(filename)?;
49    let mut opening_tag_found = false;
50    let mut closing_tag_found = false;
51    let result = BufReader::new(file).lines()
52        .filter_map(|l| l.ok())
53        .skip_while(|l| !l.contains(&file_tag))
54        .inspect(|_| opening_tag_found = true)
55        .skip(2)
56        .take_while(|l| !l.contains(&file_tag))
57        .inspect(|_| closing_tag_found = true)
58        .map(|mut s| {
59            s.truncate(s.trim_end().len());
60            s
61        })
62        .join("\n"); // the last line won't end by `\n`, which removes the last empty line
63    if closing_tag_found {
64        Ok(result)
65    } else if opening_tag_found {
66        Err(SrcTagError::NoClosingTag)
67    } else {
68        Err(SrcTagError::NoTag)
69    }
70}
71
72/// Replaces the text between two tags `tag` by `new_src` in the file `filename`. Returns `Ok` on
73/// success, or `Err` on failure, either I/O or if the tags couldn't be found.
74pub fn replace_tagged_source(filename: &str, tag: &str, new_src: &str) -> Result<(), SrcTagError> {
75    let file_tag = format!("[{tag}]");
76    let file = File::open(filename)?;
77    let mut buf = BufReader::new(file);
78    let mut count = 0;
79    let mut line = String::new();
80    let mut after = String::new();
81    let mut position = 0;
82    loop {
83        line.clear();
84        match buf.read_line(&mut line) {
85            Ok(n) => if n == 0 {
86                return if count == 0 { Err(SrcTagError::NoTag) } else { Err(SrcTagError::NoClosingTag) };
87            }
88            Err(e) => return Err(SrcTagError::Io(e)),
89        }
90        if line.contains(&file_tag) {
91            count += 1;
92            match count {
93                1 => {
94                    position = buf.stream_position()?;
95                }
96                2 => {
97                    after.push_str(&line);
98                    buf.read_to_string(&mut after)?;
99                    break;
100                }
101                _ => panic!()
102            }
103        }
104    }
105    let file = OpenOptions::new().write(true).open(filename)?;
106    file.set_len(position)?;
107    let mut buf = BufWriter::new(file);
108    buf.seek(std::io::SeekFrom::End(0))?;
109    write!(&mut buf, "\n{new_src}\n{after}")?;
110    Ok(())
111}