#
cargo_util_schemas/
restricted_names.rs

    1//! Helpers for validating and checking names like package and crate names.
    2
    3type Result<T> = std::result::Result<T, NameValidationError>;
    4
    5/// Error validating names in Cargo.
    6#[derive(Debug, thiserror::Error)]
    7#[error(transparent)]
    8pub struct NameValidationError(#[from] ErrorKind);
    9
    10/// Non-public error kind for [`NameValidationError`].
    11#[non_exhaustive]
    12#[derive(Debug, thiserror::Error)]
    13enum ErrorKind {
    14    #[error("{0} cannot be empty")]
    15    Empty(&'static str),
    16
    17    #[error("invalid character `{ch}` in {what}: `{name}`, {reason}")]
    18    InvalidCharacter {
    19        ch: char,
    20        what: &'static str,
    21        name: String,
    22        reason: &'static str,
    23    },
    24
    25    #[error(
    26        "profile name `{name}` is reserved\n{help}\n\
    27         See https://doc.rust-lang.org/cargo/reference/profiles.html \
    28         for more on configuring profiles."
    29    )]
    30    ProfileNameReservedKeyword { name: String, help: &'static str },
    31
    32    #[error("feature named `{0}` is not allowed to start with `dep:`")]
    33    FeatureNameStartsWithDepColon(String),
    34}
    35
    36pub(crate) fn validate_package_name(name: &str) -> Result<()> {
    37    for part in name.split("::") {
    38        validate_name(part, "package name")?;
    39    }
    40    Ok(())
    41}
    42
    43pub(crate) fn validate_registry_name(name: &str) -> Result<()> {
    44    validate_name(name, "registry name")
    45}
    46
    47pub(crate) fn validate_name(name: &str, what: &'static str) -> Result<()> {
    48    if name.is_empty() {
    49        return Err(ErrorKind::Empty(what).into());
    50    }
    51
    52    let mut chars = name.chars();
    53    if let Some(ch) = chars.next() {
    54        if ch.is_digit(10) {
    55            // A specific error for a potentially common case.
    56            return Err(ErrorKind::InvalidCharacter {
    57                ch,
    58                what,
    59                name: name.into(),
    60                reason: "the name cannot start with a digit",
    61            }
    62            .into());
    63        }
    64        if !(unicode_ident::is_xid_start(ch) || ch == '_') {
    65            return Err(ErrorKind::InvalidCharacter {
    66                ch,
    67                what,
    68                name: name.into(),
    69                reason: "the first character must be a Unicode XID start character \
    70                 (most letters or `_`)",
    71            }
    72            .into());
    73        }
    74    }
    75    for ch in chars {
    76        if !(unicode_ident::is_xid_continue(ch) || ch == '-') {
    77            return Err(ErrorKind::InvalidCharacter {
    78                ch,
    79                what,
    80                name: name.into(),
    81                reason: "characters must be Unicode XID characters \
    82                 (numbers, `-`, `_`, or most letters)",
    83            }
    84            .into());
    85        }
    86    }
    87    Ok(())
    88}
    89
    90/// Ensure a package name is [valid][validate_package_name]
    91pub(crate) fn sanitize_package_name(name: &str, placeholder: char) -> String {
    92    let mut slug = String::new();
    93    for part in name.split("::") {
    94        if !slug.is_empty() {
    95            slug.push_str("::");
    96        }
    97        slug.push_str(&sanitize_name(part, placeholder));
    98    }
    99    slug
    100}
    101
    102pub(crate) fn sanitize_name(name: &str, placeholder: char) -> String {
    103    let mut slug = String::new();
    104    let mut chars = name.chars();
    105    while let Some(ch) = chars.next() {
    106        if (unicode_ident::is_xid_start(ch) || ch == '_') && !ch.is_digit(10) {
    107            slug.push(ch);
    108            break;
    109        }
    110    }
    111    while let Some(ch) = chars.next() {
    112        if unicode_ident::is_xid_continue(ch) || ch == '-' {
    113            slug.push(ch);
    114        } else {
    115            slug.push(placeholder);
    116        }
    117    }
    118    if slug.is_empty() {
    119        slug.push_str("package");
    120    }
    121    slug
    122}
    123
    124/// Validate dir-names and profile names according to RFC 2678.
    125pub(crate) fn validate_profile_name(name: &str) -> Result<()> {
    126    if let Some(ch) = name
    127        .chars()
    128        .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-')
    129    {
    130        return Err(ErrorKind::InvalidCharacter {
    131            ch,
    132            what: "profile name",
    133            name: name.into(),
    134            reason: "allowed characters are letters, numbers, underscore, and hyphen",
    135        }
    136        .into());
    137    }
    138
    139    let lower_name = name.to_lowercase();
    140    if lower_name == "debug" {
    141        return Err(ErrorKind::ProfileNameReservedKeyword {
    142            name: name.into(),
    143            help: "To configure the default development profile, \
    144                use the name `dev` as in [profile.dev]",
    145        }
    146        .into());
    147    }
    148    if lower_name == "build-override" {
    149        return Err(ErrorKind::ProfileNameReservedKeyword {
    150            name: name.into(),
    151            help: "To configure build dependency settings, use [profile.dev.build-override] \
    152                 and [profile.release.build-override]",
    153        }
    154        .into());
    155    }
    156
    157    // These are some arbitrary reservations. We have no plans to use
    158    // these, but it seems safer to reserve a few just in case we want to
    159    // add more built-in profiles in the future. We can also uses special
    160    // syntax like cargo:foo if needed. But it is unlikely these will ever
    161    // be used.
    162    if matches!(
    163        lower_name.as_str(),
    164        "build"
    165            | "check"
    166            | "clean"
    167            | "config"
    168            | "fetch"
    169            | "fix"
    170            | "install"
    171            | "metadata"
    172            | "package"
    173            | "publish"
    174            | "report"
    175            | "root"
    176            | "run"
    177            | "rust"
    178            | "rustc"
    179            | "rustdoc"
    180            | "target"
    181            | "tmp"
    182            | "uninstall"
    183    ) || lower_name.starts_with("cargo")
    184    {
    185        return Err(ErrorKind::ProfileNameReservedKeyword {
    186            name: name.into(),
    187            help: "Please choose a different name.",
    188        }
    189        .into());
    190    }
    191
    192    Ok(())
    193}
    194
    195pub(crate) fn validate_feature_name(name: &str) -> Result<()> {
    196    let what = "feature name";
    197    if name.is_empty() {
    198        return Err(ErrorKind::Empty(what).into());
    199    }
    200
    201    if name.starts_with("dep:") {
    202        return Err(ErrorKind::FeatureNameStartsWithDepColon(name.into()).into());
    203    }
    204    if name.contains('/') {
    205        return Err(ErrorKind::InvalidCharacter {
    206            ch: '/',
    207            what,
    208            name: name.into(),
    209            reason: "feature name is not allowed to contain slashes",
    210        }
    211        .into());
    212    }
    213    let mut chars = name.chars();
    214    if let Some(ch) = chars.next() {
    215        if !(unicode_ident::is_xid_start(ch) || ch == '_' || ch.is_digit(10)) {
    216            return Err(ErrorKind::InvalidCharacter {
    217                ch,
    218                what,
    219                name: name.into(),
    220                reason: "the first character must be a Unicode XID start character or digit \
    221                 (most letters or `_` or `0` to `9`)",
    222            }
    223            .into());
    224        }
    225    }
    226    for ch in chars {
    227        if !(unicode_ident::is_xid_continue(ch) || ch == '-' || ch == '+' || ch == '.') {
    228            return Err(ErrorKind::InvalidCharacter {
    229                ch,
    230                what,
    231                name: name.into(),
    232                reason: "characters must be Unicode XID characters, '-', `+`, or `.` \
    233                 (numbers, `+`, `-`, `_`, `.`, or most letters)",
    234            }
    235            .into());
    236        }
    237    }
    238    Ok(())
    239}
    240
    241pub(crate) fn validate_path_base_name(name: &str) -> Result<()> {
    242    validate_name(name, "path base name")
    243}
    244
    245#[cfg(test)]
    246mod tests {
    247    use super::*;
    248
    249    #[test]
    250    fn valid_feature_names() {
    251        assert!(validate_feature_name("c++17").is_ok());
    252        assert!(validate_feature_name("128bit").is_ok());
    253        assert!(validate_feature_name("_foo").is_ok());
    254        assert!(validate_feature_name("feat-name").is_ok());
    255        assert!(validate_feature_name("feat_name").is_ok());
    256        assert!(validate_feature_name("foo.bar").is_ok());
    257
    258        assert!(validate_feature_name("").is_err());
    259        assert!(validate_feature_name("+foo").is_err());
    260        assert!(validate_feature_name("-foo").is_err());
    261        assert!(validate_feature_name(".foo").is_err());
    262        assert!(validate_feature_name("dep:bar").is_err());
    263        assert!(validate_feature_name("foo/bar").is_err());
    264        assert!(validate_feature_name("foo:bar").is_err());
    265        assert!(validate_feature_name("foo?").is_err());
    266        assert!(validate_feature_name("?foo").is_err());
    267        assert!(validate_feature_name("ⒶⒷⒸ").is_err());
    268        assert!(validate_feature_name("a¼").is_err());
    269        assert!(validate_feature_name("").is_err());
    270    }
    271}