git_ref_format_core/
check.rs

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
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use thiserror::Error;

pub struct Options {
    /// If `false`, the refname must contain at least one `/`.
    pub allow_onelevel: bool,
    /// If `true`, the refname may contain exactly one `*` character.
    pub allow_pattern: bool,
}

#[derive(Debug, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum Error {
    #[error("empty input")]
    Empty,
    #[error("lone '@' character")]
    LoneAt,
    #[error("consecutive or trailing slash")]
    Slash,
    #[error("ends with '.lock'")]
    DotLock,
    #[error("consecutive dots ('..')")]
    DotDot,
    #[error("at-open-brace ('@{{')")]
    AtOpenBrace,
    #[error("invalid character {0:?}")]
    InvalidChar(char),
    #[error("component starts with '.'")]
    StartsDot,
    #[error("component ends with '.'")]
    EndsDot,
    #[error("control character")]
    Control,
    #[error("whitespace")]
    Space,
    #[error("must contain at most one '*'")]
    Pattern,
    #[error("must contain at least one '/'")]
    OneLevel,
}

/// Validate that a string slice is a valid refname.
pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
    match s {
        "" => Err(Error::Empty),
        "@" => Err(Error::LoneAt),
        "." => Err(Error::StartsDot),
        _ => {
            let mut globs = 0usize;
            let mut parts = 0usize;

            for x in s.split('/') {
                if x.is_empty() {
                    return Err(Error::Slash);
                }

                parts += 1;

                if x.ends_with(".lock") {
                    return Err(Error::DotLock);
                }

                let last_char = x.chars().count() - 1;
                for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
                    match y {
                        ('.', '.') => return Err(Error::DotDot),
                        ('@', '{') => return Err(Error::AtOpenBrace),

                        ('\0', _) => return Err(Error::InvalidChar('\0')),
                        ('\\', _) => return Err(Error::InvalidChar('\\')),
                        ('~', _) => return Err(Error::InvalidChar('~')),
                        ('^', _) => return Err(Error::InvalidChar('^')),
                        (':', _) => return Err(Error::InvalidChar(':')),
                        ('?', _) => return Err(Error::InvalidChar('?')),
                        ('[', _) => return Err(Error::InvalidChar('[')),

                        ('*', _) => globs += 1,

                        ('.', _) if i == 0 => return Err(Error::StartsDot),
                        ('.', _) if i == last_char => return Err(Error::EndsDot),

                        (' ', _) => return Err(Error::Space),

                        (z, _) if z.is_ascii_control() => return Err(Error::Control),

                        _ => continue,
                    }
                }
            }

            if parts < 2 && !opts.allow_onelevel {
                Err(Error::OneLevel)
            } else if globs > 1 && opts.allow_pattern {
                Err(Error::Pattern)
            } else if globs > 0 && !opts.allow_pattern {
                Err(Error::InvalidChar('*'))
            } else {
                Ok(())
            }
        }
    }
}