use crate::{
file::Manifest,
prelude::{Network, ProgramID},
synthesizer::Program,
};
use anyhow::{anyhow, bail, ensure, Result};
use core::str::FromStr;
use std::{
fs::{self, File},
io::Write,
path::Path,
};
static ALEO_FILE_EXTENSION: &str = "aleo";
pub struct AleoFile<N: Network> {
file_name: String,
program_string: String,
program: Program<N>,
}
impl<N: Network> FromStr for AleoFile<N> {
type Err = anyhow::Error;
#[inline]
fn from_str(s: &str) -> Result<Self> {
let program = Program::from_str(s)?;
let program_string = s.to_string();
let file_name = match program.id().is_aleo() {
true => program.id().name().to_string(),
false => program.id().to_string(),
};
Ok(Self { file_name, program_string, program })
}
}
impl<N: Network> AleoFile<N> {
pub fn create(directory: &Path, program_id: &ProgramID<N>, is_main: bool) -> Result<Self> {
ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display());
ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): '{program_id}'");
let program_string = format!(
r#"// The '{program_id}' program.
program {program_id};
function hello:
input r0 as u32.public;
input r1 as u32.private;
add r0 r1 into r2;
output r2 as u32.private;
"#
);
let file_name = if is_main {
Self::main_file_name()
} else {
match program_id.is_aleo() {
true => program_id.to_string(),
false => format!("{program_id}.{ALEO_FILE_EXTENSION}"),
}
};
let path = directory.join(file_name);
ensure!(!path.exists(), "The Aleo file already exists: {}", path.display());
File::create(&path)?.write_all(program_string.as_bytes())?;
Self::from_filepath(&path)
}
pub fn open(directory: &Path, program_id: &ProgramID<N>, is_main: bool) -> Result<Self> {
ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display());
let file_name = if is_main {
Self::main_file_name()
} else {
match program_id.is_aleo() {
true => program_id.to_string(),
false => format!("{program_id}.{ALEO_FILE_EXTENSION}"),
}
};
let path = directory.join(file_name);
ensure!(path.exists(), "The Aleo file is missing: '{}'", path.display());
let aleo_file = Self::from_filepath(&path)?;
if is_main && aleo_file.program.id() != program_id {
bail!("The program ID from `{}` does not match in '{}'", Manifest::<N>::file_name(), path.display())
}
Ok(aleo_file)
}
pub fn exists_at(&self, file_path: &Path) -> bool {
Self::check_path(file_path).is_ok() && file_path.exists()
}
pub fn main_exists_at(directory: &Path) -> bool {
let path = directory.join(Self::main_file_name());
path.is_file() && path.exists()
}
pub fn main_file_name() -> String {
format!("main.{ALEO_FILE_EXTENSION}")
}
pub fn file_name(&self) -> &str {
&self.file_name
}
pub fn program_string(&self) -> &str {
&self.program_string
}
pub const fn program(&self) -> &Program<N> {
&self.program
}
pub fn write_to(&self, path: &Path) -> Result<()> {
Self::check_path(path)?;
let file_name = path
.file_stem()
.ok_or_else(|| anyhow!("File name not found."))?
.to_str()
.ok_or_else(|| anyhow!("File name not found."))?
.to_string();
ensure!(file_name == self.file_name, "File name does not match.");
Ok(File::create(path)?.write_all(self.program_string.as_bytes())?)
}
pub fn remove(&self, path: &Path) -> Result<()> {
if !path.exists() {
Ok(())
} else {
Self::check_path(path)?;
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
}
}
impl<N: Network> AleoFile<N> {
fn check_path(path: &Path) -> Result<()> {
ensure!(path.is_file(), "The path is not a file.");
let extension = path.extension().ok_or_else(|| anyhow!("File extension not found."))?;
ensure!(extension == ALEO_FILE_EXTENSION, "File extension is incorrect.");
Ok(())
}
fn from_filepath(file: &Path) -> Result<Self> {
Self::check_path(file)?;
ensure!(file.exists(), "File does not exist: {}", file.display());
let file_name = file
.file_stem()
.ok_or_else(|| anyhow!("File name not found."))?
.to_str()
.ok_or_else(|| anyhow!("File name not found."))?
.to_string();
let program_string = fs::read_to_string(file)?;
let program = Program::from_str(&program_string)?;
Ok(Self { file_name, program_string, program })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::Parser;
type CurrentNetwork = snarkvm_console::network::Testnet3;
fn temp_dir() -> std::path::PathBuf {
tempfile::tempdir().expect("Failed to open temporary directory").into_path()
}
#[test]
fn test_from_str() {
let program_string = r"
program token.aleo;
record token:
owner as address.private;
token_amount as u64.private;
function compute:
input r0 as token.record;
add r0.token_amount r0.token_amount into r1;
output r1 as u64.private;";
let (string, program) = Program::<CurrentNetwork>::parse(program_string).unwrap();
assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'");
let file = AleoFile::from_str(program_string).unwrap();
assert_eq!("token", file.file_name());
assert_eq!(program_string, file.program_string());
assert_eq!(&program, file.program());
}
#[test]
fn test_from_path() {
let directory = temp_dir();
let program_string = r"
program token.aleo;
record token:
owner as address.private;
token_amount as u64.private;
function compute:
input r0 as token.record;
add r0.token_amount r0.token_amount into r1;
output r1 as u64.private;";
let path = directory.join("token.aleo");
let mut file = File::create(&path).unwrap();
file.write_all(program_string.as_bytes()).unwrap();
let file = AleoFile::from_filepath(&path).unwrap();
let (string, program) = Program::<CurrentNetwork>::parse(program_string).unwrap();
assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'");
assert_eq!("token", file.file_name());
assert_eq!(program_string, file.program_string());
assert_eq!(&program, file.program());
}
}