use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashSet},
fmt, fs, io, ops,
path::{Path, PathBuf},
str,
};
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManifestFile {
manifest: Manifest,
path: PathBuf,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Manifest {
#[serde(rename = "package")]
pub pkg: Package,
#[serde(default, rename = "dependencies", with = "serde_opt")]
pub deps: Dependencies,
#[serde(default, rename = "contract-dependencies", with = "serde_opt")]
pub contract_deps: ContractDependencies,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Package {
pub name: String,
pub license: Option<String>,
#[serde(default)]
pub kind: PackageKind,
#[serde(rename = "entry-point")]
pub entry_point: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum PackageKind {
#[default]
#[serde(rename = "contract")]
Contract,
#[serde(rename = "library")]
Library,
}
pub type Dependencies = BTreeMap<String, Dependency>;
pub type ContractDependencies = BTreeMap<String, Dependency>;
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Dependency {
#[serde(flatten)]
pub source: dependency::Source,
pub package: Option<String>,
}
#[derive(Debug, Error)]
pub enum InvalidName {
#[error("must only contain ASCII non-uppercase alphanumeric chars, dashes or underscores")]
InvalidChar,
#[error("must begin with an alphabetic character")]
NonAlphabeticStart,
#[error("must end with an alphanumeric character")]
NonAlphanumericEnd,
#[error("must not be a pint language keyword")]
PintKeyword,
#[error("the given name is a word reserved by pint")]
Reserved,
}
#[derive(Debug, Error)]
#[error(r#"failed to parse package kind, expected "contract" or "library""#)]
pub struct InvalidPkgKind;
#[derive(Debug, Error)]
pub enum InvalidManifest {
#[error("manifest specifies an invalid package name {0:?}: {1}")]
PkgName(String, InvalidName),
#[error("manifest specifies an invalid dependency name {0:?}: {1}")]
DepName(String, InvalidName),
#[error("dependency name {0:?} appears more than once")]
DupDepName(String),
}
#[derive(Debug, Error)]
pub enum ManifestError {
#[error("failed to deserialize manifest from toml: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid manifest: {0}")]
Invalid(#[from] InvalidManifest),
}
#[derive(Debug, Error)]
pub enum ManifestFileError {
#[error("an IO error occurred while constructing the `ManifestFile`: {0}")]
Io(#[from] io::Error),
#[error("{0}")]
Manifest(#[from] ManifestError),
}
impl Manifest {
pub const DEFAULT_CONTRACT_ENTRY_POINT: &'static str = "contract.pnt";
pub const DEFAULT_LIBRARY_ENTRY_POINT: &'static str = "lib.pnt";
pub fn entry_point_str(&self) -> &str {
self.pkg
.entry_point
.as_deref()
.unwrap_or(match self.pkg.kind {
PackageKind::Contract => Self::DEFAULT_CONTRACT_ENTRY_POINT,
PackageKind::Library => Self::DEFAULT_LIBRARY_ENTRY_POINT,
})
}
}
impl ManifestFile {
pub const FILE_NAME: &'static str = "pint.toml";
pub fn from_path(path: &Path) -> Result<Self, ManifestFileError> {
let path = path.canonicalize()?;
let string = fs::read_to_string(&path)?;
let manifest: Manifest = string.parse()?;
let manifest_file = Self { manifest, path };
Ok(manifest_file)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn dir(&self) -> &Path {
self.path
.parent()
.expect("manifest file has no parent directory")
}
pub fn src_dir(&self) -> PathBuf {
self.dir().join("src")
}
pub fn out_dir(&self) -> PathBuf {
self.dir().join("out")
}
pub fn entry_point(&self) -> PathBuf {
self.src_dir().join(self.entry_point_str())
}
pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
self.deps
.get(dep_name)
.or_else(|| self.contract_deps.get(dep_name))
}
pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
let dir = self.dir();
let dep = self.dep(dep_name)?;
match &dep.source {
dependency::Source::Path(dep) => match dep.path.is_absolute() {
true => Some(dep.path.to_owned()),
false => dir.join(&dep.path).canonicalize().ok(),
},
}
}
}
impl fmt::Display for PackageKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Self::Contract => "contract",
Self::Library => "library",
};
write!(f, "{}", s)
}
}
impl str::FromStr for PackageKind {
type Err = InvalidPkgKind;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let kind = match s {
"contract" => Self::Contract,
"library" => Self::Library,
_ => return Err(InvalidPkgKind),
};
Ok(kind)
}
}
impl str::FromStr for Manifest {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let toml_de = toml::de::Deserializer::new(s);
let mut ignored_paths = vec![];
let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
ignored_paths.push(format!("{path}"));
})?;
check(&manifest)?;
Ok(manifest)
}
}
impl ops::Deref for ManifestFile {
type Target = Manifest;
fn deref(&self) -> &Self::Target {
&self.manifest
}
}
pub fn check(manifest: &Manifest) -> Result<(), InvalidManifest> {
check_name(&manifest.pkg.name)
.map_err(|e| InvalidManifest::PkgName(manifest.pkg.name.to_string(), e))?;
let mut names = HashSet::new();
for name in manifest.deps.keys().chain(manifest.contract_deps.keys()) {
check_name(name).map_err(|e| InvalidManifest::DepName(manifest.pkg.name.to_string(), e))?;
if !names.insert(name) {
return Err(InvalidManifest::DupDepName(name.to_string()));
}
}
Ok(())
}
pub fn check_name_char(ch: char) -> bool {
(ch.is_ascii_alphanumeric() && !ch.is_uppercase()) || ch == '-' || ch == '_'
}
pub fn check_name(name: &str) -> Result<(), InvalidName> {
if !name.chars().all(check_name_char) {
return Err(InvalidName::InvalidChar);
}
if matches!(name.chars().next(), Some(ch) if !ch.is_ascii_alphabetic()) {
return Err(InvalidName::NonAlphabeticStart);
}
if matches!(name.chars().last(), Some(ch) if !ch.is_ascii_alphanumeric()) {
return Err(InvalidName::NonAlphanumericEnd);
}
if PINT_KEYWORDS.contains(&name) {
return Err(InvalidName::PintKeyword);
}
if RESERVED.contains(&name) {
return Err(InvalidName::Reserved);
}
Ok(())
}
const PINT_KEYWORDS: &[&str] = &[
"as",
"bool",
"b256",
"cond",
"constraint",
"else",
"enum",
"exists",
"forall",
"if",
"in",
"int",
"predicate",
"interface",
"macro",
"real",
"self",
"state",
"storage",
"string",
"type",
"use",
"var",
"where",
];
const RESERVED: &[&str] = &[
"contract",
"dep",
"dependency",
"lib",
"library",
"mod",
"module",
"root",
];
pub mod dependency {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Source {
Path(Path),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Path {
pub path: std::path::PathBuf,
}
}
mod serde_opt {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub(crate) fn serialize<S, T>(t: &T, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Default + PartialEq + Serialize,
{
let opt = (t != &T::default()).then_some(t);
opt.serialize(s)
}
pub(crate) fn deserialize<'de, D, T>(d: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
let opt: Option<T> = <_>::deserialize(d)?;
Ok(opt.unwrap_or_default())
}
}