Skip to main content

miden_assembly_syntax/ast/path/
path_buf.rs

1use alloc::string::{String, ToString};
2use core::{
3    fmt,
4    ops::Deref,
5    str::{self, FromStr},
6};
7
8use miden_core::serde::{
9    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
10};
11
12use super::{Path, PathComponent, PathError};
13use crate::ast::Ident;
14
15// ITEM PATH
16// ================================================================================================
17
18/// Path to an item in a library, i.e. module, procedure, constant or type.
19#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[cfg_attr(
21    all(feature = "arbitrary", test),
22    miden_test_serialization_macros::serialization_test
23)]
24pub struct PathBuf {
25    pub(super) inner: String,
26}
27
28impl Deref for PathBuf {
29    type Target = Path;
30
31    #[inline(always)]
32    fn deref(&self) -> &Self::Target {
33        self.as_ref()
34    }
35}
36
37impl AsRef<Path> for PathBuf {
38    #[inline(always)]
39    fn as_ref(&self) -> &Path {
40        Path::new(&self.inner)
41    }
42}
43
44impl AsRef<str> for PathBuf {
45    #[inline(always)]
46    fn as_ref(&self) -> &str {
47        &self.inner
48    }
49}
50
51impl<'a> From<&'a Path> for PathBuf {
52    #[inline(always)]
53    fn from(path: &'a Path) -> Self {
54        path.to_path_buf()
55    }
56}
57
58/// Constructors
59impl PathBuf {
60    /// Get an empty [PathBuf] with `capacity` bytes allocated for the underlying path storage
61    pub fn with_capacity(capacity: usize) -> Self {
62        Self { inner: String::with_capacity(capacity) }
63    }
64
65    /// Returns a new path created from the provided source.
66    ///
67    /// A path consists of at list of components separated by `::` delimiter. A path must contain
68    /// at least one component.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if:
73    ///
74    /// * The path is empty.
75    /// * Any component of the path is empty.
76    /// * Any component is not a valid identifier (quoted or unquoted) in Miden Assembly syntax,
77    ///   i.e. starts with an ASCII alphabetic character, contains only printable ASCII characters,
78    ///   except for `::`, which must only be used as a path separator.
79    pub fn new<S>(source: &S) -> Result<Self, PathError>
80    where
81        S: AsRef<str> + ?Sized,
82    {
83        let source = source.as_ref();
84
85        let validated = Path::validate(source)?;
86
87        validated.canonicalize()
88    }
89
90    /// Extends this [PathBuf] from a collection of possibly-invalid path components
91    pub(super) fn extend_with_components<'a>(
92        &mut self,
93        components: impl IntoIterator<Item = Result<PathComponent<'a>, PathError>>,
94    ) -> Result<(), PathError> {
95        for component in components {
96            self.push_component(component?.as_str());
97        }
98        Ok(())
99    }
100
101    /// Create an absolute [Path] from a pre-validated string
102    pub fn absolute<S>(source: &S) -> Self
103    where
104        S: AsRef<str> + ?Sized,
105    {
106        let source = source.as_ref();
107        Path::new(source).to_absolute().unwrap().into_owned()
108    }
109
110    /// Create a relative [Path] from a pre-validated string
111    pub fn relative<S>(source: &S) -> Self
112    where
113        S: AsRef<str> + ?Sized,
114    {
115        let source = source.as_ref();
116        match source.strip_prefix("::") {
117            Some(rest) => Self { inner: rest.to_string() },
118            None => Self { inner: source.to_string() },
119        }
120    }
121
122    /// Get a [Path] corresponding to the this [PathBuf]
123    #[inline]
124    pub fn as_path(&self) -> &Path {
125        self.as_ref()
126    }
127
128    /// Convert this mutable [PathBuf] into an owned, read-only [`alloc::boxed::Box<Path>`]
129    pub fn into_boxed_path(self) -> alloc::boxed::Box<Path> {
130        let inner = self.inner.into_boxed_str();
131        let inner = alloc::boxed::Box::into_raw(inner);
132        // SAFETY: This cast is safe because *mut Path is equivalent to *mut str
133        unsafe { alloc::boxed::Box::from_raw(inner as *mut Path) }
134    }
135}
136
137/// Mutation
138impl PathBuf {
139    /// Overrides the parent prefix of this path.
140    ///
141    /// The parent prefix is the part of the path consisting of all components but the last one.
142    ///
143    /// If there is only a single component in `self`, this function is equivalent to appending
144    /// `self` to `parent`.
145    pub fn set_parent<P>(&mut self, parent: &P)
146    where
147        P: AsRef<Path> + ?Sized,
148    {
149        let parent = parent.as_ref();
150        match self.split_last() {
151            Some((last, _)) => {
152                let reparented = parent.join(last);
153                let _ = core::mem::replace(self, reparented);
154            },
155            None => {
156                self.inner.clear();
157                self.inner.push_str(parent.as_str());
158            },
159        }
160    }
161
162    /// Extends `self` with `path`
163    ///
164    /// If `path` is absolute, it replaces the current path.
165    ///
166    /// This function ensures that the joined path correctly delimits each path component, and that
167    /// each component is in canonical form.
168    pub fn push<P>(&mut self, path: &P)
169    where
170        P: AsRef<Path> + ?Sized,
171    {
172        let path = path.as_ref();
173
174        self.extend_with_components(path.components()).expect("invalid path");
175    }
176
177    /// Extends `self` with `component`.
178    ///
179    /// Unlike [`Self::push`], which appends another `Path` to the buffer - this method appends the
180    /// given string as a single path component, ensuring that the content is quoted properly if
181    /// needed.
182    ///
183    /// If `::` is pushed, it is treated as a literal root component (i.e. it makes the path
184    /// absolute). On a non-empty [PathBuf], this has the effect of clearing the path, similar to
185    /// what happens if you call [`PathBuf::push`] with an absolute path.
186    ///
187    /// Pushing components using this method guarantees they are in canonical form.
188    pub fn push_component<S>(&mut self, component: &S)
189    where
190        S: AsRef<str> + ?Sized,
191    {
192        let component = component.as_ref();
193        match component {
194            "" | "\"\"" => (),
195            "::" if self.inner.is_empty() => {
196                self.inner.push_str("::");
197            },
198            "::" => {
199                // Pushing the root component on a non-empty path, resets it to an empty absolute
200                // path
201                self.inner.clear();
202                self.inner.push_str("::");
203            },
204            component => {
205                // Add a delimiter if the path is non-empty
206                if !self.is_empty() {
207                    self.inner.push_str("::");
208                }
209
210                let is_quoted = component.starts_with('"') && component.ends_with('"');
211                let is_special = component == Path::KERNEL_PATH || component == Path::EXEC_PATH;
212                let requires_quoting = !is_special && Ident::requires_quoting(component);
213
214                if is_special && self.inner.is_empty() {
215                    // Special namespaces are always absolute
216                    self.inner.push_str("::");
217                    self.inner.push_str(component);
218                } else if requires_quoting && !is_quoted {
219                    // Quote when necessary
220                    self.inner.push('"');
221                    self.inner.push_str(component);
222                    self.inner.push('"');
223                } else if !requires_quoting && is_quoted {
224                    // Unquote unnecessary quoting
225                    self.inner.push_str(&component[1..(component.len() - 1)]);
226                } else {
227                    // No quoting required, or already quoted
228                    self.inner.push_str(component);
229                }
230            },
231        }
232    }
233
234    /// Truncates `self` to [`Path::parent`].
235    ///
236    /// Returns `false` if `self.parent()` is `None`, otherwise `true`.
237    pub fn pop(&mut self) -> bool {
238        match self.parent() {
239            Some(parent) => {
240                let buf = parent.as_str().to_string();
241                self.inner = buf;
242                true
243            },
244            None => false,
245        }
246    }
247}
248
249impl<'a> core::ops::AddAssign<&'a Path> for PathBuf {
250    fn add_assign(&mut self, rhs: &'a Path) {
251        self.push(rhs);
252    }
253}
254
255impl<'a> core::ops::AddAssign<&'a str> for PathBuf {
256    fn add_assign(&mut self, rhs: &'a str) {
257        self.push_component(rhs);
258    }
259}
260
261impl<'a> core::ops::AddAssign<&'a Ident> for PathBuf {
262    fn add_assign(&mut self, rhs: &'a Ident) {
263        self.push_component(rhs.as_str());
264    }
265}
266
267impl<'a> core::ops::AddAssign<&'a crate::ast::ProcedureName> for PathBuf {
268    fn add_assign(&mut self, rhs: &'a crate::ast::ProcedureName) {
269        self.push_component(rhs.as_str());
270    }
271}
272
273impl<'a> TryFrom<&'a str> for PathBuf {
274    type Error = PathError;
275
276    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
277        PathBuf::new(value)
278    }
279}
280
281fn is_canonical_path(path: &Path) -> bool {
282    let mut is_absolute = false;
283    let mut saw_normal_component = false;
284
285    for component in path.components() {
286        let component = match component {
287            Ok(component) => component,
288            Err(_) => return false,
289        };
290
291        match component {
292            PathComponent::Root => is_absolute = true,
293            component @ PathComponent::Normal(_) => {
294                let is_quoted = component.is_quoted();
295                let component = component.as_str();
296                let is_special = matches!(component, Path::KERNEL_PATH | Path::EXEC_PATH);
297                let requires_quoting = !is_special && Ident::requires_quoting(component);
298
299                if !saw_normal_component && !is_absolute && is_special {
300                    return false;
301                }
302
303                if requires_quoting != is_quoted {
304                    return false;
305                }
306
307                saw_normal_component = true;
308            },
309        }
310    }
311
312    true
313}
314
315impl TryFrom<String> for PathBuf {
316    type Error = PathError;
317
318    fn try_from(value: String) -> Result<Self, Self::Error> {
319        let path = Path::validate(&value)?;
320        if is_canonical_path(path) {
321            Ok(Self { inner: value })
322        } else {
323            path.canonicalize()
324        }
325    }
326}
327
328impl From<Ident> for PathBuf {
329    fn from(component: Ident) -> Self {
330        let mut buf = PathBuf::with_capacity(component.as_str().len());
331        buf.push_component(component.as_str());
332        buf
333    }
334}
335
336impl From<PathBuf> for String {
337    fn from(path: PathBuf) -> Self {
338        path.inner
339    }
340}
341
342impl From<PathBuf> for alloc::sync::Arc<Path> {
343    fn from(value: PathBuf) -> Self {
344        value.into_boxed_path().into()
345    }
346}
347
348impl From<alloc::borrow::Cow<'_, Path>> for PathBuf {
349    fn from(value: alloc::borrow::Cow<'_, Path>) -> Self {
350        value.into_owned()
351    }
352}
353
354impl FromStr for PathBuf {
355    type Err = PathError;
356
357    #[inline]
358    fn from_str(value: &str) -> Result<Self, Self::Err> {
359        Self::new(value)
360    }
361}
362
363impl Serializable for PathBuf {
364    fn write_into<W: ByteWriter>(&self, target: &mut W) {
365        self.as_path().write_into(target);
366    }
367}
368
369impl Serializable for Path {
370    fn write_into<W: ByteWriter>(&self, target: &mut W) {
371        target.write_u16(self.byte_len().try_into().expect("invalid path: too long"));
372        target.write_bytes(self.as_str().as_bytes());
373    }
374}
375
376impl Deserializable for PathBuf {
377    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
378        let len = source.read_u16()? as usize;
379        let path = source.read_slice(len)?;
380        let path =
381            str::from_utf8(path).map_err(|e| DeserializationError::InvalidValue(e.to_string()))?;
382        let path =
383            Path::validate(path).map_err(|e| DeserializationError::InvalidValue(e.to_string()))?;
384        path.canonicalize()
385            .map_err(|e| DeserializationError::InvalidValue(e.to_string()))
386    }
387}
388
389#[cfg(feature = "serde")]
390impl serde::Serialize for PathBuf {
391    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
392    where
393        S: serde::Serializer,
394    {
395        serializer.serialize_str(self.inner.as_str())
396    }
397}
398
399#[cfg(feature = "serde")]
400impl<'de> serde::Deserialize<'de> for PathBuf {
401    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402    where
403        D: serde::Deserializer<'de>,
404    {
405        use serde::de::Visitor;
406
407        struct PathVisitor;
408
409        impl Visitor<'_> for PathVisitor {
410            type Value = PathBuf;
411
412            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
413                formatter.write_str("a valid Path/PathBuf")
414            }
415
416            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
417            where
418                E: serde::de::Error,
419            {
420                Path::validate(value)
421                    .map_err(serde::de::Error::custom)?
422                    .canonicalize()
423                    .map_err(serde::de::Error::custom)
424            }
425
426            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
427            where
428                E: serde::de::Error,
429            {
430                self.visit_str(&value)
431            }
432        }
433
434        deserializer.deserialize_any(PathVisitor)
435    }
436}
437
438impl fmt::Display for PathBuf {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        fmt::Display::fmt(self.as_path(), f)
441    }
442}
443
444// TESTS
445// ================================================================================================
446
447/// Tests
448#[cfg(test)]
449mod tests {
450    use alloc::string::String;
451    use core::assert_matches;
452
453    use super::{PathBuf, PathError};
454    use crate::{Path, PathComponent, ast::IdentError};
455
456    #[test]
457    fn single_component_path() {
458        let path = PathBuf::new("foo").unwrap();
459        assert!(!path.is_absolute());
460        assert_eq!(path.components().count(), 1);
461        assert_eq!(path.last(), Some("foo"));
462        assert_eq!(path.first(), Some("foo"));
463    }
464
465    #[test]
466    fn relative_path_two_components() {
467        let path = PathBuf::new("foo::bar").unwrap();
468        assert!(!path.is_absolute());
469        assert_eq!(path.components().count(), 2);
470        assert_eq!(path.last(), Some("bar"));
471        assert_eq!(path.first(), Some("foo"));
472    }
473
474    #[test]
475    fn relative_path_three_components() {
476        let path = PathBuf::new("foo::bar::baz").unwrap();
477        assert!(!path.is_absolute());
478        assert_eq!(path.components().count(), 3);
479        assert_eq!(path.last(), Some("baz"));
480        assert_eq!(path.first(), Some("foo"));
481        assert_eq!(path.parent().map(Path::as_str), Some("foo::bar"));
482    }
483
484    #[test]
485    fn single_quoted_component() {
486        let path = PathBuf::new("\"miden::base/account@0.1.0\"").unwrap();
487        assert!(!path.is_absolute());
488        assert_eq!(path.components().count(), 1);
489        assert_eq!(path.last(), Some("miden::base/account@0.1.0"));
490        assert_eq!(path.first(), Some("miden::base/account@0.1.0"));
491    }
492
493    #[test]
494    fn trailing_quoted_component() {
495        let path = PathBuf::new("foo::\"miden::base/account@0.1.0\"").unwrap();
496        assert!(!path.is_absolute());
497        assert_eq!(path.components().count(), 2);
498        assert_eq!(path.last(), Some("miden::base/account@0.1.0"));
499        assert_eq!(path.first(), Some("foo"));
500    }
501
502    #[test]
503    fn interspersed_quoted_component() {
504        let path = PathBuf::new("foo::\"miden::base/account@0.1.0\"::item").unwrap();
505        assert!(!path.is_absolute());
506        assert_eq!(path.components().count(), 3);
507        assert_eq!(path.last(), Some("item"));
508        assert_eq!(path.first(), Some("foo"));
509        assert_eq!(path.parent().map(Path::as_str), Some("foo::\"miden::base/account@0.1.0\""));
510    }
511
512    #[test]
513    fn mixed_quoted_components_regression() {
514        let component = "::root_ns:root@1.0.0";
515        let module = "abi_transform_tx_kernel_get_inputs_4";
516        let function = "miden::protocol::active_note::get_inputs";
517        let quoted_function = "\"miden::protocol::active_note::get_inputs\"";
518
519        let p1 = PathBuf::new(&format!("{component}::{module}::\"{function}\"")).unwrap();
520        let mut p2 = PathBuf::new(component).unwrap();
521        p2.push_component(module);
522        p2.push_component(function);
523
524        assert_eq!(p1, p2);
525
526        // Forward iteration of path components
527
528        let mut p1components = p1.components();
529        let mut p2components = p2.components();
530
531        let p1root = p1components.next().unwrap().unwrap();
532        let p2root = p2components.next().unwrap().unwrap();
533
534        assert_eq!(p1root, PathComponent::Root);
535        assert_eq!(p1root, p2root);
536
537        let p1component = p1components.next().unwrap().unwrap();
538        let p2component = p2components.next().unwrap().unwrap();
539
540        assert_eq!(p1component, PathComponent::Normal("\"root_ns:root@1.0.0\""));
541        assert_eq!(p1component, p2component);
542
543        let p1module = p1components.next().unwrap().unwrap();
544        let p2module = p2components.next().unwrap().unwrap();
545
546        assert_eq!(p1module, PathComponent::Normal(module));
547        assert_eq!(p1module, p2module);
548
549        let p1function = p1components.next().unwrap().unwrap();
550        let p2function = p2components.next().unwrap().unwrap();
551
552        assert_eq!(p1function, PathComponent::Normal(quoted_function));
553        assert_eq!(p1function, p2function);
554
555        // Backward iteration of path components
556
557        let mut p1components = p1.components();
558        let mut p2components = p2.components();
559
560        let p1function = p1components.next_back().unwrap().unwrap();
561        let p2function = p2components.next_back().unwrap().unwrap();
562
563        assert_eq!(p1function, PathComponent::Normal(quoted_function));
564        assert_eq!(p1function, p2function);
565
566        let p1module = p1components.next_back().unwrap().unwrap();
567        let p2module = p2components.next_back().unwrap().unwrap();
568
569        assert_eq!(p1module, PathComponent::Normal(module));
570        assert_eq!(p1module, p2module);
571
572        let p1component = p1components.next_back().unwrap().unwrap();
573        let p2component = p2components.next_back().unwrap().unwrap();
574
575        assert_eq!(p1component, PathComponent::Normal("\"root_ns:root@1.0.0\""));
576        assert_eq!(p1component, p2component);
577
578        let p1root = p1components.next_back().unwrap().unwrap();
579        let p2root = p2components.next_back().unwrap().unwrap();
580
581        assert_eq!(p1root, PathComponent::Root);
582        assert_eq!(p1root, p2root);
583
584        assert!(p1.is_absolute());
585        assert_eq!(p1.components().count(), 4);
586        assert_eq!(p1.last(), Some(function));
587        assert_eq!(p1.first(), Some("root_ns:root@1.0.0"));
588        let parent = p1.parent().unwrap();
589        assert_eq!(
590            parent.as_str(),
591            "::\"root_ns:root@1.0.0\"::abi_transform_tx_kernel_get_inputs_4"
592        );
593        assert_eq!(parent.parent().map(Path::as_str), Some("::\"root_ns:root@1.0.0\""));
594
595        assert!(p2.is_absolute());
596        assert_eq!(p2.components().count(), 4);
597        assert_eq!(p2.last(), Some(function));
598        assert_eq!(p2.first(), Some("root_ns:root@1.0.0"));
599        let parent = p2.parent().unwrap();
600        assert_eq!(
601            parent.as_str(),
602            "::\"root_ns:root@1.0.0\"::abi_transform_tx_kernel_get_inputs_4"
603        );
604        assert_eq!(parent.parent().map(Path::as_str), Some("::\"root_ns:root@1.0.0\""));
605    }
606
607    #[test]
608    fn try_from_string_canonicalizes_like_str() {
609        let from_str = PathBuf::try_from("foo::\"bar\"").unwrap();
610        let from_string = PathBuf::try_from(String::from("foo::\"bar\"")).unwrap();
611
612        assert_eq!(from_string, from_str);
613        assert_eq!(from_string.as_str(), "foo::bar");
614    }
615
616    #[test]
617    fn try_from_string_reuses_canonical_allocation() {
618        let value = String::from("foo::bar");
619        let original_ptr = value.as_ptr();
620        let original_capacity = value.capacity();
621
622        let path = PathBuf::try_from(value).unwrap();
623
624        assert_eq!(path.as_str(), "foo::bar");
625        assert_eq!(path.inner.as_ptr(), original_ptr);
626        assert_eq!(path.inner.capacity(), original_capacity);
627    }
628
629    #[test]
630    fn exec_path() {
631        let path = PathBuf::new("$exec::bar::baz").unwrap();
632        assert!(path.is_absolute());
633        assert_eq!(path.components().count(), 4);
634        assert_eq!(path.last(), Some("baz"));
635        assert_eq!(path.first(), Some("$exec"));
636    }
637
638    #[test]
639    fn kernel_path() {
640        let path = PathBuf::new("$kernel::bar::baz").unwrap();
641        assert!(path.is_absolute());
642        assert_eq!(path.components().count(), 4);
643        assert_eq!(path.last(), Some("baz"));
644        assert_eq!(path.first(), Some("$kernel"));
645    }
646
647    #[test]
648    fn invalid_path_empty() {
649        let result = Path::validate("");
650        assert_matches!(result, Err(PathError::Empty));
651    }
652
653    #[test]
654    fn invalid_path_empty_component() {
655        let result = Path::validate("::");
656        assert_matches!(result, Err(PathError::EmptyComponent));
657    }
658
659    #[test]
660    fn invalid_path_trailing_delimiter() {
661        let result = Path::validate("foo::");
662        assert_matches!(result, Err(PathError::InvalidComponent(IdentError::Empty)));
663    }
664
665    #[test]
666    fn invalid_path_invalid_character() {
667        let result = Path::validate("#foo::bar");
668        assert_matches!(result, Err(PathError::InvalidComponent(IdentError::InvalidChars { .. })));
669    }
670
671    #[cfg(feature = "serde")]
672    #[test]
673    fn serde_deserialize_path_buf_canonicalizes_redundant_quotes() {
674        let path: PathBuf = serde_json::from_str(r#""\"foo\"::\"bar\"""#)
675            .expect("path deserialization must succeed");
676        assert_eq!(path.as_str(), "foo::bar");
677    }
678
679    #[cfg(feature = "serde")]
680    #[test]
681    fn serde_deserialize_path_buf_preserves_required_quotes() {
682        let path: PathBuf = serde_json::from_value(serde_json::Value::String(String::from(
683            "::foo::\"miden::base/account@0.1.0\"",
684        )))
685        .expect("path deserialization must succeed");
686        assert_eq!(path.as_str(), "::foo::\"miden::base/account@0.1.0\"");
687    }
688
689    #[cfg(feature = "serde")]
690    #[test]
691    fn serde_deserialize_path_buf_rejects_overflow_after_canonicalization() {
692        let component = format!("{}-", "a".repeat(254));
693        let mut source = String::new();
694        for i in 0..255 {
695            if i > 0 {
696                source.push_str("::");
697            }
698            source.push_str(&component);
699        }
700
701        let err = serde_json::from_value::<PathBuf>(serde_json::Value::String(source))
702            .expect_err("deserialization must fail when canonicalization exceeds u16::MAX bytes");
703        let message = format!("{err}");
704        assert!(message.contains("too long"), "unexpected error: {message}");
705    }
706}