// Copyright 2024 Aleo Network Foundation // This file is part of the snarkVM library. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at: // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::{ file::Manifest, prelude::{Network, ProgramID}, synthesizer::Program, }; use anyhow::{Result, anyhow, bail, ensure}; use core::str::FromStr; use std::{ fs::{self, File}, io::Write, path::Path, }; static ALEO_FILE_EXTENSION: &str = "aleo"; pub struct AleoFile { /// The file name (without the extension). file_name: String, /// The program as a string. program_string: String, /// The program. program: Program, } impl FromStr for AleoFile { type Err = anyhow::Error; /// Reads the file from a string. #[inline] fn from_str(s: &str) -> Result { let program = Program::from_str(s)?; let program_string = s.to_string(); // The file name is defined as the string up to the extension (excluding the extension). 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 AleoFile { /// Creates a new Aleo program file with the given directory path, program ID, and `is_main` indicator. pub fn create(directory: &Path, program_id: &ProgramID, is_main: bool) -> Result { // Ensure the directory path exists. ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display()); // Ensure the program name is valid. ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): '{program_id}'"); // Construct the initial program string. 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; "# ); // Create the file. 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}"), } }; // Construct the file path. let path = directory.join(file_name); // Ensure the file path does not already exist. ensure!(!path.exists(), "The Aleo file already exists: {}", path.display()); // Write the file. File::create(&path)?.write_all(program_string.as_bytes())?; // Return the Aleo file. Self::from_filepath(&path) } /// Opens the Aleo program file, given the directory path, program ID, and `is_main` indicator. pub fn open(directory: &Path, program_id: &ProgramID, is_main: bool) -> Result { // Ensure the directory path exists. ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display()); // Create the file. 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}"), } }; // Construct the file path. let path = directory.join(file_name); // Ensure the file path exists. ensure!(path.exists(), "The Aleo file is missing: '{}'", path.display()); // Load the Aleo file. let aleo_file = Self::from_filepath(&path)?; // Ensure the program ID matches, if this is the main file. if is_main && aleo_file.program.id() != program_id { bail!("The program ID from `{}` does not match in '{}'", Manifest::::file_name(), path.display()) } Ok(aleo_file) } /// Returns `true` if the file exists at the given path. pub fn exists_at(&self, file_path: &Path) -> bool { // Ensure the path is well-formed. Self::check_path(file_path).is_ok() && file_path.exists() } /// Returns `true` if the main program file exists at the given path. pub fn main_exists_at(directory: &Path) -> bool { // Construct the file path. let path = directory.join(Self::main_file_name()); // Return the result. path.is_file() && path.exists() } /// Returns the main Aleo program file name. pub fn main_file_name() -> String { format!("main.{ALEO_FILE_EXTENSION}") } /// Returns the file name. pub fn file_name(&self) -> &str { &self.file_name } /// Returns the program string. pub fn program_string(&self) -> &str { &self.program_string } /// Returns the program. pub const fn program(&self) -> &Program { &self.program } /// Writes the program string to the file. pub fn write_to(&self, path: &Path) -> Result<()> { // Ensure the path is well-formed. Self::check_path(path)?; // Retrieve the file name. 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 the file name matches the expected file name. ensure!(file_name == self.file_name, "File name does not match."); Ok(File::create(path)?.write_all(self.program_string.as_bytes())?) } /// Removes the file at the given path, if it exists. pub fn remove(&self, path: &Path) -> Result<()> { // If the path does not exist, do nothing. if !path.exists() { Ok(()) } else { // Ensure the path is well-formed. Self::check_path(path)?; // If the path exists, remove it. if path.exists() { // Remove the file. fs::remove_file(path)?; } Ok(()) } } } impl AleoFile { /// Checks that the given path has the correct file extension. fn check_path(path: &Path) -> Result<()> { // Ensure the given path is a file. ensure!(path.is_file(), "The path is not a file."); // Ensure the given path has the correct file extension. let extension = path.extension().ok_or_else(|| anyhow!("File extension not found."))?; ensure!(extension == ALEO_FILE_EXTENSION, "File extension is incorrect."); Ok(()) } /// Reads the program from the given file path, if it exists. fn from_filepath(file: &Path) -> Result { // Ensure the path is well-formed. Self::check_path(file)?; // Ensure the given path exists. ensure!(file.exists(), "File does not exist: {}", file.display()); // Retrieve the file name. 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(); // Read the program string. let program_string = fs::read_to_string(file)?; // Parse the program string. 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::MainnetV0; 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;"; // Initialize a new program. let (string, program) = Program::::parse(program_string).unwrap(); assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'"); // Read the program from the 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() { // Initialize a temporary directory. 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;"; // Write the program string to a file in the temporary directory. let path = directory.join("token.aleo"); let mut file = File::create(&path).unwrap(); file.write_all(program_string.as_bytes()).unwrap(); // Read the program from the path. let file = AleoFile::from_filepath(&path).unwrap(); // Initialize a new program. let (string, program) = Program::::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()); } }