use std::{
fmt::Display,
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::{anyhow, bail, ensure};
use semver::Version;
use tracing::warn;
pub fn is_valid_java_package(name: &str) -> bool {
let name = name.replace("-", "_");
for piece in name.split(".") {
let mut chars = piece.chars();
if chars.next().is_none_or(|c| !c.is_ascii_lowercase()) {
return false;
}
if !chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
return false;
}
}
true
}
pub fn is_valid_maven_artifact(name: &str) -> bool {
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
#[derive(Clone, PartialEq, Eq)]
pub struct MavenCoord {
pub group: String,
pub artifact: String,
pub version: String,
pub extension: Option<String>,
pub classifier: Option<String>,
}
#[cfg(test)]
#[test]
fn test() {
const COORD: &str = "net.neoforged:neoform:1.21.1-20240808.144430:mappings-merged@txt";
const PATH: &str = "net/neoforged/neoform/1.21.1-20240808.144430/neoform-1.21.1-20240808.144430-mappings-merged.txt";
let x = COORD.parse::<MavenCoord>().unwrap();
let y = MavenCoord::from_path(PATH).unwrap();
assert!(x == y);
assert!(x.path() == *PATH);
assert!(y.to_string() == COORD);
}
impl MavenCoord {
pub fn new(
group: String,
artifact: String,
version: String,
classifier: Option<String>,
extension: Option<String>,
) -> anyhow::Result<Self> {
if !is_valid_java_package(&group) {
bail!("invalid group {group} in maven coordinate");
}
if !is_valid_maven_artifact(&artifact) {
warn!("invalid artifact {artifact} in maven coordinate");
}
if !version.parse::<Version>().is_ok() {
warn!(
"version {version} in maven coordinate is not valid semver, this is not recommended"
);
}
Ok(Self {
group,
artifact,
version,
classifier,
extension,
})
}
pub fn path(&self) -> PathBuf {
let classifier = if let Some(s) = &self.classifier {
&format!("-{s}")
} else {
""
};
let extension = if let Some(s) = &self.extension {
&format!(".{s}")
} else {
".jar"
};
let name = format!("{}-{}{classifier}{extension}", self.artifact, self.version);
self.group
.split(".")
.fold(PathBuf::new(), |acc, x| acc.join(x))
.join(&self.artifact)
.join(self.version.to_string())
.join(name)
}
pub fn from_path(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let mut path = path.as_ref().to_path_buf();
let extension = if let Some(s) = path.extension() {
let name = s
.to_str()
.ok_or(anyhow!("invalid maven path extension {}", s.display()))?;
if name == "jar" {
None
} else {
Some(name.into())
}
} else {
Some("".into())
};
path.set_extension("");
let mut components = vec![];
for c in path.components() {
let name = c.as_os_str();
let name = name
.to_str()
.ok_or(anyhow!("invalid maven path component {}", name.display()))?;
components.push(name);
}
ensure!(
components.len() >= 4,
"invalid maven path {}, expected at least 4 components",
path.display()
);
let last = components[components.len() - 1];
let version = components[components.len() - 2];
let artifact = components[components.len() - 3];
let classifier = last
.strip_prefix(&format!("{artifact}-{version}"))
.ok_or(anyhow!("invalid maven path {}", path.display()))?;
let classifier = if classifier.is_empty() {
None
} else {
Some(
classifier
.strip_prefix("-")
.ok_or(anyhow!("invalid classifier {classifier} in maven path"))?,
)
};
let group = components[..components.len() - 3].join(".");
Ok(Self::new(
group,
artifact.into(),
version.into(),
classifier.map(|s| s.into()),
extension,
)?)
}
}
impl Display for MavenCoord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let classifier = if let Some(s) = &self.classifier {
&format!(":{s}")
} else {
""
};
let extension = if let Some(s) = &self.extension {
&format!("@{s}")
} else {
""
};
write!(
f,
"{}:{}:{}{classifier}{extension}",
self.group, self.artifact, self.version
)
}
}
impl FromStr for MavenCoord {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let pieces = s.split("@").collect::<Vec<_>>();
let (main, extension) = match pieces.len() {
0 => unreachable!(),
1 => (pieces[0], None),
2 => (pieces[0], Some(pieces[1])),
_ => bail!(
"invalid maven coordinate {s}, expected <group>:<artifact>:<version>[:<classifier>][@<extension>]"
),
};
let pieces = main.split(":").collect::<Vec<_>>();
let (group, artifact, version, classifier) = match pieces.len() {
0 => unreachable!(),
3 => (pieces[0], pieces[1], pieces[2], None),
4 => (pieces[0], pieces[1], pieces[2], Some(pieces[3])),
_ => bail!(
"invalid maven coordinate {s}, expected <group>:<artifact>:<version>[:<classifier>][@<extension>]"
),
};
Ok(Self::new(
group.into(),
artifact.into(),
version.into(),
classifier.map(|s| s.into()),
extension.map(|s| s.into()),
)?)
}
}