leo_package/lib.rs
1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! This crate deals with Leo packages on the file system and network.
18//!
19//! The main type is `Package`, which deals with Leo packages on the local filesystem.
20//! A Leo package directory is intended to have a structure like this:
21//! .
22//! ├── program.json
23//! ├── build
24//! │ ├── imports
25//! │ │ └── credits.aleo
26//! │ └── main.aleo
27//! ├── outputs
28//! │ ├── program.TypeChecking.ast
29//! │ └── program.TypeChecking.json
30//! ├── src
31//! │ └── main.leo
32//! └── tests
33//! └── test_something.leo
34//!
35//! The file `program.json` is a manifest containing the program name, version, description,
36//! and license, together with information about its dependencies.
37//!
38//! Such a directory structure, together with a `.gitignore` file, may be created
39//! on the file system using `Package::initialize`.
40//! ```no_run
41//! # use leo_ast::NetworkName;
42//! # use leo_package::{Package};
43//! let path = Package::initialize("my_package", "path/to/parent").unwrap();
44//! ```
45//!
46//! `tests` is where unit test files may be placed.
47//!
48//! Given an existing directory with such a structure, a `Package` may be created from it with
49//! `Package::from_directory`:
50//! ```no_run
51//! # use leo_ast::NetworkName;
52//! use leo_package::Package;
53//! let package = Package::from_directory("path/to/package", "/home/me/.aleo", false, false, Some(NetworkName::TestnetV0), Some("http://localhost:3030")).unwrap();
54//! ```
55//! This will read the manifest and env file and keep their data in `package.manifest` and `package.env`.
56//! It will also process dependencies and store them in topological order in `package.programs`. This processing
57//! will involve fetching bytecode from the network for network dependencies.
58//! If the `no_cache` option (3rd parameter) is set to `true`, the package will not use the dependency cache.
59//! The endpoint and network are optional and are only needed if the package has network dependencies.
60//!
61//! If you want to simply read the manifest and env file without processing dependencies, use
62//! `Package::from_directory_no_graph`.
63//!
64//! `Program` generally doesn't need to be created directly, as `Package` will create `Program`s
65//! for the main program and all dependencies. However, if you'd like to fetch bytecode for
66//! a program, you can use `Program::fetch`.
67
68#![forbid(unsafe_code)]
69
70use leo_ast::NetworkName;
71use leo_errors::{PackageError, Result, UtilError};
72use leo_span::Symbol;
73
74use std::path::Path;
75
76mod dependency;
77pub use dependency::*;
78
79mod location;
80pub use location::*;
81
82mod manifest;
83pub use manifest::*;
84
85mod package;
86pub use package::*;
87
88mod program;
89pub use program::*;
90
91pub const SOURCE_DIRECTORY: &str = "src";
92
93pub const MAIN_FILENAME: &str = "main.leo";
94
95pub const IMPORTS_DIRECTORY: &str = "build/imports";
96
97pub const OUTPUTS_DIRECTORY: &str = "outputs";
98
99pub const BUILD_DIRECTORY: &str = "build";
100
101pub const TESTS_DIRECTORY: &str = "tests";
102
103/// Maximum allowed program size in bytes.
104pub const MAX_PROGRAM_SIZE: usize = <snarkvm::prelude::TestnetV0 as snarkvm::prelude::Network>::MAX_PROGRAM_SIZE;
105
106pub const TEST_PRIVATE_KEY: &str = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH";
107
108fn symbol(name: &str) -> Result<Symbol> {
109 name.strip_suffix(".aleo").map(Symbol::intern).ok_or_else(|| PackageError::invalid_network_name(name).into())
110}
111
112/// Is this a valid name for an Aleo program?
113///
114/// Namely, it must be of the format "xxx.aleo" where `xxx` is nonempty,
115/// consist solely of ASCII alphanumeric characters and underscore, and
116/// begin with a letter.
117pub fn is_valid_aleo_name(name: &str) -> bool {
118 let Some(rest) = name.strip_suffix(".aleo") else {
119 return false;
120 };
121
122 // Check that the name is nonempty.
123 if rest.is_empty() {
124 tracing::error!("Aleo names must be nonempty");
125 return false;
126 }
127
128 let first = rest.chars().next().unwrap();
129
130 // Check that the first character is not an underscore.
131 if first == '_' {
132 tracing::error!("Aleo names cannot begin with an underscore");
133 return false;
134 }
135
136 // Check that the first character is not a number.
137 if first.is_numeric() {
138 tracing::error!("Aleo names cannot begin with a number");
139 return false;
140 }
141
142 // Iterate and check that the name is valid.
143 if rest.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
144 tracing::error!("Aleo names must can only contain ASCII alphanumeric characters and underscores.");
145 return false;
146 }
147
148 // Check that the name is not a SnarkVM reserved keyword
149 if reserved_keywords().any(|kw| kw == rest) {
150 tracing::error!(
151 "Aleo names cannot be a SnarkVM reserved keyword. Reserved keywords are: {}.",
152 reserved_keywords().collect::<Vec<_>>().join(", ")
153 );
154 return false;
155 }
156
157 // Check that the name does not contain `aleo`
158 if rest.contains("aleo") {
159 tracing::error!("Aleo names cannot contain the keyword `aleo`.",);
160 return false;
161 }
162
163 true
164}
165
166/// Get the list of all reserved and restricted keywords from snarkVM.
167/// These keywords cannot be used as program names.
168/// See: https://github.com/ProvableHQ/snarkVM/blob/046a2964f75576b2c4afbab9aa9eabc43ceb6dc3/synthesizer/program/src/lib.rs#L192
169pub fn reserved_keywords() -> impl Iterator<Item = &'static str> {
170 use snarkvm::prelude::{Program, TestnetV0};
171
172 // Flatten RESTRICTED_KEYWORDS by ignoring ConsensusVersion
173 let restricted = Program::<TestnetV0>::RESTRICTED_KEYWORDS.iter().flat_map(|(_, kws)| kws.iter().copied());
174
175 Program::<TestnetV0>::KEYWORDS.iter().copied().chain(restricted)
176}
177
178// Fetch the given endpoint url and return the sanitized response.
179pub fn fetch_from_network(url: &str) -> Result<String, UtilError> {
180 fetch_from_network_plain(url).map(|s| s.replace("\\n", "\n").replace('\"', ""))
181}
182
183pub fn fetch_from_network_plain(url: &str) -> Result<String, UtilError> {
184 let mut response = ureq::Agent::config_builder()
185 .max_redirects(0)
186 .build()
187 .new_agent()
188 .get(url)
189 .header("X-Leo-Version", env!("CARGO_PKG_VERSION"))
190 .call()
191 .map_err(|e| UtilError::failed_to_retrieve_from_endpoint(url, e))?;
192 match response.status().as_u16() {
193 200..=299 => Ok(response.body_mut().read_to_string().unwrap()),
194 301 => Err(UtilError::endpoint_moved_error(url)),
195 _ => Err(UtilError::network_error(url, response.status())),
196 }
197}
198
199/// Fetch the given program from the network and return the program as a string.
200// TODO (@d0cd) Unify with `leo_package::Program::fetch`.
201pub fn fetch_program_from_network(name: &str, endpoint: &str, network: NetworkName) -> Result<String, UtilError> {
202 let url = format!("{endpoint}/{network}/program/{name}");
203 let program = fetch_from_network(&url)?;
204 Ok(program)
205}
206
207// Verify that a fetched program is valid aleo instructions.
208pub fn verify_valid_program(name: &str, program: &str) -> Result<(), UtilError> {
209 use snarkvm::prelude::{Program, TestnetV0};
210 use std::str::FromStr as _;
211
212 // Check if the program size exceeds the maximum allowed limit.
213 let program_size = program.len();
214
215 if program_size > MAX_PROGRAM_SIZE {
216 return Err(UtilError::program_size_limit_exceeded(name, program_size, MAX_PROGRAM_SIZE));
217 }
218
219 // Parse the program to verify it's valid Aleo instructions.
220 match Program::<TestnetV0>::from_str(program) {
221 Ok(_) => Ok(()),
222 Err(_) => Err(UtilError::snarkvm_parsing_error(name)),
223 }
224}
225
226pub fn filename_no_leo_extension(path: &Path) -> Option<&str> {
227 filename_no_extension(path, ".leo")
228}
229
230pub fn filename_no_aleo_extension(path: &Path) -> Option<&str> {
231 filename_no_extension(path, ".aleo")
232}
233
234fn filename_no_extension<'a>(path: &'a Path, extension: &'static str) -> Option<&'a str> {
235 path.file_name().and_then(|os_str| os_str.to_str()).and_then(|s| s.strip_suffix(extension))
236}