Skip to main content

aion_package/
beam.rs

1//! `BeamModule` and `BeamSet` with canonical ordering.
2
3use crate::PackageError;
4
5/// Module names owned by the engine's native NIF layer.
6///
7/// The engine registers these namespaces as native functions at runtime; a
8/// package shipping bytecode under one of these names would silently serve a
9/// fake implementation for any function the engine does not register natively
10/// (the SDK's in-process test double is the canonical offender). `BeamSet::new`
11/// rejects them outright.
12pub const RESERVED_MODULE_NAMES: &[&str] = &["aion_flow_ffi"];
13
14/// A compiled BEAM module preserved exactly as supplied to the package layer.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct BeamModule {
17    /// Logical module name before deployment-time namespacing.
18    pub name: String,
19    /// Exact compiled `.beam` bytes for the logical module.
20    pub bytes: Vec<u8>,
21}
22
23impl BeamModule {
24    /// Creates a beam module value without modifying its name or bytes.
25    #[must_use]
26    pub fn new(name: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
27        Self {
28            name: name.into(),
29            bytes: bytes.into(),
30        }
31    }
32
33    /// Returns the logical module name.
34    #[must_use]
35    pub fn name(&self) -> &str {
36        &self.name
37    }
38
39    /// Returns the exact compiled module bytes.
40    #[must_use]
41    pub fn bytes(&self) -> &[u8] {
42        &self.bytes
43    }
44}
45
46/// A canonical, duplicate-free collection of compiled BEAM modules.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct BeamSet {
49    modules: Vec<BeamModule>,
50}
51
52impl BeamSet {
53    /// Creates a beam set sorted by logical module name.
54    ///
55    /// Duplicate logical module names are rejected because they would make the
56    /// canonical order ambiguous.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`PackageError::MalformedBeamEntry`] when two modules have the
61    /// same logical module name, and [`PackageError::ReservedModuleName`] when
62    /// a module uses an engine-owned namespace from [`RESERVED_MODULE_NAMES`].
63    pub fn new(mut modules: Vec<BeamModule>) -> Result<Self, PackageError> {
64        if let Some(reserved) = modules
65            .iter()
66            .find(|module| RESERVED_MODULE_NAMES.contains(&module.name.as_str()))
67        {
68            return Err(PackageError::ReservedModuleName {
69                module: reserved.name.clone(),
70            });
71        }
72        modules.sort_by(|left, right| left.name.cmp(&right.name));
73
74        if let Some(duplicate) = modules
75            .windows(2)
76            .find(|pair| pair[0].name == pair[1].name)
77            .map(|pair| pair[0].name.clone())
78        {
79            return Err(PackageError::MalformedBeamEntry { entry: duplicate });
80        }
81
82        Ok(Self { modules })
83    }
84
85    /// Returns the number of modules in the set.
86    #[must_use]
87    pub fn len(&self) -> usize {
88        self.modules.len()
89    }
90
91    /// Returns true when the set contains no modules.
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.modules.is_empty()
95    }
96
97    /// Iterates modules in canonical logical-name order.
98    pub fn iter(&self) -> impl Iterator<Item = &BeamModule> {
99        self.modules.iter()
100    }
101
102    /// Looks up exact module bytes by logical module name.
103    #[must_use]
104    pub fn get(&self, name: &str) -> Option<&[u8]> {
105        self.modules
106            .binary_search_by(|module| module.name.as_str().cmp(name))
107            .ok()
108            .map(|index| self.modules[index].bytes.as_slice())
109    }
110}
111
112impl IntoIterator for BeamSet {
113    type IntoIter = std::vec::IntoIter<BeamModule>;
114    type Item = BeamModule;
115
116    fn into_iter(self) -> Self::IntoIter {
117        self.modules.into_iter()
118    }
119}
120
121impl<'a> IntoIterator for &'a BeamSet {
122    type IntoIter = std::slice::Iter<'a, BeamModule>;
123    type Item = &'a BeamModule;
124
125    fn into_iter(self) -> Self::IntoIter {
126        self.modules.iter()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::{BeamModule, BeamSet};
133    use crate::PackageError;
134
135    #[test]
136    fn beam_set_order_is_independent_of_insertion_order() -> Result<(), PackageError> {
137        let first = BeamSet::new(vec![
138            BeamModule::new("workflow/c", vec![3]),
139            BeamModule::new("workflow/a", vec![1]),
140            BeamModule::new("workflow/b", vec![2]),
141        ])?;
142        let second = BeamSet::new(vec![
143            BeamModule::new("workflow/b", vec![2]),
144            BeamModule::new("workflow/c", vec![3]),
145            BeamModule::new("workflow/a", vec![1]),
146        ])?;
147
148        let first_names: Vec<&str> = first.iter().map(BeamModule::name).collect();
149        let second_names: Vec<&str> = second.iter().map(BeamModule::name).collect();
150
151        assert_eq!(first_names, vec!["workflow/a", "workflow/b", "workflow/c"]);
152        assert_eq!(first_names, second_names);
153        assert_eq!(first, second);
154
155        Ok(())
156    }
157
158    #[test]
159    fn beam_set_rejects_duplicate_logical_names() {
160        let result = BeamSet::new(vec![
161            BeamModule::new("workflow/a", vec![1]),
162            BeamModule::new("workflow/a", vec![2]),
163        ]);
164
165        assert!(matches!(
166            result,
167            Err(PackageError::MalformedBeamEntry { entry }) if entry == "workflow/a"
168        ));
169    }
170
171    #[test]
172    fn beam_set_rejects_engine_reserved_namespaces() {
173        let result = BeamSet::new(vec![
174            BeamModule::new("workflow/a", vec![1]),
175            BeamModule::new("aion_flow_ffi", vec![2]),
176        ]);
177
178        assert!(matches!(
179            result,
180            Err(PackageError::ReservedModuleName { module }) if module == "aion_flow_ffi"
181        ));
182    }
183
184    #[test]
185    fn lookup_returns_exact_bytes_by_logical_name() -> Result<(), PackageError> {
186        let bytes = vec![0, 1, 2, 3, 255];
187        let beams = BeamSet::new(vec![BeamModule::new("workflow/a", bytes.clone())])?;
188
189        assert_eq!(beams.get("workflow/a"), Some(bytes.as_slice()));
190        assert_eq!(beams.get("workflow/missing"), None);
191
192        Ok(())
193    }
194}