195 lines · 6.7 KB · Rust
RawDownload
use bstr::{BStr, BString, ByteSlice}; ///pub mod name {    use bstr::BString;     /// The error returned by [`name()`][super::name()].    #[derive(Debug)]    #[expect(missing_docs)]    #[non_exhaustive]    pub enum Error {        InvalidByte { byte: BString },        StartsWithSlash,        RepeatedSlash,        RepeatedDot,        LockFileSuffix,        ReflogPortion,        Asterisk,        StartsWithDot,        EndsWithDot,        EndsWithSlash,        Empty,    }     impl std::fmt::Display for Error {        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {            match self {                Error::InvalidByte { byte } => write!(                    f,                    "A ref must not contain invalid bytes or ascii control characters: {byte:?}"                ),                Error::StartsWithSlash => write!(f, "A reference name must not start with a slash '/'"),                Error::RepeatedSlash => write!(                    f,                    "Multiple slashes in a row are not allowed as they may change the reference's meaning"                ),                Error::RepeatedDot => write!(f, "A ref must not contain '..' as it may be mistaken for a range"),                Error::LockFileSuffix => write!(f, "A ref must not end with '.lock'"),                Error::ReflogPortion => write!(f, "A ref must not contain '@{{' which is a part of a ref-log"),                Error::Asterisk => write!(f, "A ref must not contain '*' character"),                Error::StartsWithDot => write!(f, "A ref must not start with a '.'"),                Error::EndsWithDot => write!(f, "A ref must not end with a '.'"),                Error::EndsWithSlash => write!(f, "A ref must not end with a '/'"),                Error::Empty => write!(f, "A ref must not be empty"),            }        }    }     impl std::error::Error for Error {}} /// Assure the given `input` resemble a valid git tag name, which is returned unchanged on success./// Tag names are provided as names, like `v1.0` or `alpha-1`, without paths.pub fn name(input: &BStr) -> Result<&BStr, name::Error> {    match name_inner(input, Mode::Validate)? {        None => Ok(input),        Some(_) => {            unreachable!("When validating, the input isn't changed")        }    }} #[derive(Eq, PartialEq)]pub(crate) enum Mode {    Sanitize,    Validate,} pub(crate) fn name_inner(input: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {    let mut out: Option<BString> =        matches!(mode, Mode::Sanitize).then(|| BString::from(Vec::with_capacity(input.len())));    if input.is_empty() {        return if let Some(mut out) = out {            out.push(b'-');            Ok(Some(out))        } else {            Err(name::Error::Empty)        };    }    if *input.last().expect("non-empty") == b'/' && out.is_none() {        return Err(name::Error::EndsWithSlash);    }    if input.first() == Some(&b'/') && out.is_none() {        return Err(name::Error::StartsWithSlash);    }     let mut previous = 0;    let mut component_start;    let mut component_end = 0;    let last = input.len() - 1;    for (byte_pos, byte) in input.iter().enumerate() {        match byte {            b'\\' | b'^' | b':' | b'[' | b'?' | b' ' | b'~' | b'\0'..=b'\x1F' | b'\x7F' => {                if let Some(out) = out.as_mut() {                    out.push(b'-');                } else {                    return Err(name::Error::InvalidByte {                        byte: (&[*byte][..]).into(),                    });                }            }            b'*' => {                if let Some(out) = out.as_mut() {                    out.push(b'-');                } else {                    return Err(name::Error::Asterisk);                }            }             b'.' if previous == b'.' => {                if out.is_none() {                    return Err(name::Error::RepeatedDot);                }            }            b'.' if previous == b'/' => {                if let Some(out) = out.as_mut() {                    out.push(b'-');                } else {                    return Err(name::Error::StartsWithDot);                }            }            b'{' if previous == b'@' => {                if let Some(out) = out.as_mut() {                    out.push(b'-');                } else {                    return Err(name::Error::ReflogPortion);                }            }            b'/' if previous == b'/' => {                if out.is_none() {                    return Err(name::Error::RepeatedSlash);                }            }            c => {                if *c == b'/' {                    component_start = component_end;                    component_end = byte_pos;                     if input[component_start..component_end].ends_with_str(".lock") {                        if let Some(out) = out.as_mut() {                            while out.ends_with(b".lock") {                                let len_without_suffix = out.len() - b".lock".len();                                out.truncate(len_without_suffix);                            }                        } else {                            return Err(name::Error::LockFileSuffix);                        }                    }                }                 if let Some(out) = out.as_mut() {                    out.push(*c);                }                 if byte_pos == last && input[component_end + 1..].ends_with_str(".lock") {                    if let Some(out) = out.as_mut() {                        while out.ends_with(b".lock") {                            let len_without_suffix = out.len() - b".lock".len();                            out.truncate(len_without_suffix);                        }                    } else {                        return Err(name::Error::LockFileSuffix);                    }                }            }        }        previous = *byte;    }     if let Some(out) = out.as_mut() {        while out.last() == Some(&b'/') {            out.pop();        }        while out.first() == Some(&b'/') {            out.remove(0);        }    }    if out.as_ref().map_or(input, |b| b.as_bstr())[0] == b'.' {        if let Some(out) = out.as_mut() {            out[0] = b'-';        } else {            return Err(name::Error::StartsWithDot);        }    }    let last = out.as_ref().map_or(input, |b| b.as_bstr()).len() - 1;    if out.as_ref().map_or(input, |b| b.as_bstr())[last] == b'.' {        if let Some(out) = out.as_mut() {            let last = out.len() - 1;            out[last] = b'-';        } else {            return Err(name::Error::EndsWithDot);        }    }    Ok(out)}