use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
#[derive(Debug, Clone)]
pub struct XarkProject {
pub xark_dir: PathBuf,
}
impl XarkProject {
pub fn resolve(path: Option<PathBuf>) -> Result<Self> {
let base = match path {
Some(p) => p,
None => std::env::current_dir().context("resolving current directory")?,
};
Ok(Self {
xark_dir: resolve_xark_dir(&base)?,
})
}
pub fn r1cs_json(&self) -> PathBuf {
self.xark_dir.join("r1cs.json")
}
pub fn circuit_xbc(&self) -> PathBuf {
self.xark_dir.join("circuit.xbc")
}
pub fn proving_key(&self) -> PathBuf {
self.xark_dir.join("pk.bin")
}
pub fn verifying_key(&self) -> PathBuf {
self.xark_dir.join("vk.bin")
}
pub fn proof(&self) -> PathBuf {
self.xark_dir.join("proof.bin")
}
pub fn public_inputs(&self) -> PathBuf {
self.xark_dir.join("public_inputs.bin")
}
pub fn circuit_name(&self) -> String {
self.xark_dir
.file_name()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty() && *s != "xark")
.unwrap_or("circuit")
.to_string()
}
pub fn entry_name(&self) -> String {
std::fs::read_to_string(self.xark_dir.join("entry"))
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s != "circuit")
.unwrap_or_else(|| self.circuit_name())
}
pub fn snarkjs_vk(&self) -> PathBuf {
self.xark_dir.join("snarkjs-verification_key.json")
}
pub fn export_dir(&self) -> PathBuf {
self.xark_dir.join("verifier")
}
pub fn client_dir(&self) -> PathBuf {
self.xark_dir.join("client")
}
pub fn find_ptau(&self) -> Option<PathBuf> {
let mut dirs = vec![self.xark_dir.clone(), self.xark_dir.join("ptau")];
if let Some(shared) = self.xark_dir.parent() {
dirs.push(shared.to_path_buf());
dirs.push(shared.join("ptau"));
if let Some(root) = shared.parent().and_then(Path::parent) {
dirs.push(root.to_path_buf());
dirs.push(root.join("ptau"));
}
}
for dir in dirs {
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "ptau") {
return Some(path);
}
}
}
}
None
}
}
fn resolve_xark_dir(base: &Path) -> Result<PathBuf> {
if base.join("circuit.xbc").is_file()
|| base.join("r1cs.json").is_file()
|| base.join("circuit.json").is_file()
{
return Ok(base.to_path_buf());
}
if base.join("Cargo.toml").is_file() {
return Ok(scoped_dir(base));
}
if let Some(dir) = single_named_subdir(base)? {
return Ok(dir);
}
let mut dir = base.to_path_buf();
if !dir.is_dir() {
dir.pop();
}
loop {
if dir.join("Cargo.toml").is_file() {
return Ok(scoped_dir(&dir));
}
if !dir.pop() {
break;
}
}
Ok(base.to_path_buf())
}
fn scoped_dir(crate_dir: &Path) -> PathBuf {
let target_xark = crate_dir.join("target").join("xark");
let name = read_pkg_name(crate_dir).unwrap_or_else(|| {
crate_dir
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("circuit")
.to_string()
});
target_xark.join(name)
}
fn single_named_subdir(base: &Path) -> Result<Option<PathBuf>> {
if !base.is_dir() {
return Ok(None);
}
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(entries) = std::fs::read_dir(base) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir()
&& (p.join("circuit.xbc").is_file()
|| p.join("r1cs.json").is_file()
|| p.join("circuit.json").is_file())
{
candidates.push(p);
}
}
}
candidates.sort();
match candidates.len() {
0 => Ok(None),
1 => Ok(Some(candidates.remove(0))),
_ => {
let names: Vec<String> = candidates
.iter()
.filter_map(|p| p.file_name().and_then(|s| s.to_str()).map(String::from))
.collect();
bail!(
"ambiguous circuit: {} holds multiple built circuits ({}). \
Point at one explicitly, e.g. `{}/{}`.",
base.display(),
names.join(", "),
base.display(),
names.first().map(String::as_str).unwrap_or("<name>"),
)
}
}
}
pub(crate) fn read_pkg_name(crate_dir: &Path) -> Option<String> {
let text = std::fs::read_to_string(crate_dir.join("Cargo.toml")).ok()?;
let mut in_package = false;
for raw in text.lines() {
let line = raw.trim();
if line.starts_with('[') {
in_package = line == "[package]";
continue;
}
if in_package && let Some(rest) = line.strip_prefix("name") {
let rest = rest.trim_start();
if let Some(val) = rest.strip_prefix('=') {
let val = val.trim().trim_matches('"').trim_matches('\'');
if !val.is_empty() {
return Some(val.to_string());
}
}
}
}
None
}