use anyhow::{bail, Result};
#[derive(Debug, Default, Clone)]
pub struct ScriptMeta {
pub dependencies: String,
pub features: String,
pub edition: String,
}
pub fn parse_meta(source: &str) -> Result<ScriptMeta> {
if let Some(m) = parse_cargo_script_block_comment(source)? {
return Ok(m);
}
if let Some(m) = parse_block_comment(source)? {
return Ok(m);
}
if let Some(m) = parse_cargo_script_short_comment(source)? {
return Ok(m);
}
if let Some(m) = parse_cargo_play_deps(source)? {
return Ok(m);
}
if let Some(m) = parse_line_comments(source)? {
return Ok(m);
}
Ok(ScriptMeta {
edition: "2024".into(),
..Default::default()
})
}
fn parse_cargo_script_block_comment(source: &str) -> Result<Option<ScriptMeta>> {
let src = strip_shebang(source);
let start_marker = "//! ```cargo";
if let Some(start) = src.find(start_marker) {
let after_start = start + start_marker.len();
if let Some(end) = src[after_start..].find("```") {
let block = &src[after_start..after_start + end];
let fragment = block
.lines()
.map(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with("//!") {
trimmed[3..].trim_start()
} else {
trimmed
}
})
.collect::<Vec<_>>()
.join("\n");
return parse_toml_fragment(&fragment);
}
}
Ok(None)
}
fn parse_cargo_script_short_comment(source: &str) -> Result<Option<ScriptMeta>> {
let src = strip_shebang(source);
let lines: Vec<&str> = src.lines().collect();
if lines.is_empty() {
return Ok(None);
}
let deps_line = lines.iter().find(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("// cargo-deps:") || trimmed.starts_with("//cargo-deps:")
});
if let Some(line) = deps_line {
let deps_part = line.split(':').nth(1).unwrap_or("").trim();
let mut toml_fragment = String::from("[dependencies]\n");
for pair in deps_part.split(',') {
let trimmed_pair = pair.trim();
if trimmed_pair.is_empty() {
continue;
}
if let Some((name, version)) = trimmed_pair.split_once('=') {
let name_trim = name.trim();
let version_trim = version.trim();
let version_clean = version_trim
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.unwrap_or(version_trim);
toml_fragment.push_str(&format!("{} = \"{}\"\n", name_trim, version_clean));
} else {
toml_fragment.push_str(&format!("{} = \"*\"\n", trimmed_pair));
}
}
return parse_toml_fragment(&toml_fragment);
}
Ok(None)
}
fn parse_cargo_play_deps(source: &str) -> Result<Option<ScriptMeta>> {
let src = strip_shebang(source);
let mut deps = String::new();
let mut found = false;
for line in src.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("//#") {
found = true;
let spec = rest.trim();
if spec.contains('=') {
deps.push_str(&format!("{}\n", spec));
} else {
deps.push_str(&format!("{} = \"*\"\n", spec));
}
} else if found && !trimmed.is_empty() && !trimmed.starts_with("//") {
break;
}
}
if found {
let toml_fragment = format!("[dependencies]\n{}", deps);
parse_toml_fragment(&toml_fragment)
} else {
Ok(None)
}
}
fn parse_block_comment(source: &str) -> Result<Option<ScriptMeta>> {
let src = strip_shebang(source);
let start = match src.find("/*") {
Some(p) => p,
None => return Ok(None),
};
if !src[..start].trim().is_empty() {
return Ok(None);
}
let end = src[start..]
.find("*/")
.ok_or_else(|| anyhow::anyhow!("unclosed `/*` metadata block"))?;
let mut offset = 2; let after_start = start + offset;
if after_start < src.len() {
let third = src.chars().nth(after_start).unwrap_or(' ');
if third == '!' || third == '*' {
offset = 3;
}
}
let inner = &src[start + offset..start + end];
parse_toml_fragment(inner)
}
fn parse_line_comments(source: &str) -> Result<Option<ScriptMeta>> {
let src = strip_shebang(source);
let mut lines: Vec<&str> = Vec::new();
let mut started = false;
for line in src.lines() {
if let Some(rest) = line.trim().strip_prefix("//!") {
lines.push(rest.trim_start_matches(' '));
started = true;
} else if started {
break;
}
}
if !started {
return Ok(None);
}
parse_toml_fragment(&lines.join("\n"))
}
fn replace_comments(s: &str) -> String {
let mut out = String::new();
let mut in_string = false;
let mut i = 0;
let chars: Vec<char> = s.chars().collect();
while i < chars.len() {
let c = chars[i];
if c == '"' {
in_string = !in_string;
out.push(c);
i += 1;
continue;
}
if !in_string && c == '/' && i + 1 < chars.len() && chars[i + 1] == '/' {
out.push('#');
i += 2;
continue;
}
out.push(c);
i += 1;
}
out
}
fn parse_toml_fragment(fragment: &str) -> Result<Option<ScriptMeta>> {
let fragment = fragment.trim();
if fragment.is_empty() {
return Ok(Some(ScriptMeta {
edition: "2024".into(),
..Default::default()
}));
}
let cleaned = replace_comments(fragment);
let value: toml::Value = toml::from_str(&cleaned).map_err(|e| {
anyhow::anyhow!("invalid TOML in script metadata:\n{e}\n\nfragment:\n{fragment}")
})?;
let mut meta = ScriptMeta {
edition: "2024".into(),
..Default::default()
};
if let Some(e) = value.get("edition").and_then(|e| e.as_str()) {
match e {
"2015" | "2018" | "2021" | "2024" => meta.edition = e.into(),
other => bail!("unsupported edition `{other}` — valid: 2015, 2018, 2021, 2024"),
}
}
if let Some(f) = value.get("features") {
meta.features = toml::to_string(f)?;
}
meta.dependencies = extract_dependencies_text(fragment);
Ok(Some(meta))
}
fn extract_dependencies_text(fragment: &str) -> String {
let lines: Vec<&str> = fragment.lines().collect();
let mut in_deps = false;
let mut deps_lines = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with("[dependencies") && trimmed.ends_with(']') {
in_deps = true;
deps_lines.push(line);
continue;
}
if in_deps {
if trimmed.starts_with('[') && !trimmed.starts_with("[dependencies") {
break;
}
deps_lines.push(line);
}
}
if !deps_lines.is_empty() {
deps_lines.join("\n")
} else {
String::new()
}
}
fn strip_shebang(s: &str) -> &str {
if s.starts_with("#!") {
s.find('\n').map(|p| &s[p + 1..]).unwrap_or("")
} else {
s
}
}