snarkvm_debug/file/
aleo.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7// http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{
16    file::Manifest,
17    prelude::{Network, ProgramID},
18    synthesizer::Program,
19};
20
21use anyhow::{anyhow, bail, ensure, Result};
22use core::str::FromStr;
23use std::{
24    fs::{self, File},
25    io::Write,
26    path::Path,
27};
28
29static ALEO_FILE_EXTENSION: &str = "aleo";
30
31pub struct AleoFile<N: Network> {
32    /// The file name (without the extension).
33    file_name: String,
34    /// The program as a string.
35    program_string: String,
36    /// The program.
37    program: Program<N>,
38}
39
40impl<N: Network> FromStr for AleoFile<N> {
41    type Err = anyhow::Error;
42
43    /// Reads the file from a string.
44    #[inline]
45    fn from_str(s: &str) -> Result<Self> {
46        let program = Program::from_str(s)?;
47        let program_string = s.to_string();
48
49        // The file name is defined as the string up to the extension (excluding the extension).
50        let file_name = match program.id().is_aleo() {
51            true => program.id().name().to_string(),
52            false => program.id().to_string(),
53        };
54
55        Ok(Self { file_name, program_string, program })
56    }
57}
58
59impl<N: Network> AleoFile<N> {
60    /// Creates a new Aleo program file with the given directory path, program ID, and `is_main` indicator.
61    pub fn create(directory: &Path, program_id: &ProgramID<N>, is_main: bool) -> Result<Self> {
62        // Ensure the directory path exists.
63        ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display());
64        // Ensure the program name is valid.
65        ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): '{program_id}'");
66
67        // Construct the initial program string.
68        let program_string = format!(
69            r#"// The '{program_id}' program.
70program {program_id};
71
72function hello:
73    input r0 as u32.public;
74    input r1 as u32.private;
75    add r0 r1 into r2;
76    output r2 as u32.private;
77"#
78        );
79
80        // Create the file.
81        let file_name = if is_main {
82            Self::main_file_name()
83        } else {
84            match program_id.is_aleo() {
85                true => program_id.to_string(),
86                false => format!("{program_id}.{ALEO_FILE_EXTENSION}"),
87            }
88        };
89        // Construct the file path.
90        let path = directory.join(file_name);
91        // Ensure the file path does not already exist.
92        ensure!(!path.exists(), "The Aleo file already exists: {}", path.display());
93
94        // Write the file.
95        File::create(&path)?.write_all(program_string.as_bytes())?;
96
97        // Return the Aleo file.
98        Self::from_filepath(&path)
99    }
100
101    /// Opens the Aleo program file, given the directory path, program ID, and `is_main` indicator.
102    pub fn open(directory: &Path, program_id: &ProgramID<N>, is_main: bool) -> Result<Self> {
103        // Ensure the directory path exists.
104        ensure!(directory.exists(), "The program directory does not exist: '{}'", directory.display());
105
106        // Create the file.
107        let file_name = if is_main {
108            Self::main_file_name()
109        } else {
110            match program_id.is_aleo() {
111                true => program_id.to_string(),
112                false => format!("{program_id}.{ALEO_FILE_EXTENSION}"),
113            }
114        };
115        // Construct the file path.
116        let path = directory.join(file_name);
117        // Ensure the file path exists.
118        ensure!(path.exists(), "The Aleo file is missing: '{}'", path.display());
119
120        // Load the Aleo file.
121        let aleo_file = Self::from_filepath(&path)?;
122
123        // Ensure the program ID matches, if this is the main file.
124        if is_main && aleo_file.program.id() != program_id {
125            bail!("The program ID from `{}` does not match in '{}'", Manifest::<N>::file_name(), path.display())
126        }
127
128        Ok(aleo_file)
129    }
130
131    /// Returns `true` if the file exists at the given path.
132    pub fn exists_at(&self, file_path: &Path) -> bool {
133        // Ensure the path is well-formed.
134        Self::check_path(file_path).is_ok() && file_path.exists()
135    }
136
137    /// Returns `true` if the main program file exists at the given path.
138    pub fn main_exists_at(directory: &Path) -> bool {
139        // Construct the file path.
140        let path = directory.join(Self::main_file_name());
141        // Return the result.
142        path.is_file() && path.exists()
143    }
144
145    /// Returns the main Aleo program file name.
146    pub fn main_file_name() -> String {
147        format!("main.{ALEO_FILE_EXTENSION}")
148    }
149
150    /// Returns the file name.
151    pub fn file_name(&self) -> &str {
152        &self.file_name
153    }
154
155    /// Returns the program string.
156    pub fn program_string(&self) -> &str {
157        &self.program_string
158    }
159
160    /// Returns the program.
161    pub const fn program(&self) -> &Program<N> {
162        &self.program
163    }
164
165    /// Writes the program string to the file.
166    pub fn write_to(&self, path: &Path) -> Result<()> {
167        // Ensure the path is well-formed.
168        Self::check_path(path)?;
169
170        // Retrieve the file name.
171        let file_name = path
172            .file_stem()
173            .ok_or_else(|| anyhow!("File name not found."))?
174            .to_str()
175            .ok_or_else(|| anyhow!("File name not found."))?
176            .to_string();
177        // Ensure the file name matches the expected file name.
178        ensure!(file_name == self.file_name, "File name does not match.");
179
180        Ok(File::create(path)?.write_all(self.program_string.as_bytes())?)
181    }
182
183    /// Removes the file at the given path, if it exists.
184    pub fn remove(&self, path: &Path) -> Result<()> {
185        // If the path does not exist, do nothing.
186        if !path.exists() {
187            Ok(())
188        } else {
189            // Ensure the path is well-formed.
190            Self::check_path(path)?;
191            // If the path exists, remove it.
192            if path.exists() {
193                // Remove the file.
194                fs::remove_file(path)?;
195            }
196            Ok(())
197        }
198    }
199}
200
201impl<N: Network> AleoFile<N> {
202    /// Checks that the given path has the correct file extension.
203    fn check_path(path: &Path) -> Result<()> {
204        // Ensure the given path is a file.
205        ensure!(path.is_file(), "The path is not a file.");
206
207        // Ensure the given path has the correct file extension.
208        let extension = path.extension().ok_or_else(|| anyhow!("File extension not found."))?;
209        ensure!(extension == ALEO_FILE_EXTENSION, "File extension is incorrect.");
210
211        Ok(())
212    }
213
214    /// Reads the program from the given file path, if it exists.
215    fn from_filepath(file: &Path) -> Result<Self> {
216        // Ensure the path is well-formed.
217        Self::check_path(file)?;
218
219        // Ensure the given path exists.
220        ensure!(file.exists(), "File does not exist: {}", file.display());
221
222        // Retrieve the file name.
223        let file_name = file
224            .file_stem()
225            .ok_or_else(|| anyhow!("File name not found."))?
226            .to_str()
227            .ok_or_else(|| anyhow!("File name not found."))?
228            .to_string();
229
230        // Read the program string.
231        let program_string = fs::read_to_string(file)?;
232        // Parse the program string.
233        let program = Program::from_str(&program_string)?;
234
235        Ok(Self { file_name, program_string, program })
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::prelude::Parser;
243
244    type CurrentNetwork = snarkvm_console::network::Testnet3;
245
246    fn temp_dir() -> std::path::PathBuf {
247        tempfile::tempdir().expect("Failed to open temporary directory").into_path()
248    }
249
250    #[test]
251    fn test_from_str() {
252        let program_string = r"
253program token.aleo;
254
255record token:
256    owner as address.private;
257    token_amount as u64.private;
258
259function compute:
260    input r0 as token.record;
261    add r0.token_amount r0.token_amount into r1;
262    output r1 as u64.private;";
263
264        // Initialize a new program.
265        let (string, program) = Program::<CurrentNetwork>::parse(program_string).unwrap();
266        assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'");
267
268        // Read the program from the string.
269        let file = AleoFile::from_str(program_string).unwrap();
270        assert_eq!("token", file.file_name());
271        assert_eq!(program_string, file.program_string());
272        assert_eq!(&program, file.program());
273    }
274
275    #[test]
276    fn test_from_path() {
277        // Initialize a temporary directory.
278        let directory = temp_dir();
279
280        let program_string = r"
281program token.aleo;
282
283record token:
284    owner as address.private;
285    token_amount as u64.private;
286
287function compute:
288    input r0 as token.record;
289    add r0.token_amount r0.token_amount into r1;
290    output r1 as u64.private;";
291
292        // Write the program string to a file in the temporary directory.
293        let path = directory.join("token.aleo");
294        let mut file = File::create(&path).unwrap();
295        file.write_all(program_string.as_bytes()).unwrap();
296
297        // Read the program from the path.
298        let file = AleoFile::from_filepath(&path).unwrap();
299
300        // Initialize a new program.
301        let (string, program) = Program::<CurrentNetwork>::parse(program_string).unwrap();
302        assert!(string.is_empty(), "Parser did not consume all of the string: '{string}'");
303
304        assert_eq!("token", file.file_name());
305        assert_eq!(program_string, file.program_string());
306        assert_eq!(&program, file.program());
307    }
308}