1use serde::{Deserialize, Serialize};
4use std::{
5 collections::{BTreeMap, HashSet},
6 fmt, fs, io, ops,
7 path::{Path, PathBuf},
8 str,
9};
10use thiserror::Error;
11
12#[derive(Clone, Debug, Eq, PartialEq, Hash)]
14pub struct ManifestFile {
15 manifest: Manifest,
17 path: PathBuf,
19}
20
21#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
25pub struct Manifest {
26 #[serde(rename = "package")]
28 pub pkg: Package,
29 #[serde(default, rename = "dependencies", with = "serde_opt")]
31 pub deps: Dependencies,
32 #[serde(default, rename = "contract-dependencies", with = "serde_opt")]
34 pub contract_deps: ContractDependencies,
35}
36
37#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
39pub struct Package {
40 pub name: String,
42 pub license: Option<String>,
44 #[serde(default)]
46 pub kind: PackageKind,
47 #[serde(rename = "entry-point")]
56 pub entry_point: Option<String>,
57}
58
59#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
61pub enum PackageKind {
62 #[default]
64 #[serde(rename = "contract")]
65 Contract,
66 #[serde(rename = "library")]
68 Library,
69}
70
71pub type Dependencies = BTreeMap<String, Dependency>;
73pub type ContractDependencies = BTreeMap<String, Dependency>;
75
76#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
78pub struct Dependency {
79 #[serde(flatten)]
81 pub source: dependency::Source,
82 pub package: Option<String>,
85}
86
87#[derive(Debug, Error)]
89pub enum InvalidName {
90 #[error("must only contain ASCII non-uppercase alphanumeric chars, dashes or underscores")]
92 InvalidChar,
93 #[error("must begin with an alphabetic character")]
95 NonAlphabeticStart,
96 #[error("must end with an alphanumeric character")]
98 NonAlphanumericEnd,
99 #[error("must not be a pint language keyword")]
101 PintKeyword,
102 #[error("the given name is a word reserved by pint")]
104 Reserved,
105}
106
107#[derive(Debug, Error)]
109#[error(r#"failed to parse package kind, expected "contract" or "library""#)]
110pub struct InvalidPkgKind;
111
112#[derive(Debug, Error)]
114pub enum InvalidManifest {
115 #[error("manifest specifies an invalid package name {0:?}: {1}")]
117 PkgName(String, InvalidName),
118 #[error("manifest specifies an invalid dependency name {0:?}: {1}")]
120 DepName(String, InvalidName),
121 #[error("dependency name {0:?} appears more than once")]
123 DupDepName(String),
124}
125
126#[derive(Debug, Error)]
128pub enum ManifestError {
129 #[error("failed to deserialize manifest from toml: {0}")]
131 Toml(#[from] toml::de::Error),
132 #[error("invalid manifest: {0}")]
134 Invalid(#[from] InvalidManifest),
135}
136
137#[derive(Debug, Error)]
139pub enum ManifestFileError {
140 #[error("an IO error occurred while constructing the `ManifestFile`: {0}")]
141 Io(#[from] io::Error),
142 #[error("{0}")]
144 Manifest(#[from] ManifestError),
145}
146
147impl Manifest {
148 pub const DEFAULT_CONTRACT_ENTRY_POINT: &'static str = "contract.pnt";
150 pub const DEFAULT_LIBRARY_ENTRY_POINT: &'static str = "lib.pnt";
152
153 pub fn entry_point_str(&self) -> &str {
155 self.pkg
156 .entry_point
157 .as_deref()
158 .unwrap_or(match self.pkg.kind {
159 PackageKind::Contract => Self::DEFAULT_CONTRACT_ENTRY_POINT,
160 PackageKind::Library => Self::DEFAULT_LIBRARY_ENTRY_POINT,
161 })
162 }
163}
164
165impl ManifestFile {
166 pub const FILE_NAME: &'static str = "pint.toml";
168
169 pub fn from_path(path: &Path) -> Result<Self, ManifestFileError> {
173 let path = path.canonicalize()?;
174 let string = fs::read_to_string(&path)?;
175 let manifest: Manifest = string.parse()?;
176 let manifest_file = Self { manifest, path };
177 Ok(manifest_file)
178 }
179
180 pub fn path(&self) -> &Path {
182 &self.path
183 }
184
185 pub fn dir(&self) -> &Path {
187 self.path
188 .parent()
189 .expect("manifest file has no parent directory")
190 }
191
192 pub fn src_dir(&self) -> PathBuf {
194 self.dir().join("src")
195 }
196
197 pub fn out_dir(&self) -> PathBuf {
199 self.dir().join("out")
200 }
201
202 pub fn entry_point(&self) -> PathBuf {
204 self.src_dir().join(self.entry_point_str())
205 }
206
207 pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
209 self.deps
210 .get(dep_name)
211 .or_else(|| self.contract_deps.get(dep_name))
212 }
213
214 pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
216 let dir = self.dir();
217 let dep = self.dep(dep_name)?;
218 match &dep.source {
219 dependency::Source::Path(dep) => match dep.path.is_absolute() {
220 true => Some(dep.path.to_owned()),
221 false => dir.join(&dep.path).canonicalize().ok(),
222 },
223 }
224 }
225}
226
227impl fmt::Display for PackageKind {
228 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229 let s = match self {
230 Self::Contract => "contract",
231 Self::Library => "library",
232 };
233 write!(f, "{}", s)
234 }
235}
236
237impl str::FromStr for PackageKind {
238 type Err = InvalidPkgKind;
239 fn from_str(s: &str) -> Result<Self, Self::Err> {
240 let kind = match s {
241 "contract" => Self::Contract,
242 "library" => Self::Library,
243 _ => return Err(InvalidPkgKind),
244 };
245 Ok(kind)
246 }
247}
248
249impl str::FromStr for Manifest {
250 type Err = ManifestError;
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 let toml_de = toml::de::Deserializer::new(s);
253 let mut ignored_paths = vec![];
254 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
255 ignored_paths.push(format!("{path}"));
257 })?;
258 check(&manifest)?;
259 Ok(manifest)
260 }
261}
262
263impl ops::Deref for ManifestFile {
264 type Target = Manifest;
265 fn deref(&self) -> &Self::Target {
266 &self.manifest
267 }
268}
269
270pub fn check(manifest: &Manifest) -> Result<(), InvalidManifest> {
272 check_name(&manifest.pkg.name)
274 .map_err(|e| InvalidManifest::PkgName(manifest.pkg.name.to_string(), e))?;
275
276 let mut names = HashSet::new();
278 for name in manifest.deps.keys().chain(manifest.contract_deps.keys()) {
279 check_name(name).map_err(|e| InvalidManifest::DepName(manifest.pkg.name.to_string(), e))?;
281
282 if !names.insert(name) {
284 return Err(InvalidManifest::DupDepName(name.to_string()));
285 }
286 }
287
288 Ok(())
289}
290
291pub fn check_name_char(ch: char) -> bool {
293 (ch.is_ascii_alphanumeric() && !ch.is_uppercase()) || ch == '-' || ch == '_'
294}
295
296pub fn check_name(name: &str) -> Result<(), InvalidName> {
298 if !name.chars().all(check_name_char) {
299 return Err(InvalidName::InvalidChar);
300 }
301
302 if matches!(name.chars().next(), Some(ch) if !ch.is_ascii_alphabetic()) {
303 return Err(InvalidName::NonAlphabeticStart);
304 }
305
306 if matches!(name.chars().last(), Some(ch) if !ch.is_ascii_alphanumeric()) {
307 return Err(InvalidName::NonAlphanumericEnd);
308 }
309
310 if PINT_KEYWORDS.contains(&name) {
311 return Err(InvalidName::PintKeyword);
312 }
313
314 if RESERVED.contains(&name) {
315 return Err(InvalidName::Reserved);
316 }
317
318 Ok(())
319}
320
321const PINT_KEYWORDS: &[&str] = &[
322 "as",
323 "bool",
324 "b256",
325 "cond",
326 "constraint",
327 "else",
328 "exists",
329 "forall",
330 "if",
331 "in",
332 "int",
333 "interface",
334 "macro",
335 "match",
336 "nil",
337 "predicate",
338 "mut",
339 "real",
340 "self",
341 "let",
342 "storage",
343 "string",
344 "type",
345 "union",
346 "use",
347 "where",
348];
349
350const RESERVED: &[&str] = &[
351 "contract",
352 "dep",
353 "dependency",
354 "lib",
355 "library",
356 "mod",
357 "module",
358 "root",
359];
360
361pub mod dependency {
363 use serde::{Deserialize, Serialize};
364
365 #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
367 #[serde(untagged)]
368 pub enum Source {
369 Path(Path),
371 }
372
373 #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
375 pub struct Path {
376 pub path: std::path::PathBuf,
378 }
379}
380
381mod serde_opt {
384 use serde::{Deserialize, Deserializer, Serialize, Serializer};
385
386 pub(crate) fn serialize<S, T>(t: &T, s: S) -> Result<S::Ok, S::Error>
388 where
389 S: Serializer,
390 T: Default + PartialEq + Serialize,
391 {
392 let opt = (t != &T::default()).then_some(t);
393 opt.serialize(s)
394 }
395
396 pub(crate) fn deserialize<'de, D, T>(d: D) -> Result<T, D::Error>
398 where
399 D: Deserializer<'de>,
400 T: Default + Deserialize<'de>,
401 {
402 let opt: Option<T> = <_>::deserialize(d)?;
403 Ok(opt.unwrap_or_default())
404 }
405}