Skip to main content

miden_mast_package/package/
section.rs

1#[cfg(feature = "arbitrary")]
2use alloc::vec;
3use alloc::{
4    borrow::{Cow, ToOwned},
5    format,
6    string::{String, ToString},
7};
8use core::{fmt, str::FromStr};
9
10use miden_assembly_syntax::DisplayHex;
11use miden_core::serde::{
12    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
13};
14#[cfg(feature = "arbitrary")]
15use proptest::prelude::*;
16
17/// A unique identifier for optional sections of the Miden package format
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[repr(transparent)]
20#[cfg_attr(
21    all(feature = "arbitrary", test),
22    miden_test_serialization_macros::serialization_test
23)]
24pub struct SectionId(Cow<'static, str>);
25
26impl SectionId {
27    /// The section containing a serialized [`crate::debug_info::PackageDebugInfo`] struct
28    pub const DEBUG_INFO: Self = Self(Cow::Borrowed("debug_info"));
29    /// This section provides the encoded metadata for a compiled account component
30    ///
31    /// Currently, this corresponds to the serialized representation of
32    /// `miden-protocol::account::AccountComponentMetadata`, i.e. name, descrioption, storage, that
33    /// is associated with this package.
34    pub const ACCOUNT_COMPONENT_METADATA: Self = Self(Cow::Borrowed("account_component_metadata"));
35    /// This section contains provenance metadata for packages assembled from project sources.
36    pub const PROJECT_SOURCE_PROVENANCE: Self = Self(Cow::Borrowed("project_source_provenance"));
37    /// This section contains the serialized kernel package linked against by an executable package.
38    pub const KERNEL: Self = Self(Cow::Borrowed("kernel"));
39
40    /// Construct a user-defined (i.e. "custom") section identifier
41    ///
42    /// Section identifiers must be either an ASCII alphanumeric, or one of the following
43    /// characters: `.`, `_`, `-`. Additionally, the identifier must start with an ASCII alphabetic
44    /// character or `_`.
45    pub fn custom(name: impl AsRef<str>) -> Result<Self, InvalidSectionIdError> {
46        let name = name.as_ref();
47        if !name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
48            return Err(InvalidSectionIdError::InvalidStart);
49        }
50        if name.contains(|c: char| !c.is_ascii_alphanumeric() && !matches!(c, '.' | '_' | '-')) {
51            return Err(InvalidSectionIdError::InvalidCharacter);
52        }
53        Ok(Self(name.to_string().into()))
54    }
55
56    /// Get this section identifier as a string
57    #[inline]
58    pub fn as_str(&self) -> &str {
59        self.0.as_ref()
60    }
61
62    /// Returns true if this section contains package debug metadata.
63    pub fn is_debug(&self) -> bool {
64        self == &Self::DEBUG_INFO
65    }
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum InvalidSectionIdError {
70    #[error("invalid section id: cannot be empty")]
71    Empty,
72    #[error(
73        "invalid section id: contains invalid characters, only the set [a-z0-9._-] are allowed"
74    )]
75    InvalidCharacter,
76    #[error("invalid section id: must start with a character in the set [a-z_]")]
77    InvalidStart,
78}
79
80impl FromStr for SectionId {
81    type Err = InvalidSectionIdError;
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        match s {
84            "debug_info" => Ok(Self::DEBUG_INFO),
85            "account_component_metadata" => Ok(Self::ACCOUNT_COMPONENT_METADATA),
86            "project_source_provenance" => Ok(Self::PROJECT_SOURCE_PROVENANCE),
87            "kernel" => Ok(Self::KERNEL),
88            custom => Self::custom(custom),
89        }
90    }
91}
92
93impl fmt::Display for SectionId {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.write_str(self.as_str())
96    }
97}
98
99impl Serializable for SectionId {
100    fn write_into<W: ByteWriter>(&self, target: &mut W) {
101        self.as_str().write_into(target);
102    }
103}
104
105impl Deserializable for SectionId {
106    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
107        String::read_from(source)?
108            .parse()
109            .map_err(|err| DeserializationError::InvalidValue(format!("invalid section id: {err}")))
110    }
111}
112
113#[derive(Clone, PartialEq, Eq)]
114#[cfg_attr(
115    all(feature = "arbitrary", test),
116    miden_test_serialization_macros::serialization_test
117)]
118pub struct Section {
119    pub id: SectionId,
120    pub data: Cow<'static, [u8]>,
121}
122
123impl fmt::Debug for Section {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        let verbose = f.alternate();
126        let mut builder = f.debug_struct("Section");
127        builder.field("id", &format_args!("{}", self.id));
128        if verbose {
129            builder.field("data", &format_args!("{}", DisplayHex(&self.data))).finish()
130        } else {
131            builder.field("data", &format_args!("{} bytes", self.data.len())).finish()
132        }
133    }
134}
135
136impl Section {
137    pub fn new<B>(id: SectionId, data: B) -> Self
138    where
139        B: Into<Cow<'static, [u8]>>,
140    {
141        Self { id, data: data.into() }
142    }
143
144    /// Returns true if this section is empty, i.e. has no data
145    pub fn is_empty(&self) -> bool {
146        self.data.is_empty()
147    }
148
149    /// Returns the size in bytes of this section's data
150    pub fn len(&self) -> usize {
151        self.data.len()
152    }
153}
154
155#[cfg(all(test, feature = "arbitrary"))]
156mod section_arbitrary {
157    use proptest::prelude::*;
158
159    use super::Section;
160    use crate::package::section::SectionId;
161
162    impl Arbitrary for Section {
163        type Parameters = ();
164        type Strategy = BoxedStrategy<Self>;
165
166        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
167            // The id round-trips through its string form (its own roundtrip test covers that);
168            // the data payload is any-valid bytes.
169            (any::<SectionId>(), proptest::collection::vec(any::<u8>(), 0..=16))
170                .prop_map(|(id, data)| Self::new(id, data))
171                .boxed()
172        }
173    }
174}
175
176impl Serializable for Section {
177    fn write_into<W: ByteWriter>(&self, target: &mut W) {
178        let id = self.id.as_str();
179        target.write_usize(id.len());
180        target.write_bytes(id.as_bytes());
181        target.write_usize(self.len());
182        target.write_bytes(&self.data);
183    }
184}
185
186impl Deserializable for Section {
187    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
188        let id_len = source.read_usize()?;
189        let id_bytes = source.read_slice(id_len)?;
190        let id_str = core::str::from_utf8(id_bytes).map_err(|err| {
191            DeserializationError::InvalidValue(format!("invalid utf-8 in section name: {err}"))
192        })?;
193        let id = id_str.parse::<SectionId>().map_err(|err| {
194            DeserializationError::InvalidValue(format!("invalid section id {id_str:?}: {err}"))
195        })?;
196
197        let len = source.read_usize()?;
198        let bytes = source.read_slice(len)?;
199        Ok(Section { id, data: Cow::Owned(bytes.to_owned()) })
200    }
201}
202
203#[cfg(feature = "arbitrary")]
204impl Arbitrary for SectionId {
205    type Parameters = ();
206    type Strategy = BoxedStrategy<Self>;
207
208    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
209        use alloc::string::String;
210
211        let builtins = proptest::sample::select(vec![
212            Self::DEBUG_INFO,
213            Self::ACCOUNT_COMPONENT_METADATA,
214            Self::PROJECT_SOURCE_PROVENANCE,
215            Self::KERNEL,
216        ]);
217
218        let custom = (
219            proptest::prop_oneof![
220                proptest::char::range('a', 'z'),
221                proptest::char::range('A', 'Z'),
222                Just('_'),
223            ],
224            proptest::collection::vec(
225                proptest::prop_oneof![
226                    proptest::char::range('a', 'z'),
227                    proptest::char::range('A', 'Z'),
228                    proptest::char::range('0', '9'),
229                    Just('.'),
230                    Just('_'),
231                    Just('-'),
232                ],
233                0..31,
234            ),
235        )
236            .prop_map(|(first, rest)| {
237                let mut name = String::with_capacity(rest.len() + 1);
238                name.push(first);
239                name.extend(rest);
240                Self::custom(name).expect("generated custom section ids are valid")
241            });
242
243        proptest::prop_oneof![builtins, custom].boxed()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use alloc::vec::Vec;
250
251    use miden_core::serde::{ByteWriter, Deserializable, Serializable, SliceReader};
252
253    use super::*;
254
255    fn section_bytes_with_id(id: &str) -> Vec<u8> {
256        let mut buf = Vec::new();
257        buf.write_usize(id.len());
258        buf.write_bytes(id.as_bytes());
259        buf.write_usize(0);
260        buf
261    }
262
263    #[test]
264    fn deserialize_rejects_invalid_section_id() {
265        for bad_id in ["", "1bad", "-bad", "bad id", "../etc"] {
266            let bytes = section_bytes_with_id(bad_id);
267            let mut reader = SliceReader::new(&bytes);
268            assert!(Section::read_from(&mut reader).is_err(), "expected error for {bad_id:?}",);
269        }
270    }
271
272    #[test]
273    fn deserialize_accepts_valid_section_id() {
274        let section = Section {
275            id: SectionId::custom("my_section").unwrap(),
276            data: Cow::Borrowed(&[]),
277        };
278        let bytes = section.to_bytes();
279        let mut reader = SliceReader::new(&bytes);
280        let result = Section::read_from(&mut reader);
281        assert!(result.is_ok());
282        assert_eq!(result.unwrap().id, section.id);
283    }
284}