use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
#[inline]
#[must_use]
pub fn split_name(full: &str) -> (Option<&str>, &str) {
let b = full.as_bytes();
if b.first() == Some(&b'@')
&& let Some(i) = memchr::memchr(b'/', &b[1..])
{
return (Some(&full[..=i]), &full[i + 2..]);
}
(None, full)
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ident<'a> {
full: &'a str,
}
impl<'a> Ident<'a> {
#[inline]
#[must_use]
pub const fn new(full: &'a str) -> Self {
Self { full }
}
#[inline]
#[must_use]
pub const fn as_str(&self) -> &'a str {
self.full
}
#[inline]
#[must_use]
pub fn scope(&self) -> Option<&'a str> {
split_name(self.full).0
}
#[inline]
#[must_use]
pub fn name(&self) -> &'a str {
split_name(self.full).1
}
#[inline]
#[must_use]
pub fn parts(&self) -> (Option<&'a str>, &'a str) {
split_name(self.full)
}
}
impl Ord for Ident<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.parts().cmp(&other.parts())
}
}
impl PartialOrd for Ident<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Ident<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.full)
}
}
impl fmt::Debug for Ident<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (scope, name) = self.parts();
f.debug_struct("Ident")
.field("scope", &scope)
.field("name", &name)
.finish()
}
}
impl AsRef<str> for Ident<'_> {
fn as_ref(&self) -> &str {
self.full
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Range<'a> {
raw: &'a str,
sep: Option<usize>,
}
impl<'a> Range<'a> {
#[inline]
#[must_use]
pub fn from_raw(raw: &'a str) -> Self {
Self {
raw,
sep: memchr::memchr(b':', raw.as_bytes()),
}
}
#[inline]
#[must_use]
pub const fn raw(&self) -> &'a str {
self.raw
}
#[inline]
#[must_use]
pub fn protocol_str(&self) -> Option<&'a str> {
self.sep.map(|i| &self.raw[..i])
}
#[inline]
#[must_use]
pub fn selector(&self) -> &'a str {
self.sep.map_or(self.raw, |i| &self.raw[i + 1..])
}
}
impl fmt::Debug for Range<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Range")
.field("raw", &self.raw)
.field("protocol_sep_index", &self.sep)
.finish()
}
}
impl fmt::Display for Range<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.raw)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
Npm,
Workspace,
Patch,
Git,
File,
Portal,
Exec,
Link,
Unknown,
}
impl<'a> Range<'a> {
#[must_use]
pub fn protocol(&self) -> Protocol {
match self.protocol_str() {
Some("npm") => Protocol::Npm,
Some("workspace") => Protocol::Workspace,
Some("patch") => Protocol::Patch,
Some("file") => Protocol::File,
Some("portal") => Protocol::Portal,
Some("exec") => Protocol::Exec,
Some("link") => Protocol::Link,
Some(p) if p.starts_with("git") => Protocol::Git,
Some(_) | None => Protocol::Unknown,
}
}
fn selector_if(&self, p: Protocol) -> Option<&'a str> {
(self.protocol() == p).then(|| self.selector())
}
#[must_use]
pub fn as_npm_range(&self) -> Option<&'a str> {
self.selector_if(Protocol::Npm)
}
#[must_use]
pub fn as_workspace_path(&self) -> Option<&'a str> {
self.selector_if(Protocol::Workspace)
}
#[must_use]
pub fn as_link_path(&self) -> Option<&'a str> {
self.selector_if(Protocol::Link)
}
#[must_use]
pub fn as_file_path(&self) -> Option<&'a str> {
self.selector_if(Protocol::File)
}
#[must_use]
pub fn as_portal_path(&self) -> Option<&'a str> {
self.selector_if(Protocol::Portal)
}
#[must_use]
pub fn as_exec_command(&self) -> Option<&'a str> {
self.selector_if(Protocol::Exec)
}
#[must_use]
pub fn as_git_url_and_fragment(&self) -> Option<(&'a str, Option<&'a str>)> {
match self.protocol() {
Protocol::Git => {
let raw = self.raw();
raw.find('#').map_or(Some((raw, None)), |pos| {
Some((&raw[..pos], Some(&raw[pos + 1..])))
})
}
_ => None,
}
}
#[must_use]
pub fn as_patch_inner_and_source(&self) -> Option<(&'a str, Option<&'a str>)> {
match self.protocol() {
Protocol::Patch => {
let sel = self.selector();
sel.find('#').map_or(Some((sel, None)), |pos| {
Some((&sel[..pos], Some(&sel[pos + 1..])))
})
}
_ => None,
}
}
}
#[derive(Clone, Copy)]
pub struct Descriptor<'a> {
text: &'a str,
name_len: u32,
range_start: u32,
}
impl<'a> Descriptor<'a> {
#[inline]
pub(crate) fn from_text(text: &'a str, name_len: u32, range_start: u32) -> Self {
debug_assert!(name_len <= range_start && range_start as usize <= text.len());
Self {
text,
name_len,
range_start,
}
}
#[inline]
#[must_use]
pub fn ident(&self) -> Ident<'a> {
Ident::new(self.text.get(..self.name_len as usize).unwrap_or(""))
}
#[inline]
#[must_use]
pub fn range(&self) -> &'a str {
self.text.get(self.range_start as usize..).unwrap_or("")
}
#[inline]
#[must_use]
pub fn range_struct(&self) -> Range<'a> {
Range::from_raw(self.range())
}
#[inline]
#[must_use]
pub fn raw(&self) -> Option<&'a str> {
let n = self.name_len as usize;
(self.range_start as usize == n + 1 && self.text.as_bytes().get(n) == Some(&b'@'))
.then_some(self.text)
}
}
impl PartialEq for Descriptor<'_> {
fn eq(&self, other: &Self) -> bool {
self.ident() == other.ident() && self.range() == other.range()
}
}
impl Eq for Descriptor<'_> {}
impl Hash for Descriptor<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ident().hash(state);
self.range().hash(state);
}
}
impl fmt::Display for Descriptor<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.raw() {
Some(raw) => f.write_str(raw),
None => write!(f, "{}@{}", self.ident().as_str(), self.range()),
}
}
}
impl fmt::Debug for Descriptor<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Descriptor")
.field("ident", &self.ident())
.field("range", &self.range_struct())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Locator<'a> {
ident: Ident<'a>,
reference: &'a str,
}
impl<'a> Locator<'a> {
#[must_use]
pub const fn new(ident: Ident<'a>, reference: &'a str) -> Self {
Self { ident, reference }
}
#[must_use]
pub const fn ident(&self) -> Ident<'a> {
self.ident
}
#[must_use]
pub const fn reference(&self) -> &'a str {
self.reference
}
#[must_use]
pub fn from_resolution(raw: &'a str) -> Option<Self> {
let b = raw.as_bytes();
let at = if b.first() == Some(&b'@') {
memchr::memchr(b'/', b).and_then(|sl| memchr::memchr(b'@', &b[sl..]).map(|i| sl + i))
} else {
memchr::memchr(b'@', b)
}?;
let (name, reference) = raw.split_at(at);
Some(Self::new(
Ident::new(name),
reference.trim_start_matches('@'),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn views_are_small_and_debug_is_readable() {
assert_eq!(std::mem::size_of::<Ident<'_>>(), 16);
assert_eq!(std::mem::size_of::<Descriptor<'_>>(), 24);
assert_eq!(
format!("{:?}", Range::from_raw("npm:^1")),
"Range { raw: \"npm:^1\", protocol_sep_index: Some(3) }"
);
assert_eq!(
format!("{:?}", Range::from_raw("*")),
"Range { raw: \"*\", protocol_sep_index: None }"
);
assert_eq!(
format!("{:?}", Ident::new("@s/a")),
"Ident { scope: Some(\"@s\"), name: \"a\" }"
);
let r = Range::from_raw(":lead");
assert_eq!((r.protocol_str(), r.selector()), (Some(""), "lead"));
}
#[test]
fn ident_accessors_order_and_display() {
let a = Ident::new("@b/a");
assert_eq!((a.scope(), a.name(), a.as_str()), (Some("@b"), "a", "@b/a"));
assert_eq!(a.to_string(), "@b/a");
let mut v = [
Ident::new("@a/z"),
Ident::new("z"),
Ident::new("@a/b"),
Ident::new("a"),
];
v.sort();
let s: Vec<_> = v.iter().map(Ident::as_str).collect();
assert_eq!(
s,
["a", "z", "@a/b", "@a/z"],
"unscoped first, then by scope and name"
);
assert_eq!(Ident::new("x"), Ident::new("x"));
assert_ne!(Ident::new("@x/y"), Ident::new("y"));
}
#[test]
fn descriptor_parts_raw_and_display() {
let d = Descriptor::from_text("@s/a@npm:^1", 4, 5);
assert_eq!(d.ident().as_str(), "@s/a");
assert_eq!(d.range(), "npm:^1");
assert_eq!(d.raw(), Some("@s/a@npm:^1"));
assert_eq!(d.to_string(), "@s/a@npm:^1");
assert_eq!(d.range_struct().protocol(), Protocol::Npm);
let text = "@s/a\": \"npm:^1";
let dep = Descriptor::from_text(text, 4, 8);
assert_eq!(dep.range(), "npm:^1");
assert_eq!(dep.raw(), None);
assert_eq!(dep.to_string(), "@s/a@npm:^1");
assert_eq!(d, dep, "same ident and range");
let mut h1 = std::collections::hash_map::DefaultHasher::new();
let mut h2 = std::collections::hash_map::DefaultHasher::new();
d.hash(&mut h1);
dep.hash(&mut h2);
assert_eq!(h1.finish(), h2.finish());
assert_eq!(Descriptor::from_text("a: b", 1, 3).raw(), None);
let bad = Descriptor::from_text("ab", 1, 2);
assert_eq!((bad.ident().as_str(), bad.range()), ("a", ""));
assert!(!format!("{bad:?}").is_empty());
}
#[test]
fn descriptor_debug_matches_0_6() {
#[allow(dead_code)]
mod v06 {
#[derive(Debug)]
pub struct Ident<'a> {
pub scope: Option<&'a str>,
pub name: &'a str,
}
#[derive(Debug)]
pub struct Range<'a> {
pub raw: &'a str,
pub protocol_sep_index: Option<usize>,
}
#[derive(Debug)]
pub struct Descriptor<'a> {
pub ident: Ident<'a>,
pub range: Range<'a>,
}
}
let d = Descriptor::from_text("@s/a@npm:^1", 4, 5);
assert_eq!(
format!("{d:?}"),
"Descriptor { ident: Ident { scope: Some(\"@s\"), name: \"a\" }, \
range: Range { raw: \"npm:^1\", protocol_sep_index: Some(3) } }"
);
let dep = Descriptor::from_text("a\": \"*", 1, 5);
assert_eq!(
format!("{dep:?}"),
"Descriptor { ident: Ident { scope: None, name: \"a\" }, \
range: Range { raw: \"*\", protocol_sep_index: None } }"
);
let old = v06::Descriptor {
ident: v06::Ident {
scope: Some("@s"),
name: "a",
},
range: v06::Range {
raw: "npm:^1",
protocol_sep_index: Some(3),
},
};
assert_eq!(format!("{d:#?}"), format!("{old:#?}"));
assert_eq!(format!("{d:?}"), format!("{old:?}"));
}
#[test]
fn split_name_matches_the_0_6_rule() {
fn reference(name_part: &str) -> (Option<&str>, &str) {
name_part
.strip_prefix('@')
.map_or((None, name_part), |stripped| {
stripped
.split_once('/')
.map_or((None, name_part), |(scope, name)| {
(Some(&name_part[..=scope.len()]), name)
})
})
}
let names = [
"",
"@",
"@/",
"/x",
"x/",
"a",
"@a",
"@a/",
"@/b",
"@a/b",
"@a/b/c",
"a/b",
"@@a/b",
"@\u{e9}/\u{fc}",
"\u{e9}",
"@s/@x",
"@a//b",
"a@b/c",
];
for n in names {
assert_eq!(split_name(n), reference(n), "{n:?}");
let i = Ident::new(n);
assert_eq!((i.scope(), i.name()), reference(n), "{n:?}");
}
}
#[test]
fn from_resolution_matches_the_0_6_rule() {
fn reference(raw: &str) -> Option<(Option<&str>, &str, &str)> {
let at_index = if raw.starts_with('@') {
raw
.find('/')
.and_then(|slash| raw[slash..].find('@').map(|i| slash + i))
} else {
raw.find('@')
}?;
let (name_part, reference_with_at) = raw.split_at(at_index);
let ident = Ident::new(name_part);
Some((
ident.scope(),
ident.name(),
reference_with_at.trim_start_matches('@'),
))
}
for raw in [
"a@npm:1",
"@s/a@npm:1",
"a@@npm:1",
"@s/a",
"plain",
"@noslash@1",
"@s/a@",
"",
"@",
"@@",
"@/@",
"a/b@c",
"@s/a/b@npm:1@2",
"\u{e9}@\u{fc}",
"@\u{e9}/x@@@y",
] {
let got =
Locator::from_resolution(raw).map(|l| (l.ident().scope(), l.ident().name(), l.reference()));
assert_eq!(got, reference(raw), "{raw:?}");
}
}
#[test]
fn test_range_no_protocol() {
let r = Range::from_raw("*");
assert_eq!(r.raw(), "*");
assert_eq!(r.protocol_str(), None);
assert_eq!(r.selector(), "*");
}
#[test]
fn test_range_with_npm_protocol() {
let r = Range::from_raw("npm:^2.0.0");
assert_eq!(r.raw(), "npm:^2.0.0");
assert_eq!(r.protocol_str(), Some("npm"));
assert_eq!(r.selector(), "^2.0.0");
assert_eq!(r.protocol(), Protocol::Npm);
}
#[test]
fn test_range_with_patch_protocol() {
let raw = "patch:is-odd@npm%3A3.0.1#~/.yarn/patches/x.patch";
let r = Range::from_raw(raw);
assert_eq!(r.raw(), raw);
assert_eq!(r.protocol_str(), Some("patch"));
assert!(r.selector().starts_with("is-odd@npm%3A3.0.1#"));
assert_eq!(r.protocol(), Protocol::Patch);
}
#[test]
fn test_range_with_link_protocol() {
let r = Range::from_raw("link:./packages/a");
assert_eq!(r.protocol_str(), Some("link"));
assert_eq!(r.selector(), "./packages/a");
assert_eq!(r.protocol(), Protocol::Link);
}
#[test]
fn test_range_with_git_protocol() {
let r = Range::from_raw("git+ssh://host/repo.git#v1");
assert_eq!(r.protocol_str(), Some("git+ssh"));
assert_eq!(r.protocol(), Protocol::Git);
let (url, frag) = r.as_git_url_and_fragment().unwrap();
assert_eq!(url, "git+ssh://host/repo.git");
assert_eq!(frag, Some("v1"));
}
#[test]
fn test_range_protocol_specific_accessors() {
let npm = Range::from_raw("npm:^1.2.3");
assert_eq!(npm.as_npm_range(), Some("^1.2.3"));
let ws = Range::from_raw("workspace:packages/a");
assert_eq!(ws.as_workspace_path(), Some("packages/a"));
let link = Range::from_raw("link:./local");
assert_eq!(link.as_link_path(), Some("./local"));
let file = Range::from_raw("file:../tarballs/pkg.tgz");
assert_eq!(file.as_file_path(), Some("../tarballs/pkg.tgz"));
let portal = Range::from_raw("portal:./inner");
assert_eq!(portal.as_portal_path(), Some("./inner"));
let exec = Range::from_raw("exec:node ./script.js");
assert_eq!(exec.as_exec_command(), Some("node ./script.js"));
let patch = Range::from_raw("patch:is-odd@npm%3A3.0.1#~/.yarn/patches/is-odd-npm-3.0.1.patch");
let (inner, src) = patch.as_patch_inner_and_source().unwrap();
assert!(inner.starts_with("is-odd@npm%3A3.0.1"));
assert!(src.unwrap().starts_with("~/.yarn/patches/"));
}
}