Maven: a group's maven-metadata.xml lists its plugins by prefix, so mvn <prefix>:<goal> resolves; Gradle's publish and resolve checked against it
4 files+287−90/4 viewed
| 122 | 122 | `credentials(PasswordCredentials::class)` reads `acmeUsername` and | |
| 123 | 123 | `acmePassword` from `gradle.properties` or from the environment as | |
| 124 | 124 | `ORG_GRADLE_PROJECT_acmeUsername` and `ORG_GRADLE_PROJECT_acmePassword`. | |
| 125 | − | Gradle's Gradle Module Metadata (`.module`) is uploaded and served beside | |
| 126 | − | the POM. | |
| 127 | 125 | ||
| 126 | + | Gradle uploads the jar, the POM, the sources and javadoc jars if you build | |
| 127 | + | them, and its Gradle Module Metadata (`.module`), each with its `.md5`, | |
| 128 | + | `.sha1`, `.sha256` and `.sha512`. The `.module` file is served beside the | |
| 129 | + | POM, so a Gradle build that depends on the artifact reads its variants | |
| 130 | + | (API and runtime dependencies, capabilities) from it, and Maven reads the | |
| 131 | + | POM. Gradle's `HEAD` requests, which it makes to check files it has | |
| 132 | + | cached (with `--refresh-dependencies`, or for a SNAPSHOT), are answered | |
| 133 | + | with each file's size and type. | |
| 134 | + | ||
| 128 | 135 | ## Which repository an artifact belongs to | |
| 129 | 136 | ||
| 130 | 137 | The first file deployed makes the artifact. When a repository of the | |
| 182 | 189 | -DremoteRepositories=acme::default::https://g1t.sh/-/maven/acme/ | |
| 183 | 190 | ``` | |
| 184 | 191 | ||
| 192 | + | ## Maven plugins | |
| 193 | + | ||
| 194 | + | A plugin is deployed like any other artifact, with `<packaging>maven-plugin</packaging>`. | |
| 195 | + | g1t lists the plugins of each group in the group's own | |
| 196 | + | `maven-metadata.xml` (`com/acme/maven-metadata.xml` for the group | |
| 197 | + | `com.acme`), with the prefix each is called by: the `goalPrefix` from the | |
| 198 | + | descriptor `maven-plugin-plugin` puts in its jar, else the one Maven | |
| 199 | + | works out from its artifactId (`hello-maven-plugin` is `hello`). | |
| 200 | + | ||
| 201 | + | To call a plugin by its prefix, as `mvn hello:greet`, name its group in | |
| 202 | + | `~/.m2/settings.xml` and the repository as a plugin repository: | |
| 203 | + | ||
| 204 | + | ```xml | |
| 205 | + | <settings> | |
| 206 | + | <pluginGroups> | |
| 207 | + | <pluginGroup>com.acme</pluginGroup> | |
| 208 | + | </pluginGroups> | |
| 209 | + | <profiles> | |
| 210 | + | <profile> | |
| 211 | + | <id>acme</id> | |
| 212 | + | <pluginRepositories> | |
| 213 | + | <pluginRepository> | |
| 214 | + | <id>acme</id> | |
| 215 | + | <url>https://g1t.sh/-/maven/acme/</url> | |
| 216 | + | </pluginRepository> | |
| 217 | + | </pluginRepositories> | |
| 218 | + | </profile> | |
| 219 | + | </profiles> | |
| 220 | + | <activeProfiles> | |
| 221 | + | <activeProfile>acme</activeProfile> | |
| 222 | + | </activeProfiles> | |
| 223 | + | </settings> | |
| 224 | + | ``` | |
| 225 | + | ||
| 226 | + | The group's metadata lists only the plugins the credentials' owner may | |
| 227 | + | see. A plugin's full coordinates (`mvn com.acme:hello-maven-plugin:1.0.0:greet`) | |
| 228 | + | work without the plugin group. | |
| 229 | + | ||
| 185 | 230 | ## SNAPSHOTs | |
| 186 | 231 | ||
| 187 | 232 | A version ending in `-SNAPSHOT` takes a new build each time it is | |
| 202 | 247 | uploaded are checked against it, and a mismatch is refused with `400`. | |
| 203 | 248 | - **`maven-metadata.xml` is made by g1t** from the versions there, so it | |
| 204 | 249 | always lists every version, with the highest as `latest` and the highest | |
| 205 | − | that is not a SNAPSHOT as `release`. The one a build uploads is accepted | |
| 206 | − | and not kept. | |
| 250 | + | that is not a SNAPSHOT as `release`, and a group's lists its | |
| 251 | + | [plugins](#maven-plugins). The one a build uploads is accepted and not | |
| 252 | + | kept. | |
| 207 | 253 | - **The deploy's last step publishes it.** Maven and Gradle upload the | |
| 208 | 254 | artifact's `maven-metadata.xml` after its files. Then each version (or | |
| 209 | 255 | SNAPSHOT build) whose POM arrived in the deploy is published: it is an |
| 1192 | 1192 | .results() | |
| 1193 | 1193 | } | |
| 1194 | 1194 | ||
| 1195 | + | /// A workspace's Maven artifacts of one groupId (`com.acme:*`), by | |
| 1196 | + | /// name: those named from `com.acme:` up to `com.acme;`, the | |
| 1197 | + | /// character after `:`. | |
| 1198 | + | pub async fn maven_group(&self, workspace: &str, group: &str, limit: u32) -> Result<Vec<PackageRow>> { | |
| 1199 | + | self.prepare( | |
| 1200 | + | &format!( | |
| 1201 | + | "SELECT {PACKAGE_COLUMNS} FROM packages WHERE workspace = ? AND ecosystem = 'maven' AND name >= ? AND name < ? | |
| 1202 | + | AND workspace_deleted_at IS NULL ORDER BY name LIMIT {limit}" | |
| 1203 | + | ), | |
| 1204 | + | &[text(workspace), text(&format!("{group}:")), text(&format!("{group};"))], | |
| 1205 | + | )? | |
| 1206 | + | .all() | |
| 1207 | + | .await? | |
| 1208 | + | .results() | |
| 1209 | + | } | |
| 1210 | + | ||
| 1195 | 1211 | pub async fn forget_blob(&self, digest: &str) -> Result<()> { | |
| 1196 | 1212 | let d = [text(digest)]; | |
| 1197 | 1213 | self.db |
| 114 | 114 | ArtifactMetadata { group: String, artifact: String, checksum: Option<Checksum> }, | |
| 115 | 115 | /// `com/acme/web/1.0-SNAPSHOT/maven-metadata.xml`: a SNAPSHOT's builds. | |
| 116 | 116 | VersionMetadata { group: String, artifact: String, version: String, checksum: Option<Checksum> }, | |
| 117 | + | /// `acme/maven-metadata.xml`: a one-part group's plugins, by prefix. | |
| 118 | + | /// A deeper group's (`com/acme/plugins/maven-metadata.xml`) reads as an | |
| 119 | + | /// artifact's, and is answered with the plugins of the group the whole | |
| 120 | + | /// path names when there is no such artifact. | |
| 121 | + | GroupMetadata { group: String, checksum: Option<Checksum> }, | |
| 117 | 122 | /// `com/acme/web/1.0.0/web-1.0.0.jar`: one of a version's files. | |
| 118 | 123 | File { group: String, artifact: String, version: String, file: String, checksum: Option<Checksum> }, | |
| 119 | 124 | } | |
| 138 | 143 | let (artifact, version) = (parts[n - 3].to_owned(), parts[n - 2].to_owned()); | |
| 139 | 144 | return Some((workspace, MavenPath::VersionMetadata { group, artifact, version, checksum })); | |
| 140 | 145 | } | |
| 146 | + | if n == 2 && valid_group_part(parts[0]) { | |
| 147 | + | return Some((workspace, MavenPath::GroupMetadata { group: parts[0].to_owned(), checksum })); | |
| 148 | + | } | |
| 141 | 149 | if n < 3 || !valid_artifact(parts[n - 2]) { | |
| 142 | 150 | return None; | |
| 143 | 151 | } | |
| 405 | 413 | pub description: Option<String>, | |
| 406 | 414 | /// `<scm><url>`, else `<url>`: where its source is. | |
| 407 | 415 | pub source: Option<String>, | |
| 416 | + | /// `jar` when it names none; `maven-plugin` for a plugin. | |
| 417 | + | pub packaging: String, | |
| 408 | 418 | } | |
| 409 | 419 | ||
| 410 | 420 | /// Reads a POM, with the groupId and version a `<parent>` gives it. | |
| 423 | 433 | name: project.child_text("name"), | |
| 424 | 434 | description: project.child_text("description"), | |
| 425 | 435 | source: project.child("scm").and_then(|scm| scm.child_text("url")).or_else(|| project.child_text("url")), | |
| 436 | + | packaging: project.child_text("packaging").unwrap_or_else(|| "jar".to_owned()), | |
| 426 | 437 | }) | |
| 427 | 438 | } | |
| 428 | 439 | ||
| 440 | + | /// The prefix Maven gives a plugin that names none: its artifactId without | |
| 441 | + | /// `maven` and `plugin` (`acme-maven-plugin` and `maven-acme-plugin` are | |
| 442 | + | /// `acme`), as `mvn acme:<goal>` calls it. | |
| 443 | + | pub fn default_prefix(artifact: &str) -> String { | |
| 444 | + | if artifact == "maven-plugin-plugin" { | |
| 445 | + | return "plugin".to_owned(); | |
| 446 | + | } | |
| 447 | + | let strip = |text: &str, word: &str| -> String { | |
| 448 | + | // `-?word-?`, as Maven's regular expression removes it. | |
| 449 | + | let mut out = text.to_owned(); | |
| 450 | + | while let Some(at) = out.find(word) { | |
| 451 | + | let start = if at > 0 && out.as_bytes()[at - 1] == b'-' { at - 1 } else { at }; | |
| 452 | + | let mut end = at + word.len(); | |
| 453 | + | if out.as_bytes().get(end) == Some(&b'-') { | |
| 454 | + | end += 1; | |
| 455 | + | } | |
| 456 | + | out.replace_range(start..end, ""); | |
| 457 | + | } | |
| 458 | + | out | |
| 459 | + | }; | |
| 460 | + | strip(&strip(artifact, "maven"), "plugin") | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | /// The plugin descriptor's prefix and name, from the | |
| 464 | + | /// `META-INF/maven/plugin.xml` that `maven-plugin-plugin` puts in the jar. | |
| 465 | + | pub fn plugin_descriptor(text: &str) -> Option<(Option<String>, Option<String>)> { | |
| 466 | + | let plugin = xml::parse(text).ok()?; | |
| 467 | + | (plugin.name == "plugin").then(|| (plugin.child_text("goalPrefix"), plugin.child_text("name"))) | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | /// One plugin as a group's `maven-metadata.xml` lists it. | |
| 471 | + | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 472 | + | pub struct Plugin { | |
| 473 | + | pub prefix: String, | |
| 474 | + | pub artifact: String, | |
| 475 | + | pub name: String, | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | /// The `<plugins>` of a group's `maven-metadata.xml`, by prefix, which is | |
| 479 | + | /// how Maven finds `mvn <prefix>:<goal>` among the groups it is told of. | |
| 480 | + | pub fn plugins_block(plugins: &[Plugin]) -> String { | |
| 481 | + | let mut sorted: Vec<&Plugin> = plugins.iter().collect(); | |
| 482 | + | sorted.sort_by(|a, b| (&a.prefix, &a.artifact).cmp(&(&b.prefix, &b.artifact))); | |
| 483 | + | let mut xml = String::from(" <plugins> | |
| 484 | + | "); | |
| 485 | + | for plugin in sorted { | |
| 486 | + | xml.push_str(&format!( | |
| 487 | + | " <plugin> | |
| 488 | + | <name>{}</name> | |
| 489 | + | <prefix>{}</prefix> | |
| 490 | + | <artifactId>{}</artifactId> | |
| 491 | + | </plugin> | |
| 492 | + | ", | |
| 493 | + | xml::escape(&plugin.name), | |
| 494 | + | xml::escape(&plugin.prefix), | |
| 495 | + | xml::escape(&plugin.artifact) | |
| 496 | + | )); | |
| 497 | + | } | |
| 498 | + | xml.push_str(" </plugins> | |
| 499 | + | "); | |
| 500 | + | xml | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | /// A group's `maven-metadata.xml`: its plugins alone, or added to an | |
| 504 | + | /// artifact's metadata when the path is both. | |
| 505 | + | pub fn group_metadata(artifact_xml: Option<String>, plugins: &[Plugin]) -> String { | |
| 506 | + | let block = plugins_block(plugins); | |
| 507 | + | match artifact_xml { | |
| 508 | + | Some(xml) => match xml.rfind("</metadata>") { | |
| 509 | + | Some(at) => format!("{}{block}{}", &xml[..at], &xml[at..]), | |
| 510 | + | None => xml, | |
| 511 | + | }, | |
| 512 | + | None => format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?> | |
| 513 | + | <metadata> | |
| 514 | + | {block}</metadata> | |
| 515 | + | "), | |
| 516 | + | } | |
| 517 | + | } | |
| 518 | + | ||
| 429 | 519 | #[cfg(test)] | |
| 430 | 520 | mod tests { | |
| 431 | 521 | use super::*; | |
| 474 | 564 | assert_eq!(route("/-/maven/acme/com/../web/1.0.0/web-1.0.0.jar"), None); | |
| 475 | 565 | assert_eq!(route("/-/maven/acme/com/acme/web/1.0.0/"), None); | |
| 476 | 566 | assert_eq!(route("/-/maven/acme/maven-metadata.xml"), None); | |
| 567 | + | assert_eq!( | |
| 568 | + | route("/-/maven/acme/acme/maven-metadata.xml.sha1"), | |
| 569 | + | Some(("acme".into(), MavenPath::GroupMetadata { group: "acme".into(), checksum: Some(Checksum::Sha1) })) | |
| 570 | + | ); | |
| 477 | 571 | assert_eq!(route("/-/maven/"), None); | |
| 478 | 572 | } | |
| 479 | 573 | ||
| 574 | 668 | assert_eq!((pom.group.as_str(), pom.artifact.as_str(), pom.version.as_str()), ("com.acme", "web", "1.0.0")); | |
| 575 | 669 | assert_eq!(pom.description.as_deref(), Some("The web client")); | |
| 576 | 670 | assert_eq!(pom.source.as_deref(), Some("https://g1t.sh/acme/web")); | |
| 671 | + | assert_eq!(pom.packaging, "jar"); | |
| 672 | + | let plugin = read_pom(b"<project><groupId>com.acme</groupId><artifactId>acme-maven-plugin</artifactId><version>1</version><packaging>maven-plugin</packaging></project>").unwrap(); | |
| 673 | + | assert_eq!(plugin.packaging, "maven-plugin"); | |
| 577 | 674 | assert!(read_pom(b"<project><artifactId>x</artifactId></project>").is_err(), "no groupId"); | |
| 578 | 675 | assert!(read_pom(b"not xml").is_err()); | |
| 579 | 676 | } | |
| 580 | 677 | ||
| 581 | 678 | #[test] | |
| 679 | + | fn plugins_are_listed_by_prefix() { | |
| 680 | + | assert_eq!(default_prefix("acme-maven-plugin"), "acme"); | |
| 681 | + | assert_eq!(default_prefix("maven-acme-plugin"), "acme"); | |
| 682 | + | assert_eq!(default_prefix("hello-plugin"), "hello"); | |
| 683 | + | assert_eq!(default_prefix("maven-plugin-plugin"), "plugin"); | |
| 684 | + | assert_eq!(default_prefix("tools"), "tools"); | |
| 685 | + | assert_eq!( | |
| 686 | + | plugin_descriptor("<plugin><name>Acme</name><groupId>com.acme</groupId><goalPrefix>acme</goalPrefix><mojos/></plugin>"), | |
| 687 | + | Some((Some("acme".to_owned()), Some("Acme".to_owned()))) | |
| 688 | + | ); | |
| 689 | + | assert_eq!(plugin_descriptor("<project/>"), None); | |
| 690 | + | let plugins = [ | |
| 691 | + | Plugin { prefix: "zed".into(), artifact: "zed-maven-plugin".into(), name: "Zed".into() }, | |
| 692 | + | Plugin { prefix: "acme".into(), artifact: "acme-maven-plugin".into(), name: "Acme & co".into() }, | |
| 693 | + | ]; | |
| 694 | + | let doc = xml::parse(&group_metadata(None, &plugins)).unwrap(); | |
| 695 | + | let listed: Vec<(String, String, String)> = doc | |
| 696 | + | .child("plugins") | |
| 697 | + | .unwrap() | |
| 698 | + | .children_named("plugin") | |
| 699 | + | .map(|p| (p.child_text("prefix").unwrap(), p.child_text("artifactId").unwrap(), p.child_text("name").unwrap())) | |
| 700 | + | .collect(); | |
| 701 | + | assert_eq!( | |
| 702 | + | listed, | |
| 703 | + | [ | |
| 704 | + | ("acme".to_owned(), "acme-maven-plugin".to_owned(), "Acme & co".to_owned()), | |
| 705 | + | ("zed".to_owned(), "zed-maven-plugin".to_owned(), "Zed".to_owned()) | |
| 706 | + | ] | |
| 707 | + | ); | |
| 708 | + | // A path that is an artifact and a group says both. | |
| 709 | + | let both = group_metadata(Some(artifact_metadata("com", "acme", &["1.0".into()], "2026-10-06T00:00:00Z")), &plugins[..1]); | |
| 710 | + | let doc = xml::parse(&both).unwrap(); | |
| 711 | + | assert!(doc.child("versioning").is_some() && doc.child("plugins").is_some()); | |
| 712 | + | } | |
| 713 | + | ||
| 714 | + | #[test] | |
| 582 | 715 | fn checksums_are_hex_of_the_file() { | |
| 583 | 716 | assert_eq!(split_checksum("web-1.0.jar.sha1"), ("web-1.0.jar", Some(Checksum::Sha1))); | |
| 584 | 717 | assert_eq!(split_checksum("web-1.0.jar"), ("web-1.0.jar", None)); |
| 25 | 25 | use worker::{Context, Headers, Method, Request, Response, ResponseBody, Result}; | |
| 26 | 26 | ||
| 27 | 27 | use crate::access::{self, Action}; | |
| 28 | + | use crate::archive; | |
| 28 | 29 | use crate::db::{Checksums, NewFile, NewVersion, PackageRow, VersionRow}; | |
| 29 | 30 | use crate::digest::Digest; | |
| 30 | 31 | use crate::maven::{self, Checksum, MavenPath}; | |
| 38 | 39 | const MAX_VERSIONS: u32 = 5000; | |
| 39 | 40 | /// The longest POM read for its description and source. | |
| 40 | 41 | const MAX_POM_BYTES: usize = 1024 * 1024; | |
| 42 | + | /// The most artifacts of a group read for its plugins. | |
| 43 | + | const MAX_GROUP: u32 = 500; | |
| 44 | + | /// Where a plugin's jar keeps its descriptor, and the most read of it. | |
| 45 | + | const PLUGIN_DESCRIPTOR: &str = "META-INF/maven/plugin.xml"; | |
| 46 | + | const MAX_DESCRIPTOR_BYTES: usize = 4 * 1024 * 1024; | |
| 41 | 47 | const DOCS: &str = "https://docs.g1t.sh/guides/maven/"; | |
| 42 | 48 | const TOKENS: &str = "https://g1t.sh/settings/tokens"; | |
| 43 | 49 | ||
| 106 | 112 | (MavenPath::ArtifactMetadata { group, artifact, checksum }, Method::Get | Method::Head) => { | |
| 107 | 113 | self.maven_metadata(workspace, &group, &artifact, None, checksum, viewer, head).await | |
| 108 | 114 | } | |
| 115 | + | (MavenPath::GroupMetadata { group, checksum }, Method::Get | Method::Head) => { | |
| 116 | + | self.maven_group_metadata(workspace, &group, None, checksum, viewer, head).await | |
| 117 | + | } | |
| 109 | 118 | (MavenPath::VersionMetadata { group, artifact, version, checksum }, Method::Get | Method::Head) => { | |
| 110 | 119 | self.maven_metadata(workspace, &group, &artifact, Some(&version), checksum, viewer, head).await | |
| 111 | 120 | } | |
| 120 | 129 | let name = maven::package_name(&group, &artifact); | |
| 121 | 130 | self.maven_checksum(&mut request, workspace, &name, &version, &file, checksum, viewer).await | |
| 122 | 131 | } | |
| 123 | − | (path @ (MavenPath::ArtifactMetadata { .. } | MavenPath::VersionMetadata { .. }), Method::Put) => { | |
| 132 | + | (path @ (MavenPath::ArtifactMetadata { .. } | MavenPath::VersionMetadata { .. } | MavenPath::GroupMetadata { .. }), Method::Put) => { | |
| 124 | 133 | self.maven_metadata_upload(&mut request, workspace, &path, viewer).await | |
| 125 | 134 | } | |
| 126 | 135 | _ => error(405, "Not a method this address takes. Versions are deleted on the package's page."), | |
| 172 | 181 | viewer: Option<&User>, | |
| 173 | 182 | head: bool, | |
| 174 | 183 | ) -> Result<Response> { | |
| 175 | − | let Some(package) = self.maven_package(workspace, &maven::package_name(group, artifact)).await? else { | |
| 184 | + | let found = self.maven_package(workspace, &maven::package_name(group, artifact)).await?; | |
| 185 | + | // `com/acme/plugins/maven-metadata.xml` is also the group | |
| 186 | + | // `com.acme.plugins`'s, which lists its plugins. | |
| 187 | + | let Some(package) = found else { | |
| 188 | + | if snapshot.is_none() { | |
| 189 | + | return self.maven_group_metadata(workspace, &format!("{group}.{artifact}"), None, checksum, viewer, head).await; | |
| 190 | + | } | |
| 176 | 191 | return self.maven_absent(workspace, viewer).await; | |
| 177 | 192 | }; | |
| 178 | 193 | if let Some(refusal) = self.maven_check(viewer, &package, Action::Pull).await? { | |
| 184 | 199 | if versions.is_empty() { | |
| 185 | 200 | return self.maven_absent(workspace, viewer).await; | |
| 186 | 201 | } | |
| 187 | − | maven::artifact_metadata(group, artifact, &versions, &package.updated_at) | |
| 202 | + | let xml = maven::artifact_metadata(group, artifact, &versions, &package.updated_at); | |
| 203 | + | return self.maven_group_metadata(workspace, &format!("{group}.{artifact}"), Some(xml), checksum, viewer, head).await; | |
| 188 | 204 | } | |
| 189 | 205 | Some(version) => { | |
| 190 | 206 | let Some(row) = self.db.version_named(&package.id, version).await? else { | |
| 203 | 219 | } | |
| 204 | 220 | } | |
| 205 | 221 | ||
| 222 | + | /// A group's `maven-metadata.xml`: the plugins among its artifacts the | |
| 223 | + | /// viewer may see, by prefix, so `mvn <prefix>:<goal>` finds them when | |
| 224 | + | /// the group is one of its `<pluginGroups>`. `artifact` is the | |
| 225 | + | /// metadata of an artifact at the same path, which it is added to. | |
| 226 | + | async fn maven_group_metadata( | |
| 227 | + | &self, | |
| 228 | + | workspace: &str, | |
| 229 | + | group: &str, | |
| 230 | + | artifact: Option<String>, | |
| 231 | + | checksum: Option<Checksum>, | |
| 232 | + | viewer: Option<&User>, | |
| 233 | + | head: bool, | |
| 234 | + | ) -> Result<Response> { | |
| 235 | + | let mut plugins = Vec::new(); | |
| 236 | + | for package in self.db.maven_group(workspace, group, MAX_GROUP).await? { | |
| 237 | + | if !access::decide(viewer, &TargetOf::package(&package).view(), Action::Pull).allowed { | |
| 238 | + | continue; | |
| 239 | + | } | |
| 240 | + | let artifact_id = package.name.rsplit(':').next().unwrap_or("").to_owned(); | |
| 241 | + | // The highest version that is a plugin says its prefix and name. | |
| 242 | + | let mut versions = self.db.versions(&package.id, MAX_VERSIONS).await?; | |
| 243 | + | versions.sort_by(|a, b| maven::compare(&b.version, &a.version)); | |
| 244 | + | let Some(meta) = versions.iter().map(VersionRow::meta).find(|m| m["packaging"] == "maven-plugin" || m["plugin"].is_object()) else { | |
| 245 | + | continue; | |
| 246 | + | }; | |
| 247 | + | let text = |value: &Value| value.as_str().map(str::trim).filter(|t| !t.is_empty()).map(str::to_owned); | |
| 248 | + | plugins.push(maven::Plugin { | |
| 249 | + | prefix: text(&meta["plugin"]["prefix"]).unwrap_or_else(|| maven::default_prefix(&artifact_id)), | |
| 250 | + | name: text(&meta["name"]).or_else(|| text(&meta["plugin"]["name"])).unwrap_or_else(|| artifact_id.clone()), | |
| 251 | + | artifact: artifact_id, | |
| 252 | + | }); | |
| 253 | + | } | |
| 254 | + | let xml = match (artifact, plugins.is_empty()) { | |
| 255 | + | (artifact, false) => maven::group_metadata(artifact, &plugins), | |
| 256 | + | (Some(xml), true) => xml, | |
| 257 | + | (None, true) => return self.maven_absent(workspace, viewer).await, | |
| 258 | + | }; | |
| 259 | + | match checksum { | |
| 260 | + | Some(checksum) => serve(checksum.of(xml.as_bytes()).into_bytes(), "text/plain", head, "no-cache"), | |
| 261 | + | None => serve(xml.into_bytes(), "application/xml", head, "no-cache"), | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 206 | 265 | /// One of a version's files, or a checksum of it. | |
| 207 | 266 | #[allow(clippy::too_many_arguments)] | |
| 208 | 267 | async fn maven_file( | |
| 360 | 419 | None | |
| 361 | 420 | }; | |
| 362 | 421 | ||
| 422 | + | // The main jar of a Maven plugin holds its descriptor. | |
| 423 | + | let plugin = if parsed.classifier.is_none() && parsed.extension == "jar" { | |
| 424 | + | archive::zip_entries(&bytes) | |
| 425 | + | .ok() | |
| 426 | + | .and_then(|entries| entries.into_iter().find(|e| e.name == PLUGIN_DESCRIPTOR)) | |
| 427 | + | .and_then(|entry| archive::zip_read(&bytes, &entry, MAX_DESCRIPTOR_BYTES).ok()) | |
| 428 | + | .and_then(|xml| maven::plugin_descriptor(&String::from_utf8_lossy(&xml))) | |
| 429 | + | } else { | |
| 430 | + | None | |
| 431 | + | }; | |
| 432 | + | ||
| 363 | 433 | let name = maven::package_name(group, artifact); | |
| 364 | 434 | let found = self.db.package(workspace, MAVEN, &name).await?; | |
| 365 | 435 | if found.as_ref().is_some_and(PackageRow::hidden) || (found.is_none() && self.db.workspace_hidden(workspace).await?) { | |
| 445 | 515 | ||
| 446 | 516 | if let Some(pom) = pom { | |
| 447 | 517 | self.maven_pom(&package, &row, &digest, &pom, viewer).await?; | |
| 518 | + | } else if let Some((prefix, title)) = plugin { | |
| 519 | + | // A plugin's jar names the prefix it is called by. | |
| 520 | + | let mut metadata = self.db.version_named(&package.id, version).await?.map(|v| v.meta()).unwrap_or_default(); | |
| 521 | + | if !metadata.is_object() { | |
| 522 | + | metadata = json!({}); | |
| 523 | + | } | |
| 524 | + | metadata["plugin"] = json!({ "prefix": prefix, "name": title }); | |
| 525 | + | self.db.set_version(&row.id, &row.digest, &metadata.to_string()).await?; | |
| 448 | 526 | } | |
| 449 | 527 | created() | |
| 450 | 528 | } | |
| 463 | 541 | metadata["name"] = json!(pom.name); | |
| 464 | 542 | metadata["description"] = json!(pom.description); | |
| 465 | 543 | metadata["source"] = json!(pom.source); | |
| 544 | + | metadata["packaging"] = json!(pom.packaging); | |
| 466 | 545 | self.db.set_version(&row.id, &digest.to_string(), &metadata.to_string()).await?; | |
| 467 | 546 | let versions = self.db.versions(&package.id, MAX_VERSIONS).await?; | |
| 468 | 547 | let releases: Vec<&str> = versions.iter().map(|v| v.version.as_str()).filter(|v| !maven::is_snapshot(v)).collect(); | |
| 579 | 658 | if let Err(refused) = self.maven_body(request).await? { | |
| 580 | 659 | return Ok(refused); | |
| 581 | 660 | } | |
| 582 | − | let (MavenPath::ArtifactMetadata { group, artifact, .. } | MavenPath::VersionMetadata { group, artifact, .. } | MavenPath::File { group, artifact, .. }) = path; | |
| 583 | − | let found = self.maven_package(workspace, &maven::package_name(group, artifact)).await?; | |
| 661 | + | let found = match path { | |
| 662 | + | MavenPath::ArtifactMetadata { group, artifact, .. } | MavenPath::VersionMetadata { group, artifact, .. } | MavenPath::File { group, artifact, .. } => { | |
| 663 | + | self.maven_package(workspace, &maven::package_name(group, artifact)).await? | |
| 664 | + | } | |
| 665 | + | MavenPath::GroupMetadata { .. } => None, | |
| 666 | + | }; | |
| 584 | 667 | let target = match &found { | |
| 585 | 668 | Some(package) => TargetOf::package(package), | |
| 586 | 669 | // A plugin group's metadata names no artifact of its own. |