mirror of
https://github.com/SkyfallWasTaken/dinopkg.git
synced 2024-12-03 17:23:40 +00:00
Add validate_package_name crate
This commit is contained in:
parent
396b6d7124
commit
24c2634165
5 changed files with 299 additions and 5 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -1850,18 +1850,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2113,12 +2113,28 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "validate_package_name"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"thiserror",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
||||
members = ["crates/*"]
|
||||
|
|
10
crates/validate_package_name/Cargo.toml
Normal file
10
crates/validate_package_name/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "validate_package_name"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.5.0"
|
||||
regex = "1.10.5"
|
||||
thiserror = "1.0.62"
|
||||
urlencoding = "2.1.3"
|
59
crates/validate_package_name/src/banned_names.rs
Normal file
59
crates/validate_package_name/src/banned_names.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
/// All the names **not** allowed.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
const BANNED_NAMES: [&str; 47] = [
|
||||
"node_modules",
|
||||
"favicon.ico",
|
||||
// https://github.com/juliangruber/builtins/blob/main/index.js (used by npm)
|
||||
"assert",
|
||||
"buffer",
|
||||
"child_process",
|
||||
"cluster",
|
||||
"console",
|
||||
"constants",
|
||||
"crypto",
|
||||
"dgram",
|
||||
"dns",
|
||||
"domain",
|
||||
"events",
|
||||
"fs",
|
||||
"http",
|
||||
"https",
|
||||
"module",
|
||||
"net",
|
||||
"os",
|
||||
"path",
|
||||
"punycode",
|
||||
"querystring",
|
||||
"readline",
|
||||
"repl",
|
||||
"stream",
|
||||
"string_decoder",
|
||||
"sys",
|
||||
"timers",
|
||||
"tls",
|
||||
"tty",
|
||||
"url",
|
||||
"util",
|
||||
"vm",
|
||||
"zlib",
|
||||
// also from npm `builtins`, but it's the version-locked modules
|
||||
"freelist",
|
||||
"v8",
|
||||
"process",
|
||||
"inspector",
|
||||
"async_hooks",
|
||||
"http2",
|
||||
"perf_hooks",
|
||||
"trace_events",
|
||||
"worker_threads",
|
||||
"node:test",
|
||||
// also from npm `builtins`, but it's the experimental modules
|
||||
"worker_threads",
|
||||
"wasi",
|
||||
"diagnostics_channel",
|
||||
];
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
pub fn is_banned(name: &String) -> bool {
|
||||
BANNED_NAMES.contains(&name.to_lowercase().as_str())
|
||||
}
|
209
crates/validate_package_name/src/lib.rs
Normal file
209
crates/validate_package_name/src/lib.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use urlencoding::encode;
|
||||
|
||||
mod banned_names;
|
||||
use banned_names::is_banned;
|
||||
|
||||
lazy_static! {
|
||||
static ref SCOPED_PACKAGE_REGEX: Regex = Regex::new("^(?:@([^/]+?)[/])?([^/]+?)$").unwrap();
|
||||
}
|
||||
|
||||
const MAX_LEN: usize = 214;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("package name must not be empty")]
|
||||
NameEmpty,
|
||||
|
||||
#[error("package name cannot start with a period or underscore")]
|
||||
InvalidStartingChar,
|
||||
|
||||
#[error("package name cannot contain leading or trailing spaces")]
|
||||
LeadingOrTrailingSpaces,
|
||||
|
||||
#[error("package name is blacklisted or is a core module name")]
|
||||
NameNotAllowed,
|
||||
|
||||
#[error("package name cannot contain more than 214 characters")]
|
||||
NameTooLong,
|
||||
|
||||
#[error("package name cannot contain capital letters")]
|
||||
CapsNotAllowed,
|
||||
|
||||
#[error("scoped package name is invalid")]
|
||||
ScopedPackageNameInvalid,
|
||||
|
||||
#[error("package name can only contain URL-friendly characters")]
|
||||
NameMustBeUrlFriendly,
|
||||
}
|
||||
|
||||
/// Validates an `npm` package name.
|
||||
///
|
||||
/// ```rust
|
||||
/// use validate_package_name::validate;
|
||||
///
|
||||
/// assert!(validate(&String::from("hello")).is_ok())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if the package name is invalid.
|
||||
pub fn validate(name: &String) -> Result<(), Error> {
|
||||
if name.is_empty() {
|
||||
return Err(Error::NameEmpty);
|
||||
}
|
||||
|
||||
if name.starts_with('.') || name.starts_with('_') {
|
||||
return Err(Error::InvalidStartingChar);
|
||||
}
|
||||
|
||||
if name.trim() != name {
|
||||
return Err(Error::LeadingOrTrailingSpaces);
|
||||
}
|
||||
|
||||
if is_banned(name) {
|
||||
return Err(Error::NameNotAllowed);
|
||||
}
|
||||
|
||||
if name.len() > MAX_LEN {
|
||||
return Err(Error::NameTooLong);
|
||||
}
|
||||
|
||||
if &name.to_lowercase() != name {
|
||||
return Err(Error::CapsNotAllowed);
|
||||
}
|
||||
|
||||
if &encode(name).into_owned() != name {
|
||||
let name_match = SCOPED_PACKAGE_REGEX.captures(name);
|
||||
|
||||
if let Some(matches) = name_match {
|
||||
// The regex returns **three** matches:
|
||||
//
|
||||
// - the full name ("@custard/hey")
|
||||
// - the user ("custard"),
|
||||
// - the package ("hi")
|
||||
|
||||
if matches.len() != 3 || matches.get(1).is_none() || matches.get(2).is_none() {
|
||||
return Err(Error::ScopedPackageNameInvalid);
|
||||
}
|
||||
|
||||
let user = &matches[1];
|
||||
let pkg = &matches[2];
|
||||
|
||||
if encode(user) == user && encode(pkg) == pkg {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
return Err(Error::NameMustBeUrlFriendly);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::validate;
|
||||
|
||||
fn assert_err(name: &str) {
|
||||
let is_valid = validate(&String::from(name));
|
||||
|
||||
if is_valid.is_ok() {
|
||||
panic!("test failed: package name `{name}` passes when it shouldn't")
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_ok(name: &str) {
|
||||
let is_valid = validate(&String::from(name));
|
||||
|
||||
if let Err(e) = is_valid {
|
||||
panic!("test failed: package name `{name}` fails with error: {e}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_valid() {
|
||||
assert_ok("some-package");
|
||||
assert_ok("discord.js");
|
||||
assert_ok("num3ric");
|
||||
assert_ok("under_scores")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_special_characters() {
|
||||
assert_err("crazy!");
|
||||
assert_err("@npm-zors/money!time.js")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_scoped() {
|
||||
assert_ok("@custard/hi")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_zero_len() {
|
||||
assert_err("")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_name_that_starts_with_period() {
|
||||
assert_err(".start-with-period")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_name_that_starts_with_underscore() {
|
||||
assert_err("_start-with-underscore")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_colons_in_name() {
|
||||
assert_err("contain:colons")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_leading_space() {
|
||||
assert_err(" leading-space")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_trailing_space() {
|
||||
assert_err("trailing-space ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_non_url_friendly() {
|
||||
assert_err("s/l/a/s/h/e/s")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_blacklisted_name() {
|
||||
assert_err("favicon.ico");
|
||||
assert_err("node_modules")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_core_modules() {
|
||||
assert_err("http");
|
||||
assert_err("process")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_max_len() {
|
||||
assert_ok("ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_mixed_case() {
|
||||
assert_err("hello-WORLD")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_name_over_max_len() {
|
||||
assert_err("ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyoucool")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_empty() {
|
||||
assert_err("")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue