mirror of
https://github.com/SkyfallWasTaken/dinopkg.git
synced 2024-11-10 06:09:39 +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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.61"
|
version = "1.0.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2113,12 +2113,28 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validate_package_name"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
|
"thiserror",
|
||||||
|
"urlencoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
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