gix-config 0.61.0

A git-config file parser and editor from the gitoxide project
Documentation
use std::borrow::Cow;

use bstr::{BStr, BString, ByteSlice};

use gix_utils::git_is_space;

/// Removes quotes, if any, from the provided inputs, and transforms the
/// escape sequences `\n`, `\t` and `\b` into newline, tab and backspace
/// (byte `0x08`) respectively, matching how `git` interprets them.
///
/// It assumes the input contains a even number of unescaped quotes,
/// and will unescape escaped quotes and everything else (even though the latter
/// would have been rejected in the parsing stage).
///
/// The return values should be safe for value interpretation.
/// If normalization requires no byte changes, including when it only trims enclosing quotes,
/// the returned value borrows from `input`. Transforming escapes or embedded quotes produces an
/// owned value instead.
///
/// This is the function used to normalize raw values from higher level
/// abstractions. Generally speaking these
/// high level abstractions will handle normalization for you, and you do not
/// need to call this yourself. However, if you're directly handling events
/// from the parser, you may want to use this to help with value interpretation.
///
/// # Examples
///
/// Internally quoted values are turned into an owned variant with quotes removed.
///
/// ```
/// # use gix_config::value::normalize;
/// assert_eq!(&*normalize("hello \"world\""), "hello world");
/// ```
///
/// Escaped quotes are unescaped.
///
/// ```
/// # use gix_config::value::normalize;
/// assert_eq!(&*normalize(r#"hello "world\"""#), r#"hello world""#);
/// ```
#[must_use]
pub fn normalize(input: &(impl crate::AsBStr + ?Sized)) -> Cow<'_, BStr> {
    normalize_inner(input.as_bstr())
}

fn normalize_inner(input: &BStr) -> Cow<'_, BStr> {
    // An optimization to strip enclosing quotes without producing a new value/copy it.
    if input.len() >= 2
        && input[0] == b'"'
        && input[input.len() - 1] == b'"'
        && input[1..input.len() - 1].find_byteset(br#"\""#).is_none()
    {
        return Cow::Borrowed(input[1..input.len() - 1].as_ref());
    }

    if input.find_byteset(br#"\""#).is_none() {
        return Cow::Borrowed(input);
    }
    let mut out: BString = Vec::with_capacity(input.len()).into();
    let mut bytes = input.iter().copied();
    let mut is_in_quotes = false;
    while let Some(c) = bytes.next() {
        match c {
            b'\\' => match bytes.next() {
                Some(b'n') => out.push(b'\n'),
                Some(b't') => out.push(b'\t'),
                Some(b'b') => out.push(b'\x08'),
                Some(c) => {
                    out.push(c);
                }
                None => break,
            },
            b'"' => is_in_quotes = !is_in_quotes,
            // Empty quotes contribute no bytes, so Git keeps ignoring unquoted whitespace after them.
            c if !is_in_quotes && out.is_empty() && git_is_space(c) => {}
            _ => out.push(c),
        }
    }
    Cow::Owned(out)
}