pacenotes_installer\install/
ini.rs1enum Search {
3 Replaced,
4 SectionEndsAt(usize),
6 SectionRunsToEnd,
7 NoSection,
8}
9
10pub fn set_ini_key(text: &str, section: &str, key: &str, value: &str) -> String {
17 let newline = if text.contains("\r\n") { "\r\n" } else { "\n" };
18 let ends_with_newline = text.ends_with('\n') || text.is_empty();
19
20 let mut lines: Vec<String> = text
21 .split('\n')
22 .map(|line| line.strip_suffix('\r').unwrap_or(line).to_string())
23 .collect();
24 if ends_with_newline && lines.last().is_some_and(|line| line.is_empty()) {
26 lines.pop();
27 }
28
29 let entry = format!("{key}={value}");
30 match replace_key(&mut lines, section, key, value) {
31 Search::Replaced => {}
32 Search::SectionEndsAt(at) => lines.insert(at, entry),
33 Search::SectionRunsToEnd => {
34 let after_last_content = lines
35 .iter()
36 .rposition(|line| !line.trim().is_empty())
37 .map_or(lines.len(), |at| at + 1);
38 lines.insert(after_last_content, entry);
39 }
40 Search::NoSection => {
41 if lines.last().is_some_and(|line| !line.trim().is_empty()) {
42 lines.push(String::new());
43 }
44 lines.push(format!("[{section}]"));
45 lines.push(entry);
46 }
47 }
48
49 let mut out = lines.join(newline);
50 if ends_with_newline && !out.is_empty() {
51 out.push_str(newline);
52 }
53 out
54}
55
56fn replace_key(lines: &mut [String], section: &str, key: &str, value: &str) -> Search {
57 let mut in_section = false;
58 let mut section_seen = false;
59 let mut section_end = None;
60 for (index, line) in lines.iter_mut().enumerate() {
61 let trimmed = line.trim();
62 if let Some(name) = trimmed
63 .strip_prefix('[')
64 .and_then(|rest| rest.strip_suffix(']'))
65 {
66 if in_section {
67 section_end = Some(index);
68 }
69 in_section = name.eq_ignore_ascii_case(section);
70 section_seen |= in_section;
71 continue;
72 }
73 if !in_section || trimmed.starts_with(';') || trimmed.starts_with('#') {
74 continue;
75 }
76 let Some((left, right)) = line.split_once('=') else {
77 continue;
78 };
79 if left.trim().eq_ignore_ascii_case(key) {
80 let indent: String = right.chars().take_while(|c| c.is_whitespace()).collect();
81 let tail = inline_comment(right).unwrap_or("");
82 *line = format!("{left}={indent}{value}{tail}");
83 return Search::Replaced;
84 }
85 }
86 match (section_seen, section_end) {
87 (true, Some(at)) => Search::SectionEndsAt(at),
88 (true, None) => Search::SectionRunsToEnd,
89 (false, _) => Search::NoSection,
90 }
91}
92
93fn inline_comment(value: &str) -> Option<&str> {
96 value.char_indices().find_map(|(at, c)| {
97 let before = &value[..at];
98 let starts_comment = (c == ';' || c == '#') && before.ends_with(char::is_whitespace);
99 starts_comment.then(|| &value[before.trim_end().len()..])
100 })
101}
102
103#[cfg(test)]
104mod tests;