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