gix-config 0.59.0

A git-config file parser and editor from the gitoxide project
Documentation
mod section {
    use crate::parse::{Comment, Event, Events, SectionRef, section};

    #[test]
    #[cfg(target_pointer_width = "64")]
    fn size_of_events() {
        assert_eq!(
            std::mem::size_of::<SectionRef>(),
            40,
            "this value should only ever decrease"
        );
        assert_eq!(std::mem::size_of::<Events>(), 512);
        assert_eq!(std::mem::size_of::<crate::parse::Span>(), 8);
        assert_eq!(std::mem::size_of::<Event>(), 56);
        assert_eq!(std::mem::size_of::<Comment>(), 12);
        assert_eq!(std::mem::size_of::<section::Name>(), 24);
        assert_eq!(std::mem::size_of::<section::ValueName>(), 24);
    }

    mod header {
        mod unvalidated {
            use crate::parse::section::unvalidated::KeyRef;

            #[test]
            fn section_name_only() {
                assert_eq!(
                    KeyRef::parse("core").unwrap(),
                    KeyRef {
                        section_name: "core",
                        subsection_name: None
                    }
                );
            }

            #[test]
            fn section_name_and_subsection() {
                assert_eq!(
                    KeyRef::parse("core.bare").unwrap(),
                    KeyRef {
                        section_name: "core",
                        subsection_name: Some("bare".into())
                    }
                );
            }

            #[test]
            fn section_name_and_subsection_with_separators() {
                assert_eq!(
                    KeyRef::parse("remote.https:///home/user.git").unwrap(),
                    KeyRef {
                        section_name: "remote",
                        subsection_name: Some("https:///home/user.git".into())
                    }
                );
            }
        }
    }
}

mod span {
    use crate::parse::Span;

    #[test]
    #[cfg(target_pointer_width = "64")]
    fn reports_range_and_rebase_overflow() {
        assert!(Span::range(u32::MAX as usize + 1, 0).is_err());
        assert!(Span::range(0, u32::MAX as usize + 1).is_err());

        let mut span = Span::range(u32::MAX as usize, 0).expect("the maximum offset is representable");
        assert_eq!(span.rebase(1), Err(crate::parse::span::Error));
    }
}

mod event {
    mod write_to {
        use crate::parse::{EventRef, Events};

        fn header(
            name: &'static str,
            subsection: impl Into<Option<(&'static str, &'static str)>>,
        ) -> EventRef<'static> {
            let (separator, subsection_name) =
                subsection.into().map_or((None, None), |(separator, subsection_name)| {
                    (Some(separator.into()), Some(subsection_name.into()))
                });
            EventRef::SectionHeader {
                name: name.into(),
                separator,
                subsection_name,
            }
        }

        fn write_event(event: EventRef<'_>) -> Vec<u8> {
            let mut out = Vec::new();
            event.write_to(&mut out).expect("writing to memory succeeds");
            out
        }

        fn write_events(input: &str) -> Vec<u8> {
            let events = Events::from_str(input).unwrap();
            let mut out = Vec::new();
            for event in events.iter() {
                event.write_to(&mut out).unwrap();
            }
            out
        }

        #[test]
        fn legacy_subsection_format_does_not_use_escapes() {
            assert_eq!(
                write_event(header("invalid", Some((".", r#"\ ""#)))),
                br#"[invalid.\ "]"#,
                "no escaping happens for legacy subsections"
            );
        }

        #[test]
        fn subsections_escape_two_characters_only() {
            assert_eq!(
                write_event(header("invalid", Some((" ", "\\ \"\npost newline")))),
                b"[invalid \"\\\\ \\\"\npost newline\"]",
                "newlines are invalid in validated subsections, but EventRef can represent them"
            );
        }

        #[test]
        fn empty_section_name_with_quoted_subsection() {
            assert_eq!(
                write_event(header("", Some((" ", "core")))),
                br#"[ "core"]"#,
                "Git accepts an empty section name with `core` as subsection"
            );
        }

        #[test]
        fn nul_byte_in_quoted_subsection() {
            assert_eq!(
                write_event(header("hello", Some((" ", "hello\0")))),
                b"[hello \"hello\0\"]",
                "Git accepts NUL bytes in quoted subsection names"
            );
        }

        #[test]
        fn key_value_before_first_section() {
            let input = "a = b\n";
            assert_eq!(
                write_events(input),
                input.as_bytes(),
                "Git accepts key/value pairs before the first section, and we preserve them"
            );
        }

        #[test]
        fn value_with_trailing_backslash_at_eof() {
            let input = "[core]\na=hello\\";
            assert_eq!(
                write_events(input),
                input.as_bytes(),
                "Git accepts EOF as a line continuation terminator, and we preserve the original trailing backslash"
            );
        }
    }
}

pub(crate) mod util {
    //! This module is only included for tests, and contains common unit test helper
    //! functions.
    //!
    //! Parsed events store spans into a separate backing buffer, so their fields cannot be inspected
    //! or compared meaningfully without the backing buffer. The `Owned*` types resolve those spans into
    //! owned byte strings, allowing tests to compare event contents independently of span coordinates
    //! and to retain expected or actual values after a temporary backing buffer is dropped.

    use bstr::BString;

    use crate::parse::{Event, section};

    #[derive(Clone, Debug, Eq, PartialEq)]
    pub(crate) struct OwnedHeader {
        pub(crate) name: BString,
        pub(crate) separator: Option<BString>,
        pub(crate) subsection_name: Option<BString>,
    }

    #[derive(Clone, Debug, Eq, PartialEq)]
    pub(crate) enum OwnedEvent {
        Comment(OwnedComment),
        SectionHeader(OwnedHeader),
        SectionValueName(BString),
        Value(BString),
        Newline(BString),
        ValueNotDone(BString),
        ValueDone(BString),
        Whitespace(BString),
        KeyValueSeparator,
    }

    #[derive(Clone, Debug, Eq, PartialEq)]
    pub(crate) struct OwnedComment {
        pub(crate) tag: u8,
        pub(crate) text: BString,
    }

    #[derive(Clone, Debug, Eq, PartialEq)]
    pub(crate) struct OwnedSection {
        pub(crate) header: OwnedHeader,
        pub(crate) events: Vec<OwnedEvent>,
    }

    pub(crate) fn own_event(event: &Event, backing: &[u8]) -> OwnedEvent {
        match event {
            Event::Comment(comment) => OwnedEvent::Comment(OwnedComment {
                tag: comment.tag,
                text: comment.text.to_bstring_in(backing),
            }),
            Event::SectionHeader(header) => OwnedEvent::SectionHeader(own_header(header, backing)),
            Event::SectionValueName(name) => OwnedEvent::SectionValueName(name.to_bstring_in(backing)),
            Event::Value(value) => OwnedEvent::Value(value.to_bstring_in(backing)),
            Event::Newline(value) => OwnedEvent::Newline(value.to_bstring_in(backing)),
            Event::ValueNotDone(value) => OwnedEvent::ValueNotDone(value.to_bstring_in(backing)),
            Event::ValueDone(value) => OwnedEvent::ValueDone(value.to_bstring_in(backing)),
            Event::Whitespace(value) => OwnedEvent::Whitespace(value.to_bstring_in(backing)),
            Event::KeyValueSeparator => OwnedEvent::KeyValueSeparator,
        }
    }

    pub(crate) fn own_header(header: &section::HeaderData, backing: &[u8]) -> OwnedHeader {
        OwnedHeader {
            name: header.name.to_bstring_in(backing),
            separator: header.separator.map(|separator| separator.to_bstring_in(backing)),
            subsection_name: header
                .subsection_name
                .as_ref()
                .map(|subsection_name| subsection_name.value_in(backing).to_owned()),
        }
    }

    pub fn section_header(name: &str, subsection: impl Into<Option<(&'static str, &'static str)>>) -> OwnedHeader {
        if let Some((separator, subsection_name)) = subsection.into() {
            OwnedHeader {
                name: name.into(),
                separator: Some(separator.into()),
                subsection_name: Some(subsection_name.into()),
            }
        } else {
            OwnedHeader {
                name: name.into(),
                separator: None,
                subsection_name: None,
            }
        }
    }

    pub(crate) fn name_event(name: &'static str) -> OwnedEvent {
        OwnedEvent::SectionValueName(name.into())
    }

    pub(crate) fn value_event(value: &'static str) -> OwnedEvent {
        OwnedEvent::Value(value.into())
    }

    pub(crate) fn value_not_done_event(value: &'static str) -> OwnedEvent {
        OwnedEvent::ValueNotDone(value.into())
    }

    pub(crate) fn value_done_event(value: &'static str) -> OwnedEvent {
        OwnedEvent::ValueDone(value.into())
    }

    pub(crate) fn newline_event() -> OwnedEvent {
        newline_custom_event("\n")
    }

    pub(crate) fn newline_custom_event(value: &'static str) -> OwnedEvent {
        OwnedEvent::Newline(value.into())
    }

    pub(crate) fn whitespace_event(value: &'static str) -> OwnedEvent {
        OwnedEvent::Whitespace(value.into())
    }

    pub(crate) fn comment_event(tag: char, msg: &'static str) -> OwnedEvent {
        OwnedEvent::Comment(comment(tag, msg))
    }

    pub(crate) fn comment(comment_tag: char, comment: &'static str) -> OwnedComment {
        OwnedComment {
            tag: comment_tag as u8,
            text: comment.into(),
        }
    }

    pub(crate) const fn fully_consumed<T>(t: T) -> (&'static [u8], T) {
        (&[], t)
    }
}