gix_validate/
reference.rs1use bstr::{BStr, BString, ByteSlice};
2
3pub mod name {
5 use bstr::BString;
6 use std::convert::Infallible;
7
8 #[derive(Debug)]
10 #[expect(missing_docs)]
11 #[non_exhaustive]
12 pub enum Error {
13 InvalidByte { byte: BString },
14 StartsWithSlash,
15 RepeatedSlash,
16 RepeatedDot,
17 LockFileSuffix,
18 ReflogPortion,
19 Asterisk,
20 StartsWithDot,
21 EndsWithDot,
22 EndsWithSlash,
23 Empty,
24 SomeLowercase,
25 Reserved { name: BString },
26 }
27
28 impl std::fmt::Display for Error {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Error::InvalidByte { byte } => write!(f, "Reference name contains invalid byte: {byte:?}"),
32 Error::StartsWithSlash => write!(f, "Reference name cannot start with a slash"),
33 Error::RepeatedSlash => write!(f, "Reference name cannot contain repeated slashes"),
34 Error::RepeatedDot => write!(f, "Reference name cannot contain repeated dots"),
35 Error::LockFileSuffix => write!(f, "Reference name cannot end with '.lock'"),
36 Error::ReflogPortion => write!(f, "Reference name cannot contain '@{{'"),
37 Error::Asterisk => write!(f, "Reference name cannot contain '*'"),
38 Error::StartsWithDot => write!(f, "Reference name cannot start with a dot"),
39 Error::EndsWithDot => write!(f, "Reference name cannot end with a dot"),
40 Error::EndsWithSlash => write!(f, "Reference name cannot end with a slash"),
41 Error::Empty => write!(f, "Reference name cannot be empty"),
42 Error::SomeLowercase => write!(f, "Standalone references must be all uppercased, like 'HEAD'"),
43 Error::Reserved { name } => write!(f, "Reference name is reserved and cannot be used: {name:?}"),
44 }
45 }
46 }
47
48 impl std::error::Error for Error {
49 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50 Some(const { &gix_error::ClassificationMarker::VALIDATION })
51 }
52 }
53
54 impl From<crate::tag::name::Error> for Error {
55 fn from(err: crate::tag::name::Error) -> Self {
56 match err {
57 crate::tag::name::Error::InvalidByte { byte } => Error::InvalidByte { byte },
58 crate::tag::name::Error::StartsWithSlash => Error::StartsWithSlash,
59 crate::tag::name::Error::RepeatedSlash => Error::RepeatedSlash,
60 crate::tag::name::Error::RepeatedDot => Error::RepeatedDot,
61 crate::tag::name::Error::LockFileSuffix => Error::LockFileSuffix,
62 crate::tag::name::Error::ReflogPortion => Error::ReflogPortion,
63 crate::tag::name::Error::Asterisk => Error::Asterisk,
64 crate::tag::name::Error::StartsWithDot => Error::StartsWithDot,
65 crate::tag::name::Error::EndsWithDot => Error::EndsWithDot,
66 crate::tag::name::Error::EndsWithSlash => Error::EndsWithSlash,
67 crate::tag::name::Error::Empty => Error::Empty,
68 }
69 }
70 }
71
72 impl From<Infallible> for Error {
73 fn from(_: Infallible) -> Self {
74 unreachable!("this impl is needed to allow passing a known valid partial path as parameter")
75 }
76 }
77}
78
79pub fn name(path: &BStr) -> Result<&BStr, name::Error> {
82 match validate(path, Mode::Complete)? {
83 None => Ok(path),
84 Some(_) => {
85 unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
86 }
87 }
88}
89
90pub fn branch_name(path: &BStr) -> Result<&BStr, name::Error> {
94 let path = name(path)?;
95 if path == "refs/heads/HEAD" {
96 return Err(name::Error::Reserved { name: path.into() });
97 }
98 Ok(path)
99}
100
101pub fn name_partial(path: &BStr) -> Result<&BStr, name::Error> {
104 match validate(path, Mode::Partial)? {
105 None => Ok(path),
106 Some(_) => {
107 unreachable!("Without sanitization, there is no chance a sanitized version is returned.")
108 }
109 }
110}
111
112pub fn name_partial_or_sanitize(path: &BStr) -> BString {
117 validate(path, Mode::PartialSanitize)
118 .expect("BUG: errors cannot happen as any issue is fixed instantly")
119 .expect("we always rebuild the path")
120}
121
122enum Mode {
123 Complete,
124 Partial,
125 PartialSanitize,
127}
128
129fn validate(path: &BStr, mode: Mode) -> Result<Option<BString>, name::Error> {
130 let mut out = crate::tag::name_inner(
131 path,
132 match mode {
133 Mode::Complete | Mode::Partial => crate::tag::Mode::Validate,
134 Mode::PartialSanitize => crate::tag::Mode::Sanitize,
135 },
136 )?;
137 if out.as_ref().map_or(path, |b| b.as_bstr()) == "@" {
141 match out.as_mut() {
142 Some(out) => out[0] = b'-',
143 None => return Err(name::Error::Reserved { name: path.into() }),
144 }
145 }
146 if let Mode::Complete = mode {
147 let input = out.as_ref().map_or(path, |b| b.as_bstr());
148 let saw_slash = input.find_byte(b'/').is_some();
149 if !saw_slash && !input.iter().all(|c| c.is_ascii_uppercase() || *c == b'_') {
150 return Err(name::Error::SomeLowercase);
151 }
152 }
153 Ok(out)
154}