use serde::Deserialize;
use crate::error::MavenError;
use crate::xml::from_str_panic_safe;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pom {
pub model_version: Option<String>,
pub group_id: String,
pub artifact_id: String,
pub version: String,
pub packaging: String,
pub parent: Option<PomParent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PomParent {
pub group_id: String,
pub artifact_id: String,
pub version: String,
}
pub fn parse_pom(xml: &str) -> Result<Pom, MavenError> {
let raw: RawPom = from_str_panic_safe(xml, MavenError::InvalidPom)?;
let parent = raw.parent.as_ref().map(|p| PomParent {
group_id: p.group_id.clone().unwrap_or_default(),
artifact_id: p.artifact_id.clone().unwrap_or_default(),
version: p.version.clone().unwrap_or_default(),
});
let group_id = raw
.group_id
.clone()
.or_else(|| parent.as_ref().map(|p| p.group_id.clone()))
.unwrap_or_default();
let version = raw
.version
.clone()
.or_else(|| parent.as_ref().map(|p| p.version.clone()))
.unwrap_or_default();
let artifact_id = raw.artifact_id.clone().unwrap_or_default();
if group_id.is_empty() {
return Err(MavenError::InvalidPom(
"POM missing groupId (and no <parent><groupId>)".into(),
));
}
if artifact_id.is_empty() {
return Err(MavenError::InvalidPom("POM missing artifactId".into()));
}
if version.is_empty() {
return Err(MavenError::InvalidPom(
"POM missing version (and no <parent><version>)".into(),
));
}
let packaging = raw.packaging.clone().unwrap_or_else(|| "jar".to_string());
Ok(Pom {
model_version: raw.model_version,
group_id,
artifact_id,
version,
packaging,
parent,
})
}
#[derive(Debug, Deserialize)]
struct RawPom {
#[serde(rename = "modelVersion", default)]
model_version: Option<String>,
#[serde(rename = "groupId", default)]
group_id: Option<String>,
#[serde(rename = "artifactId", default)]
artifact_id: Option<String>,
#[serde(rename = "version", default)]
version: Option<String>,
#[serde(rename = "packaging", default)]
packaging: Option<String>,
#[serde(rename = "parent", default)]
parent: Option<RawParent>,
}
#[derive(Debug, Deserialize)]
struct RawParent {
#[serde(rename = "groupId", default)]
group_id: Option<String>,
#[serde(rename = "artifactId", default)]
artifact_id: Option<String>,
#[serde(rename = "version", default)]
version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::parse_pom;
const FULL_POM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>foo</artifactId>
<version>1.2.3</version>
<packaging>jar</packaging>
</project>"#;
const PARENT_INHERITED_POM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.parent</groupId>
<artifactId>parent-pom</artifactId>
<version>9.9.9</version>
</parent>
<artifactId>child</artifactId>
</project>"#;
#[test]
fn parses_full_pom() {
let p = parse_pom(FULL_POM).expect("ok");
assert_eq!(p.group_id, "com.example");
assert_eq!(p.artifact_id, "foo");
assert_eq!(p.version, "1.2.3");
assert_eq!(p.packaging, "jar");
assert_eq!(p.model_version.as_deref(), Some("4.0.0"));
}
#[test]
fn inherits_group_and_version_from_parent() {
let p = parse_pom(PARENT_INHERITED_POM).expect("ok");
assert_eq!(p.group_id, "com.example.parent");
assert_eq!(p.artifact_id, "child");
assert_eq!(p.version, "9.9.9");
assert_eq!(p.packaging, "jar");
let parent = p.parent.expect("parent present");
assert_eq!(parent.artifact_id, "parent-pom");
}
#[test]
fn missing_artifact_id_fails() {
let bad = r"<project><groupId>a</groupId><version>1</version></project>";
let err = parse_pom(bad).expect_err("reject");
assert!(err.to_string().contains("artifactId"));
}
#[test]
fn malformed_xml_fails() {
let err = parse_pom("<project><artifactId>oops").expect_err("reject");
assert!(err.to_string().contains("XML parse failed"));
}
#[test]
fn quick_xml_unreachable_panic_caught_2026_05_15() {
let bytes: &[u8] = &[
0x3c, 0x3e, 0x3c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x09, 0x70, 0x0a, 0x2e,
0x0a, 0x24, 0x0a, 0x02, 0x0a, 0x2e, 0x3e, 0x3e, 0x23, 0x3e, 0x3c, 0x21, 0x44, 0x4f,
0x43, 0x54, 0x59, 0x50, 0x65, 0x09, 0x3b, 0x3a, 0x3d, 0x22, 0x22, 0x3a, 0x0d, 0x2f,
0x3e, 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x65, 0x09, 0x3b, 0x3a, 0x3d,
0x22, 0x30, 0x22, 0x31, 0x22, 0x2e, 0x3c, 0x2f, 0x3e, 0x3c, 0x22, 0x3e, 0x23, 0x3e,
0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x65, 0x09, 0x3b, 0x3a, 0x3d, 0x22,
0x22, 0x3a, 0x0d, 0x2f, 0x3e, 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x65,
0x09, 0x3b, 0x3a, 0x3d, 0x22, 0x30, 0x22, 0x31, 0x22, 0x2e, 0x3c, 0x2f, 0x3e, 0x3c,
0x2f, 0x3e, 0x3c, 0x2f, 0x22, 0x3e, 0x3e, 0x0a, 0x05, 0x0a, 0x23, 0x0a, 0x70, 0x0a,
0x05, 0x0a,
];
let s = std::str::from_utf8(bytes).expect("artifact bytes are valid UTF-8");
let err = parse_pom(s).expect_err("must reject without panicking");
let msg = err.to_string();
assert!(
msg.contains("XML parse failed") || msg.contains("XML parser panicked"),
"unexpected error: {msg}"
);
}
}