use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct ConanTxtParser;
impl ManifestParser for ConanTxtParser {
fn filename(&self) -> &'static str {
"conanfile.txt"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let deps = parse_conan_txt(content);
Ok(ParsedManifest {
ecosystem: "conan",
name: None,
version: None,
dependencies: deps,
})
}
}
pub(crate) fn parse_conan_txt(content: &str) -> Vec<DeclaredDep> {
let mut deps = Vec::new();
let mut in_requires = false;
for line in content.lines() {
let line = line.trim();
if line.starts_with('[') {
in_requires = line.eq_ignore_ascii_case("[requires]");
continue;
}
if !in_requires || line.is_empty() || line.starts_with('#') {
continue;
}
let dep_str = line.split('#').next().unwrap_or(line).trim();
if dep_str.is_empty() {
continue;
}
if let Some(slash_idx) = dep_str.find('/') {
let name = dep_str[..slash_idx].trim().to_string();
let rest = dep_str[slash_idx + 1..].trim();
let version = rest.split('@').next().unwrap_or(rest).trim();
let version_req = if version.is_empty() {
None
} else {
Some(version.to_string())
};
if !name.is_empty() {
deps.push(DeclaredDep {
name,
version_req,
kind: DepKind::Normal,
});
}
} else {
deps.push(DeclaredDep {
name: dep_str.to_string(),
version_req: None,
kind: DepKind::Normal,
});
}
}
deps
}
pub struct ConanPyParser;
impl ManifestParser for ConanPyParser {
fn filename(&self) -> &'static str {
"conanfile.py"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut deps = Vec::new();
let mut in_requires_list = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("requires") && trimmed.contains('=') && !in_requires_list {
let after_eq = trimmed.split_once('=').map(|x| x.1).unwrap_or("").trim();
extract_quoted_refs(after_eq, &mut deps);
if after_eq.contains('[') && !after_eq.contains(']') {
in_requires_list = true;
}
continue;
}
if in_requires_list {
extract_quoted_refs(trimmed, &mut deps);
if trimmed.contains(']') {
in_requires_list = false;
}
continue;
}
if (trimmed.starts_with("self.requires(") || trimmed.starts_with("self.tool_requires("))
&& trimmed.contains('"')
&& let Some(ref_str) = extract_first_string(trimmed)
&& let Some(dep) = conan_ref_to_dep(&ref_str)
{
deps.push(dep);
}
}
Ok(ParsedManifest {
ecosystem: "conan",
name: None,
version: None,
dependencies: deps,
})
}
}
fn extract_quoted_refs(line: &str, out: &mut Vec<DeclaredDep>) {
let mut rest = line;
while let Some(start) = rest.find('"') {
rest = &rest[start + 1..];
if let Some(end) = rest.find('"') {
let s = &rest[..end];
if let Some(dep) = conan_ref_to_dep(s) {
out.push(dep);
}
rest = &rest[end + 1..];
} else {
break;
}
}
}
fn extract_first_string(line: &str) -> Option<String> {
let start = line.find('"')? + 1;
let end = line[start..].find('"')?;
Some(line[start..start + end].to_string())
}
fn conan_ref_to_dep(s: &str) -> Option<DeclaredDep> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(slash_idx) = s.find('/') {
let name = s[..slash_idx].trim().to_string();
let rest = s[slash_idx + 1..].trim();
let version = rest.split('@').next().unwrap_or(rest).trim();
let version_req = if version.is_empty() {
None
} else {
Some(version.to_string())
};
if name.is_empty() {
return None;
}
Some(DeclaredDep {
name,
version_req,
kind: DepKind::Normal,
})
} else {
Some(DeclaredDep {
name: s.to_string(),
version_req: None,
kind: DepKind::Normal,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_conan_txt() {
let content = r#"[requires]
zlib/1.2.13
boost/1.83.0@conan/stable
openssl/3.1.0 # pinned
[generators]
cmake
"#;
let m = ConanTxtParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "conan");
assert_eq!(m.dependencies.len(), 3);
let zlib = m.dependencies.iter().find(|d| d.name == "zlib").unwrap();
assert_eq!(zlib.version_req.as_deref(), Some("1.2.13"));
let boost = m.dependencies.iter().find(|d| d.name == "boost").unwrap();
assert_eq!(boost.version_req.as_deref(), Some("1.83.0"));
}
#[test]
fn test_conan_py_list() {
let content = r#"from conan import ConanFile
class MyConan(ConanFile):
requires = ["zlib/1.2.13", "boost/1.83.0"]
tool_requires = []
"#;
let m = ConanPyParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "conan");
assert_eq!(m.dependencies.len(), 2);
assert!(m.dependencies.iter().any(|d| d.name == "zlib"));
assert!(m.dependencies.iter().any(|d| d.name == "boost"));
}
#[test]
fn test_conan_py_self_requires() {
let content = r#"from conan import ConanFile
class MyConan(ConanFile):
def requirements(self):
self.requires("zlib/1.2.13")
self.requires("openssl/3.1.0", headers=True)
self.tool_requires("cmake/3.25.0")
"#;
let m = ConanPyParser.parse(content).unwrap();
assert_eq!(m.dependencies.len(), 3);
}
}