1use crate::{MAX_PROGRAM_SIZE, *};
18
19use leo_errors::{PackageError, Result, UtilError};
20use leo_span::Symbol;
21
22use snarkvm::prelude::{Program as SvmProgram, TestnetV0};
23
24use indexmap::{IndexMap, IndexSet};
25use std::path::Path;
26
27fn find_cached_edition(cache_directory: &Path, name: &str) -> Option<u16> {
30 let program_cache = cache_directory.join(name);
31 if !program_cache.exists() {
32 return None;
33 }
34
35 std::fs::read_dir(&program_cache)
37 .ok()?
38 .filter_map(|entry| entry.ok())
39 .filter_map(|entry| {
40 let file_name = entry.file_name();
41 let name = file_name.to_str()?;
42 name.parse::<u16>().ok()
43 })
44 .max()
45}
46
47#[derive(Clone, Debug)]
49pub struct Program {
50 pub name: Symbol,
52 pub data: ProgramData,
53 pub edition: Option<u16>,
54 pub dependencies: IndexSet<Dependency>,
55 pub is_local: bool,
56 pub is_test: bool,
57}
58
59impl Program {
60 pub fn from_aleo_path<P: AsRef<Path>>(name: Symbol, path: P, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
63 Self::from_aleo_path_impl(name, path.as_ref(), map)
64 }
65
66 fn from_aleo_path_impl(name: Symbol, path: &Path, map: &IndexMap<Symbol, Dependency>) -> Result<Self> {
67 let bytecode = std::fs::read_to_string(path).map_err(|e| {
68 UtilError::util_file_io_error(format_args!("Trying to read aleo file at {}", path.display()), e)
69 })?;
70
71 let dependencies = parse_dependencies_from_aleo(name, &bytecode, map)?;
72
73 Ok(Program {
74 name,
75 data: ProgramData::Bytecode(bytecode),
76 edition: None,
77 dependencies,
78 is_local: true,
79 is_test: false,
80 })
81 }
82
83 pub fn from_package_path<P: AsRef<Path>>(name: Symbol, path: P) -> Result<Self> {
86 Self::from_package_path_impl(name, path.as_ref())
87 }
88
89 fn from_package_path_impl(name: Symbol, path: &Path) -> Result<Self> {
90 let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
91 let manifest_symbol = crate::symbol(&manifest.program)?;
92 if name != manifest_symbol {
93 return Err(PackageError::conflicting_manifest(
94 format_args!("{name}.aleo"),
95 format_args!("{manifest_symbol}.aleo"),
96 )
97 .into());
98 }
99 let source_directory = path.join(SOURCE_DIRECTORY);
100 source_directory.read_dir().map_err(|e| {
101 UtilError::util_file_io_error(format_args!("Failed to read directory {}", source_directory.display()), e)
102 })?;
103
104 let source_path = source_directory.join(MAIN_FILENAME);
105
106 Ok(Program {
107 name,
108 data: ProgramData::SourcePath { directory: path.to_path_buf(), source: source_path },
109 edition: None,
110 dependencies: manifest
111 .dependencies
112 .unwrap_or_default()
113 .into_iter()
114 .map(|dependency| canonicalize_dependency_path_relative_to(path, dependency))
115 .collect::<Result<IndexSet<_>, _>>()?,
116 is_local: true,
117 is_test: false,
118 })
119 }
120
121 pub fn from_test_path<P: AsRef<Path>>(source_path: P, main_program: Dependency) -> Result<Self> {
128 Self::from_path_test_impl(source_path.as_ref(), main_program)
129 }
130
131 fn from_path_test_impl(source_path: &Path, main_program: Dependency) -> Result<Self> {
132 let name = filename_no_leo_extension(source_path)
133 .ok_or_else(|| PackageError::failed_path(source_path.display(), ""))?;
134 let test_directory = source_path.parent().ok_or_else(|| {
135 UtilError::failed_to_open_file(format_args!("Failed to find directory for test {}", source_path.display()))
136 })?;
137 let package_directory = test_directory.parent().ok_or_else(|| {
138 UtilError::failed_to_open_file(format_args!("Failed to find package for test {}", source_path.display()))
139 })?;
140 let manifest = Manifest::read_from_file(package_directory.join(MANIFEST_FILENAME))?;
141 let mut dependencies = manifest
142 .dev_dependencies
143 .unwrap_or_default()
144 .into_iter()
145 .map(|dependency| canonicalize_dependency_path_relative_to(package_directory, dependency))
146 .collect::<Result<IndexSet<_>, _>>()?;
147 dependencies.insert(main_program);
148
149 Ok(Program {
150 name: Symbol::intern(name),
151 edition: None,
152 data: ProgramData::SourcePath {
153 directory: test_directory.to_path_buf(),
154 source: source_path.to_path_buf(),
155 },
156 dependencies,
157 is_local: true,
158 is_test: true,
159 })
160 }
161
162 pub fn fetch<P: AsRef<Path>>(
165 name: Symbol,
166 edition: Option<u16>,
167 home_path: P,
168 network: NetworkName,
169 endpoint: &str,
170 no_cache: bool,
171 ) -> Result<Self> {
172 Self::fetch_impl(name, edition, home_path.as_ref(), network, endpoint, no_cache)
173 }
174
175 fn fetch_impl(
176 name: Symbol,
177 edition: Option<u16>,
178 home_path: &Path,
179 network: NetworkName,
180 endpoint: &str,
181 no_cache: bool,
182 ) -> Result<Self> {
183 let cache_directory = home_path.join(format!("registry/{network}"));
185
186 let edition = match edition {
189 _ if name == Symbol::intern("credits") => 0,
191 Some(edition) => edition,
192 None if !no_cache => {
193 match find_cached_edition(&cache_directory, &name.to_string()) {
195 Some(cached_edition) => cached_edition,
196 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network)?,
197 }
198 }
199 None => crate::fetch_latest_edition(&name.to_string(), endpoint, network)?,
201 };
202
203 let cache_directory = cache_directory.join(format!("{name}/{edition}"));
205 let full_cache_path = cache_directory.join(format!("{name}.aleo"));
206 if !cache_directory.exists() {
207 std::fs::create_dir_all(&cache_directory).map_err(|err| {
209 UtilError::util_file_io_error(format!("Could not write path {}", cache_directory.display()), err)
210 })?;
211 }
212
213 let existing_bytecode = match full_cache_path.exists() {
215 false => None,
216 true => {
217 let existing_contents = std::fs::read_to_string(&full_cache_path).map_err(|e| {
218 UtilError::util_file_io_error(
219 format_args!("Trying to read cached file at {}", full_cache_path.display()),
220 e,
221 )
222 })?;
223 Some(existing_contents)
224 }
225 };
226
227 let bytecode = match (existing_bytecode, no_cache) {
228 (Some(bytecode), false) => bytecode,
230 (existing, _) => {
232 let primary_url = if name == Symbol::intern("credits") {
234 format!("{endpoint}/{network}/program/credits.aleo")
235 } else {
236 format!("{endpoint}/{network}/program/{name}.aleo/{edition}")
237 };
238 let secondary_url = format!("{endpoint}/{network}/program/{name}.aleo");
239 let contents = fetch_from_network(&primary_url)
240 .or_else(|_| fetch_from_network(&secondary_url))
241 .map_err(|err| {
242 UtilError::failed_to_retrieve_from_endpoint(
243 primary_url,
244 format_args!("Failed to fetch program `{name}` from network `{network}`: {err}"),
245 )
246 })?;
247
248 if let Some(existing_contents) = existing
250 && existing_contents != contents
251 {
252 println!(
253 "Warning: The cached file at `{}` is different from the one fetched from the network. The cached file will be overwritten.",
254 full_cache_path.display()
255 );
256 }
257
258 std::fs::write(&full_cache_path, &contents).map_err(|err| {
260 UtilError::util_file_io_error(
261 format_args!("Could not open file `{}`", full_cache_path.display()),
262 err,
263 )
264 })?;
265
266 contents
267 }
268 };
269
270 let dependencies = parse_dependencies_from_aleo(name, &bytecode, &IndexMap::new())?;
271
272 Ok(Program {
273 name,
274 data: ProgramData::Bytecode(bytecode),
275 edition: Some(edition),
276 dependencies,
277 is_local: false,
278 is_test: false,
279 })
280 }
281}
282
283fn canonicalize_dependency_path_relative_to(base: &Path, mut dependency: Dependency) -> Result<Dependency> {
288 if let Some(path) = &mut dependency.path
289 && !path.is_absolute()
290 {
291 let joined = base.join(&path);
292 *path = joined.canonicalize().map_err(|e| PackageError::failed_path(joined.display(), e))?;
293 }
294 Ok(dependency)
295}
296
297fn parse_dependencies_from_aleo(
299 name: Symbol,
300 bytecode: &str,
301 existing: &IndexMap<Symbol, Dependency>,
302) -> Result<IndexSet<Dependency>> {
303 let program_size = bytecode.len();
305
306 if program_size > MAX_PROGRAM_SIZE {
307 return Err(leo_errors::LeoError::UtilError(UtilError::program_size_limit_exceeded(
308 name,
309 program_size,
310 MAX_PROGRAM_SIZE,
311 )));
312 }
313
314 let svm_program: SvmProgram<TestnetV0> = bytecode.parse().map_err(|_| UtilError::snarkvm_parsing_error(name))?;
316 let dependencies = svm_program
317 .imports()
318 .keys()
319 .map(|program_id| {
320 if let Some(dependency) = existing.get(&Symbol::intern(&program_id.name().to_string())) {
323 dependency.clone()
324 } else {
325 let name = program_id.to_string();
326 Dependency { name, location: Location::Network, path: None, edition: None }
327 }
328 })
329 .collect();
330 Ok(dependencies)
331}