1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use std::{borrow::Cow, collections::HashMap, convert::TryFrom, path::Path};

use super::{GitConfig, SectionId};
use crate::{
    file::LookupTreeNode,
    parser::{Key, ParserOrIoError, SectionHeaderName},
};

enum ResolvedTreeNode<'event> {
    Terminal(HashMap<Key<'event>, Cow<'event, [u8]>>),
    NonTerminal(HashMap<Cow<'event, str>, HashMap<Key<'event>, Cow<'event, [u8]>>>),
}

/// A `git-config` that resolves entries on creation, providing a
/// [`HashMap`]-like interface for users.
///
/// This does not provide the same guarantees as [`GitConfig`]; namely, it does
/// not remember comments nor whitespace. Additionally, values are normalized
/// upon creation, so it's not possible to retrieve the original value.
#[allow(clippy::module_name_repetitions)]
pub struct ResolvedGitConfig<'data>(HashMap<SectionLookupTuple<'data>, HashMap<Key<'data>, Cow<'data, [u8]>>>);

type SectionLookupTuple<'data> = (SectionHeaderName<'data>, Option<Cow<'data, str>>);

impl ResolvedGitConfig<'static> {
    /// Opens a `git-config` file from the given path.
    ///
    /// # Errors
    ///
    /// This returns an error if an IO error occurs, or if the file is not a
    /// valid `git-config` file.
    #[inline]
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, ParserOrIoError<'static>> {
        GitConfig::open(path.as_ref()).map(Self::from)
    }
}

impl<'data> ResolvedGitConfig<'data> {
    /// Resolves a given [`GitConfig`].
    #[must_use]
    pub fn from_config(config: GitConfig<'data>) -> Self {
        // Map a <SectionId, SectionBody> into <SectionId, HashMap<Key, Cow<[u8]>>>.
        let sections: HashMap<_, _> = config
            .sections
            .into_iter()
            .map(|(key, section_body)| {
                let mut mapping: HashMap<Key, Cow<[u8]>> = HashMap::new();
                for (key, value) in section_body {
                    mapping.insert(key, value);
                }
                (key, mapping)
            })
            .collect();

        let section_name_to_node = config.section_lookup_tree.into_iter().map(|(section_name, vec)| {
            let node = vec.into_iter().map(|node| match node {
                LookupTreeNode::Terminal(items) => ResolvedTreeNode::Terminal(resolve_sections(&sections, items)),
                LookupTreeNode::NonTerminal(mapping) => {
                    let items = mapping
                        .into_iter()
                        .map(|(key, items)| (key, resolve_sections(&sections, items)))
                        .collect();
                    ResolvedTreeNode::NonTerminal(items)
                }
            });

            (section_name, node)
        });

        let mut resolved: HashMap<_, HashMap<Key, Cow<[u8]>>> = HashMap::new();

        for (section_name, node) in section_name_to_node {
            for node in node {
                match node {
                    ResolvedTreeNode::Terminal(mapping) => {
                        let entry = resolved.entry((section_name.clone(), None)).or_default();
                        entry.extend(mapping);
                    }
                    ResolvedTreeNode::NonTerminal(mapping) => {
                        for (subsection, mapping) in mapping {
                            let entry = resolved.entry((section_name.clone(), Some(subsection))).or_default();
                            entry.extend(mapping);
                        }
                    }
                };
            }
        }

        Self(resolved)
    }
}

fn resolve_sections<'key, 'data>(
    mapping: &HashMap<SectionId, HashMap<Key<'key>, Cow<'data, [u8]>>>,
    sections: Vec<SectionId>,
) -> HashMap<Key<'key>, Cow<'data, [u8]>> {
    sections
        .into_iter()
        .flat_map(|section_id| mapping.get(&section_id).expect("GitConfig invariant failed").iter())
        // Copy the Cow struct, not the underlying slice.
        .map(|(key, value)| (Key::clone(key), Cow::clone(value)))
        .collect()
}

impl TryFrom<&Path> for ResolvedGitConfig<'static> {
    type Error = ParserOrIoError<'static>;

    #[inline]
    fn try_from(path: &Path) -> Result<Self, Self::Error> {
        Self::open(path)
    }
}

impl<'data> From<GitConfig<'data>> for ResolvedGitConfig<'data> {
    #[inline]
    fn from(config: GitConfig<'data>) -> Self {
        Self::from_config(config)
    }
}