ini_merge/
merge.rs

1//! INI merger functionality
2
3use self::mutations::transforms::Transformer;
4use self::mutations::Action;
5use self::mutations::Mutations;
6use self::mutations::SectionAction;
7use crate::loader::Loader;
8use crate::loader::{self};
9use crate::source_loader::SectionAndKey;
10use crate::source_loader::SourceIni;
11use crate::source_loader::SourceValue;
12use crate::source_loader::{self};
13use lending_iterator::prelude::*;
14use log::error;
15use std::borrow::Cow;
16use std::collections::HashSet;
17use std::io::Read;
18use thiserror::Error;
19
20pub mod mutations;
21
22#[cfg(test)]
23mod tests;
24
25/// Error type for INI merger
26#[derive(Debug, Error)]
27#[non_exhaustive]
28pub enum MergeError {
29    /// An error while loading the target INI
30    #[error("Failed to load target INI due to {0}")]
31    TargetLoad(#[source] Box<dyn std::error::Error + 'static + Send + Sync>),
32    /// An error while loading the source INI
33    #[error("Failed to load source INI due to {0}")]
34    SourceLoad(#[source] Box<dyn std::error::Error + 'static + Send + Sync>),
35}
36
37/// State tracking for the merge algorithm
38#[derive(Debug)]
39struct MergeState {
40    /// Buffer building up the merged result
41    result: Vec<String>,
42    /// Temporary buffer that may be discarded or appended to
43    /// [`MergeState::result`] depending on what follows
44    pending_lines: Vec<String>,
45    /// All the section names we have seen so far
46    seen_sections: HashSet<String>,
47    /// All the keys we have seen so far in the current section (cleared for
48    /// each new section)
49    seen_keys: HashSet<String>,
50    /// Name of the current section
51    cur_section: String,
52}
53
54impl MergeState {
55    fn new() -> Self {
56        Self {
57            result: Vec::default(),
58            pending_lines: Vec::default(),
59            seen_sections: HashSet::default(),
60            seen_keys: HashSet::default(),
61            cur_section: crate::OUTSIDE_SECTION.to_string(),
62        }
63    }
64
65    /// Push a line to either pending lines or directly to the output.
66    fn push_raw(&mut self, raw: String) {
67        if self.pending_lines.is_empty() {
68            self.result.push(raw);
69        } else {
70            self.pending_lines.push(raw);
71        }
72    }
73
74    /// Emit the pending section header (if any)
75    ///
76    /// This deals with the case of a section missing from the source + an
77    /// ignore key on an entry in that section. Without this, we would emit
78    /// the entry without the section header.
79    ///
80    /// Comments from such sections might also end up pending.
81    fn emit_pending_lines(&mut self) {
82        self.result.append(&mut self.pending_lines);
83    }
84
85    /// Emit lines that only exist in the source or are forced by setters.
86    ///
87    /// Call just before switching to the next section.
88    fn emit_non_target_lines(&mut self, source: &SourceIni, mutations: &Mutations) {
89        if source.has_section(self.cur_section.as_str()) {
90            match mutations.find_section_action(self.cur_section.as_str()) {
91                None => {
92                    let mut unseen_entries: Vec<_> = source
93                        .section_entries(&self.cur_section)
94                        .filter(|e| !self.seen_keys.contains(e.0.as_ref()))
95                        .collect();
96                    unseen_entries.sort_by_key(|e| e.0);
97                    for (key, value) in unseen_entries {
98                        let action = mutations.find_action(self.cur_section.as_str(), key);
99                        self.seen_keys.insert(key.to_string());
100                        self.emit_kv(action.as_deref(), key, Some(value), None);
101                    }
102                }
103                Some(SectionAction::Ignore) => (),
104                Some(SectionAction::Delete) => (),
105            }
106        }
107        self.emit_force_keys(mutations);
108
109        self.seen_keys.clear();
110    }
111
112    /// Emit lines from forced keys in the current section
113    fn emit_force_keys(&mut self, mutations: &Mutations) {
114        if let Some(forced_keys) = mutations.forced_keys.get(&self.cur_section) {
115            self.emit_pending_lines();
116            let mut forced_keys: Vec<_> = forced_keys
117                .iter()
118                .filter(|&e| !self.seen_keys.contains(e))
119                .collect();
120            forced_keys.sort();
121            for key in forced_keys {
122                let action = mutations.find_action(self.cur_section.as_str(), key);
123                self.emit_kv(action.as_deref(), key, None, None);
124            }
125        }
126    }
127
128    /// Emit a key-value line, handling transforms. Ignores are NOT handled here
129    /// fully.
130    fn emit_kv(
131        &mut self,
132        action: Option<&Action>,
133        key: &str,
134        source: Option<&SourceValue>,
135        target: Option<ini_roundtrip::Item<'_>>,
136    ) {
137        match action {
138            None => {
139                match source {
140                    Some(val) => self.result.push(val.raw().into()),
141                    // PANIC safety: In all cases were we are called with action pass, we should
142                    // have a source line. This invariant is upheld in MutationsBuilder when it
143                    // constructs forced_keys.
144                    None => panic!("This should never happen"),
145                }
146            }
147            Some(Action::Ignore) => (),
148            Some(Action::Delete) => (),
149            Some(Action::Transform(transform)) => {
150                let src =
151                    source.map(|v| crate::Property::from_src(self.cur_section.as_str(), key, v));
152                let tgt = target
153                    .and_then(|v| crate::Property::try_from_ini(self.cur_section.as_str(), v));
154                let transform_result = transform.call(&src, &tgt);
155                match transform_result {
156                    Ok(mutations::transforms::TransformerAction::Nothing) => (),
157                    Ok(mutations::transforms::TransformerAction::Line(raw_line)) => {
158                        self.result.push(raw_line.into_owned());
159                    }
160                    Err(e) => {
161                        error!(target: "ini-merge", "Failed to transform key {key}: {e}");
162                    }
163                }
164            }
165        }
166    }
167}
168
169/// Process the target file, merging the state of source and target files
170pub(crate) fn merge<'a>(
171    target: &'a mut Loader,
172    source: &'a SourceIni,
173    mutations: &Mutations,
174) -> Vec<String> {
175    let mut state = MergeState::new();
176
177    while let Some(ref entry) = target.next() {
178        match *entry {
179            ini_roundtrip::Item::Error(raw) => {
180                error!(target: "ini-merge", "Failed to parse line, copying verbatim: {raw}");
181                state.push_raw(raw.into());
182            }
183            ini_roundtrip::Item::Comment { raw } | ini_roundtrip::Item::Blank { raw } => {
184                state.push_raw(raw.into());
185            }
186            ini_roundtrip::Item::Section { name, raw } => {
187                // Emit any pending source only lines. Can't be done in SectionEnd,
188                // since there can be keys before the first section.
189                state.emit_non_target_lines(source, mutations);
190                // Bookkeeping
191                state.cur_section.clear();
192                state.cur_section.push_str(name);
193                state.seen_sections.insert(name.into());
194                state.seen_keys.clear();
195                state.pending_lines.clear();
196
197                match mutations.find_section_action(name) {
198                    Some(SectionAction::Ignore) => state.push_raw(raw.into()),
199                    None if source.has_section(name) => state.push_raw(raw.into()),
200                    // We cannot yet be sure that this section shouldn't exist.
201                    // It is possible that a key in this section is ignored, even
202                    // though the whole section is not.
203                    None => state.pending_lines.push(raw.into()),
204                    // We will definitely skip the section in this case.
205                    Some(SectionAction::Delete) => (),
206                }
207            }
208            ini_roundtrip::Item::SectionEnd => (),
209            target @ ini_roundtrip::Item::Property { key, val: _, raw } => {
210                // Bookkeeping
211                let action = mutations.find_action(&state.cur_section, key);
212                let src_property = source.property(&SectionAndKey::new(
213                    Cow::Owned(state.cur_section.clone()),
214                    Cow::Borrowed(key),
215                ));
216                match action.as_deref() {
217                    None => {
218                        if let Some(src_val) = src_property {
219                            state.seen_keys.insert(key.into());
220                            state.emit_pending_lines();
221                            state.emit_kv(action.as_deref(), key, Some(src_val), Some(target));
222                        }
223                    }
224                    Some(Action::Ignore) => {
225                        state.seen_keys.insert(key.into());
226                        state.emit_pending_lines();
227                        state.result.push(raw.into());
228                    }
229                    Some(Action::Delete) => {
230                        // Nothing to do, just don't emit anything
231                    }
232                    Some(Action::Transform(_)) => {
233                        state.seen_keys.insert(key.into());
234                        state.emit_pending_lines();
235                        state.emit_kv(action.as_deref(), key, src_property, Some(target));
236                    }
237                }
238            }
239        }
240    }
241
242    // End of system file, emit source only keys for the last section.
243    state.emit_non_target_lines(source, mutations);
244
245    // Go through and emit any source only sections
246    let mut unseen_sections: HashSet<_> = source
247        .sections()
248        .filter(|x| !state.seen_sections.contains(x.0))
249        .map(|(section, raw)| (section, raw.to_owned()))
250        .collect();
251    // Also handle forced keys from `set` directives for sections that don't exist
252    // anywhere.
253    unseen_sections.extend(
254        mutations
255            .forced_keys
256            .keys()
257            .filter(|&x| !state.seen_sections.contains(x))
258            .map(|section| (section, format!("[{section}]"))),
259    );
260    let mut unseen_sections: Vec<_> = unseen_sections.into_iter().collect();
261    unseen_sections.sort_by_key(|e| e.0);
262    for (section, raw) in unseen_sections {
263        if section == crate::OUTSIDE_SECTION {
264            // This case is handled above by the Section case for the first section.
265            continue;
266        }
267        match mutations.find_section_action(section) {
268            None => (),
269            Some(SectionAction::Ignore) => continue,
270            Some(SectionAction::Delete) => continue,
271        }
272        state.cur_section.clear();
273        state.cur_section.push_str(section);
274        state.seen_keys.clear();
275        state.seen_sections.insert(section.into());
276        state.pending_lines.clear();
277
278        state.result.push(raw.clone());
279        for (key, value) in source.section_entries(section) {
280            let action = mutations.find_action(section, key);
281            state.seen_keys.insert(key.to_string());
282            state.emit_kv(action.as_deref(), key, Some(value), None);
283        }
284        state.emit_force_keys(mutations);
285    }
286
287    state.result
288}
289
290/// Merge two INI files, giving the merged file as a vector of strings, one per
291/// line.
292pub fn merge_ini(
293    target: &mut impl Read,
294    source: &mut impl Read,
295    mutations: &Mutations,
296) -> Result<Vec<String>, MergeError> {
297    let mut target =
298        loader::load_ini(target).map_err(|inner| MergeError::TargetLoad(inner.into()))?;
299    let source = source_loader::load_source_ini(source)
300        .map_err(|inner| MergeError::SourceLoad(inner.into()))?;
301    Ok(merge(&mut target, &source, mutations))
302}