miden_debug_engine/
linker.rs1use alloc::{boxed::Box, sync::Arc};
2use std::path::{Path, PathBuf};
3
4use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
5use miden_mast_package::{Package, PackageId};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct LinkLibrary {
10 pub name: PackageId,
17 pub path: Option<PathBuf>,
19 pub linkage: Linkage,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Linkage {
25 Dynamic,
26 Static,
27}
28
29impl LinkLibrary {
30 pub fn name(&self) -> &str {
32 self.name.as_ref()
33 }
34
35 pub fn is_core(&self) -> bool {
36 matches!(self.name.as_ref(), "miden-core" | "core" | "std")
37 }
38
39 pub fn is_protocol(&self) -> bool {
40 matches!(self.name.as_ref(), "miden-protocol" | "protocol" | "base")
41 }
42
43 pub fn load(&self, search_paths: &[PathBuf]) -> Result<Arc<Package>, Report> {
44 if let Some(path) = self.path.as_deref() {
45 return self.load_from_path(path);
46 }
47
48 let path = self.find(search_paths)?;
50
51 self.load_from_path(&path)
52 }
53
54 fn load_from_path(&self, path: &Path) -> Result<Arc<Package>, Report> {
55 if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
56 return Err(Report::msg(format!(
57 "link library '{}' is not a compiled .masp package",
58 path.display()
59 )));
60 }
61
62 let bytes = std::fs::read(path).into_diagnostic()?;
63 miden_mast_package::Package::read_from_bytes_trusted(&bytes)
64 .map_err(|e| {
65 Report::msg(format!("failed to load Miden package from {}: {e}", path.display()))
66 })
67 .map(Arc::new)
68 }
69
70 fn find(&self, search_paths: &[PathBuf]) -> Result<PathBuf, Report> {
71 use std::fs;
72
73 for search_path in search_paths {
74 let reader = fs::read_dir(search_path).map_err(|err| {
75 Report::msg(format!(
76 "invalid library search path '{}': {err}",
77 search_path.display()
78 ))
79 })?;
80 for entry in reader {
81 let Ok(entry) = entry else {
82 continue;
83 };
84 let path = entry.path();
85 if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
86 continue;
87 }
88 let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
89 continue;
90 };
91 if stem != self.name() {
92 continue;
93 }
94
95 if !path.is_file() {
96 return Err(Report::msg(format!(
97 "unable to load Miden Assembly package from '{}': not a file",
98 path.display()
99 )));
100 }
101 return Ok(path);
102 }
103 }
104
105 Err(Report::msg(format!(
106 "unable to locate library '{}' using any of the provided search paths",
107 self.name
108 )))
109 }
110}
111
112#[cfg(feature = "std")]
113impl clap::builder::ValueParserFactory for LinkLibrary {
114 type Parser = LinkLibraryParser;
115
116 fn value_parser() -> Self::Parser {
117 LinkLibraryParser
118 }
119}
120
121#[cfg(feature = "std")]
122#[doc(hidden)]
123#[derive(Clone)]
124pub struct LinkLibraryParser;
125
126#[cfg(feature = "std")]
127impl clap::builder::TypedValueParser for LinkLibraryParser {
128 type Value = LinkLibrary;
129
130 fn possible_values(
131 &self,
132 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
133 use clap::builder::PossibleValue;
134
135 Some(Box::new(
136 [PossibleValue::new("masp").help("A compiled Miden package file")].into_iter(),
137 ))
138 }
139
140 fn parse_ref(
148 &self,
149 _cmd: &clap::Command,
150 _arg: Option<&clap::Arg>,
151 value: &std::ffi::OsStr,
152 ) -> Result<Self::Value, clap::error::Error> {
153 use clap::error::{Error, ErrorKind};
154
155 let value = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
156 let (kind, name) = value
157 .split_once('=')
158 .map(|(kind, name)| (Some(kind), name))
159 .unwrap_or((None, value));
160
161 let linkage = match kind {
162 Some(kind) => match kind.split_once(':') {
163 Some(("masp", "static")) => Linkage::Static,
164 Some(("masp", "dynamic")) => Linkage::Dynamic,
165 Some(("masp", other)) => {
166 return Err(Error::raw(
167 ErrorKind::ValueValidation,
168 format!("unrecognized linkage modifier '{other}'"),
169 ));
170 }
171 None if kind == "masp" => Linkage::Dynamic,
172 Some(_) | None => {
173 return Err(Error::raw(
174 ErrorKind::ValueValidation,
175 "invalid link library kind: supported values are 'masp'",
176 ));
177 }
178 },
179 None => Linkage::Dynamic,
180 };
181
182 if name.is_empty() {
183 return Err(Error::raw(
184 ErrorKind::ValueValidation,
185 "invalid link library: must specify a name or path",
186 ));
187 }
188
189 let maybe_path = Path::new(name);
190 let path = match maybe_path.components().count() {
191 _ if maybe_path.extension().is_some() || maybe_path.is_dir() => {
192 maybe_path.canonicalize().map_err(|err| {
194 Error::raw(
195 ErrorKind::ValueValidation,
196 format!("invalid link library '{}': {err}", maybe_path.display()),
197 )
198 })?
199 }
200 1 => {
201 let name = maybe_path.file_name().unwrap().to_str().unwrap();
203 return Ok(LinkLibrary {
204 name: name.into(),
205 path: None,
206 linkage,
207 });
208 }
209 _ => {
210 maybe_path.canonicalize().map_err(|err| {
212 Error::raw(
213 ErrorKind::ValueValidation,
214 format!("invalid link library: '{}': {err}", maybe_path.display()),
215 )
216 })?
217 }
218 };
219
220 if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
221 return Err(Error::raw(
222 ErrorKind::ValueValidation,
223 format!(
224 "invalid link library: expected '{}' to refer to a compiled .masp package",
225 path.display()
226 ),
227 ));
228 }
229
230 let name = path.file_stem().unwrap().to_str().unwrap();
231 Ok(LinkLibrary {
232 name: name.into(),
233 path: Some(path),
234 linkage,
235 })
236 }
237}
238
239#[cfg(all(test, feature = "std"))]
240mod tests {
241 use std::{ffi::OsStr, string::ToString};
242
243 use clap::builder::TypedValueParser;
244
245 use super::*;
246
247 #[test]
248 fn parser_rejects_masm_sources() {
249 let source = tempfile::Builder::new().suffix(".masm").tempfile().unwrap();
250 let error = LinkLibraryParser
251 .parse_ref(&clap::Command::new("test"), None, source.path().as_os_str())
252 .unwrap_err();
253
254 assert!(error.to_string().contains("compiled .masp package"));
255 }
256
257 #[test]
258 fn parser_rejects_project_directories() {
259 let project = tempfile::tempdir().unwrap();
260 let error = LinkLibraryParser
261 .parse_ref(&clap::Command::new("test"), None, project.path().as_os_str())
262 .unwrap_err();
263
264 assert!(error.to_string().contains("compiled .masp package"));
265 }
266
267 #[test]
268 fn parser_rejects_source_kind() {
269 let error = LinkLibraryParser
270 .parse_ref(&clap::Command::new("test"), None, OsStr::new("masm=library"))
271 .unwrap_err();
272
273 assert!(error.to_string().contains("supported values are 'masp'"));
274 }
275
276 #[test]
277 fn parser_accepts_explicit_static_package() {
278 let package = tempfile::Builder::new().suffix(".masp").tempfile().unwrap();
279 let value = format!("masp:static={}", package.path().display());
280 let library = LinkLibraryParser
281 .parse_ref(&clap::Command::new("test"), None, OsStr::new(&value))
282 .unwrap();
283
284 assert_eq!(library.linkage, Linkage::Static);
285 assert_eq!(library.path.unwrap(), package.path().canonicalize().unwrap());
286 }
287}