Skip to main content

miden_mast_package/package/
serialization.rs

1//! The serialization format of `Package` is as follows:
2//!
3//! #### Header
4//! - `MAGIC_PACKAGE`, a 4-byte tag, followed by a NUL-byte, i.e. `b"\0"`
5//! - `VERSION`, a 3-byte semantic version number, 1 byte for each component, i.e. MAJ.MIN.PATCH
6//!
7//! #### Metadata
8//! - `name` (`String`)
9//! - `version` ([`miden_assembly_syntax::Version`] serialized as a `String`)
10//! - `description` (optional, `String`)
11//! - `kind` (`u8`, see [`crate::TargetType`])
12//!
13//! #### Code
14//! - `mast` (see [`miden_assembly_syntax::Library`])
15//!
16//! #### Manifest
17//! - `manifest` (see [`crate::PackageManifest`])
18//!
19//! #### Custom Sections
20//! - `sections` (a vector of zero or more [`crate::Section`])
21
22use alloc::{
23    format,
24    string::{String, ToString},
25    sync::Arc,
26    vec::Vec,
27};
28
29use miden_assembly_syntax::{
30    Library,
31    ast::{AttributeSet, PathBuf},
32};
33use miden_core::{
34    Word,
35    serde::{
36        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
37        SliceReader,
38    },
39};
40
41use super::{ConstantExport, PackageId, ProcedureExport, TargetType, TypeExport};
42use crate::{Dependency, Package, PackageExport, PackageManifest, Section};
43
44// CONSTANTS
45// ================================================================================================
46
47/// Magic string for detecting that a file is serialized [`Package`]
48const MAGIC_PACKAGE: &[u8; 5] = b"MASP\0";
49
50/// The format version.
51///
52/// If future modifications are made to this format, the version should be incremented by 1.
53const VERSION: [u8; 3] = [4, 0, 0];
54
55/// Byte-read budget multiplier for package deserialization from a byte slice.
56///
57/// The budget is intentionally finite to reject malicious length prefixes, but larger than the
58/// source length because collection deserialization uses conservative per-element size estimates.
59const PACKAGE_BYTE_READ_BUDGET_MULTIPLIER: usize = 64;
60
61// PACKAGE SERIALIZATION/DESERIALIZATION
62// ================================================================================================
63
64impl Serializable for Package {
65    fn write_into<W: ByteWriter>(&self, target: &mut W) {
66        // Write magic & version
67        target.write_bytes(MAGIC_PACKAGE);
68        target.write_bytes(&VERSION);
69
70        // Write package name
71        self.name.write_into(target);
72
73        // Write package version
74        self.version.to_string().write_into(target);
75
76        // Write package description
77        self.description.write_into(target);
78
79        // Write package kind
80        target.write_u8(self.kind.into());
81
82        // Write MAST artifact
83        self.mast.write_into(target);
84
85        // Write manifest
86        self.manifest.write_into(target);
87
88        // Write custom sections
89        target.write_usize(self.sections.len());
90        for section in self.sections.iter() {
91            section.write_into(target);
92        }
93    }
94}
95
96impl Deserializable for Package {
97    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
98        // Read and validate magic & version
99        let magic: [u8; 5] = source.read_array()?;
100        if magic != *MAGIC_PACKAGE {
101            return Err(DeserializationError::InvalidValue(format!(
102                "invalid magic bytes. Expected '{MAGIC_PACKAGE:?}', got '{magic:?}'"
103            )));
104        }
105
106        let version: [u8; 3] = source.read_array()?;
107        if version != VERSION {
108            return Err(DeserializationError::InvalidValue(format!(
109                "unsupported version. Got '{version:?}', but only '{VERSION:?}' is supported"
110            )));
111        }
112
113        // Read package name
114        let name = PackageId::read_from(source)?;
115
116        // Read package version
117        let version = String::read_from(source)?
118            .parse::<crate::Version>()
119            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
120
121        // Read package description
122        let description = Option::<String>::read_from(source)?;
123
124        // Read package kind
125        let kind_tag = source.read_u8()?;
126        let kind = TargetType::try_from(kind_tag)
127            .map_err(|e| DeserializationError::InvalidValue(e.to_string()))?;
128
129        // Read MAST artifact
130        let mast = Arc::new(Library::read_from(source)?);
131
132        // Read manifest
133        let manifest = PackageManifest::read_from(source)?;
134
135        // Read custom sections
136        let sections = Vec::<Section>::read_from(source)?;
137
138        Ok(Self {
139            name,
140            version,
141            description,
142            kind,
143            mast,
144            manifest,
145            sections,
146        })
147    }
148
149    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
150        let budget = bytes.len().saturating_mul(PACKAGE_BYTE_READ_BUDGET_MULTIPLIER);
151        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
152        Self::read_from(&mut reader)
153    }
154}
155
156// PACKAGE MANIFEST SERIALIZATION/DESERIALIZATION
157// ================================================================================================
158
159#[cfg(feature = "serde")]
160impl serde::Serialize for PackageManifest {
161    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162    where
163        S: serde::Serializer,
164    {
165        use alloc::collections::BTreeMap;
166
167        use miden_assembly_syntax::Path;
168        use serde::ser::SerializeStruct;
169
170        struct PackageExports<'a>(&'a BTreeMap<Arc<Path>, PackageExport>);
171
172        impl serde::Serialize for PackageExports<'_> {
173            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
174            where
175                S: serde::Serializer,
176            {
177                use serde::ser::SerializeSeq;
178
179                let mut serializer = serializer.serialize_seq(Some(self.0.len()))?;
180                for value in self.0.values() {
181                    serializer.serialize_element(value)?;
182                }
183                serializer.end()
184            }
185        }
186
187        let mut serializer = serializer.serialize_struct("PackageManifest", 2)?;
188        serializer.serialize_field("exports", &PackageExports(&self.exports))?;
189        serializer.serialize_field("dependencies", &self.dependencies)?;
190        serializer.end()
191    }
192}
193
194#[cfg(feature = "serde")]
195impl<'de> serde::Deserialize<'de> for PackageManifest {
196    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197    where
198        D: serde::Deserializer<'de>,
199    {
200        #[derive(serde::Deserialize)]
201        #[serde(field_identifier, rename_all = "lowercase")]
202        enum Field {
203            Exports,
204            Dependencies,
205        }
206
207        struct PackageManifestVisitor;
208
209        impl<'de> serde::de::Visitor<'de> for PackageManifestVisitor {
210            type Value = PackageManifest;
211
212            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
213                formatter.write_str("struct PackageManifest")
214            }
215
216            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
217            where
218                A: serde::de::SeqAccess<'de>,
219            {
220                let exports = seq
221                    .next_element::<Vec<PackageExport>>()?
222                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
223                let dependencies = seq
224                    .next_element::<Vec<Dependency>>()?
225                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
226                PackageManifest::new(exports)
227                    .and_then(|manifest| manifest.with_dependencies(dependencies))
228                    .map_err(serde::de::Error::custom)
229            }
230
231            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
232            where
233                A: serde::de::MapAccess<'de>,
234            {
235                let mut exports = None;
236                let mut dependencies = None;
237                while let Some(key) = map.next_key()? {
238                    match key {
239                        Field::Exports => {
240                            if exports.is_some() {
241                                return Err(serde::de::Error::duplicate_field("exports"));
242                            }
243                            exports = Some(map.next_value::<Vec<PackageExport>>()?);
244                        },
245                        Field::Dependencies => {
246                            if dependencies.is_some() {
247                                return Err(serde::de::Error::duplicate_field("dependencies"));
248                            }
249                            dependencies = Some(map.next_value::<Vec<Dependency>>()?);
250                        },
251                    }
252                }
253                let exports = exports.ok_or_else(|| serde::de::Error::missing_field("exports"))?;
254                let dependencies =
255                    dependencies.ok_or_else(|| serde::de::Error::missing_field("dependencies"))?;
256                PackageManifest::new(exports)
257                    .and_then(|manifest| manifest.with_dependencies(dependencies))
258                    .map_err(serde::de::Error::custom)
259            }
260        }
261
262        deserializer.deserialize_struct(
263            "PackageManifest",
264            &["exports", "dependencies"],
265            PackageManifestVisitor,
266        )
267    }
268}
269
270impl Serializable for PackageManifest {
271    fn write_into<W: ByteWriter>(&self, target: &mut W) {
272        // Write exports
273        target.write_usize(self.num_exports());
274        for export in self.exports() {
275            export.write_into(target);
276        }
277
278        // Write dependencies
279        target.write_usize(self.num_dependencies());
280        for dep in self.dependencies() {
281            dep.write_into(target);
282        }
283    }
284}
285
286impl Deserializable for PackageManifest {
287    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
288        // Read exports
289        let exports_len = source.read_usize()?;
290        let exports = source.read_many_iter(exports_len)?.collect::<Result<Vec<_>, _>>()?;
291
292        // Read dependencies
293        let dependencies = Vec::<Dependency>::read_from(source)?;
294
295        PackageManifest::new(exports)
296            .and_then(|manifest| manifest.with_dependencies(dependencies))
297            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
298    }
299}
300
301// PACKAGE EXPORT SERIALIZATION/DESERIALIZATION
302// ================================================================================================
303impl Serializable for PackageExport {
304    fn write_into<W: ByteWriter>(&self, target: &mut W) {
305        target.write_u8(self.tag());
306        match self {
307            Self::Procedure(export) => export.write_into(target),
308            Self::Constant(export) => export.write_into(target),
309            Self::Type(export) => export.write_into(target),
310        }
311    }
312}
313
314impl Deserializable for PackageExport {
315    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
316        match source.read_u8()? {
317            1 => ProcedureExport::read_from(source).map(Self::Procedure),
318            2 => ConstantExport::read_from(source).map(Self::Constant),
319            3 => TypeExport::read_from(source).map(Self::Type),
320            invalid => Err(DeserializationError::InvalidValue(format!(
321                "unexpected PackageExport tag: '{invalid}'"
322            ))),
323        }
324    }
325}
326
327impl Serializable for ProcedureExport {
328    fn write_into<W: ByteWriter>(&self, target: &mut W) {
329        self.path.write_into(target);
330        self.digest.write_into(target);
331        match self.signature.as_ref() {
332            Some(sig) => {
333                target.write_bool(true);
334                sig.write_into(target);
335            },
336            None => {
337                target.write_bool(false);
338            },
339        }
340        self.attributes.write_into(target);
341    }
342}
343
344impl Deserializable for ProcedureExport {
345    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
346        use miden_assembly_syntax::ast::types::FunctionType;
347        let path = PathBuf::read_from(source)?.into_boxed_path().into();
348        let digest = Word::read_from(source)?;
349        let signature = if source.read_bool()? {
350            Some(FunctionType::read_from(source)?)
351        } else {
352            None
353        };
354        let attributes = AttributeSet::read_from(source)?;
355        Ok(Self { path, digest, signature, attributes })
356    }
357}
358
359impl Serializable for ConstantExport {
360    fn write_into<W: ByteWriter>(&self, target: &mut W) {
361        self.path.write_into(target);
362        self.value.write_into(target);
363    }
364}
365
366impl Deserializable for ConstantExport {
367    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
368        let path = PathBuf::read_from(source)?.into_boxed_path().into();
369        let value = miden_assembly_syntax::ast::ConstantValue::read_from(source)?;
370        Ok(Self { path, value })
371    }
372}
373
374impl Serializable for TypeExport {
375    fn write_into<W: ByteWriter>(&self, target: &mut W) {
376        self.path.write_into(target);
377        self.ty.write_into(target);
378    }
379}
380
381impl Deserializable for TypeExport {
382    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
383        use miden_assembly_syntax::ast::types::Type;
384        let path = PathBuf::read_from(source)?.into_boxed_path().into();
385        let ty = Type::read_from(source)?;
386        Ok(Self { path, ty })
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use alloc::{
393        collections::BTreeMap,
394        string::{String, ToString},
395        sync::Arc,
396        vec,
397        vec::Vec,
398    };
399
400    use miden_assembly_syntax::{
401        Library,
402        ast::{AttributeSet, Path as AstPath, PathBuf},
403        library::{LibraryExport, ProcedureExport as LibraryProcedureExport},
404    };
405    use miden_core::{
406        Word,
407        mast::{BasicBlockNodeBuilder, MastForest, MastForestContributor, MastNodeExt, MastNodeId},
408        operations::Operation,
409        serde::{
410            BudgetedReader, ByteWriter, Deserializable, DeserializationError, Serializable,
411            SliceReader,
412        },
413    };
414    #[cfg(feature = "serde")]
415    use serde_json::{json, to_value};
416
417    use super::{
418        MAGIC_PACKAGE, PACKAGE_BYTE_READ_BUDGET_MULTIPLIER, Package, PackageExport,
419        PackageManifest, Section, VERSION,
420    };
421    use crate::{
422        Dependency, ManifestValidationError, PackageId, SectionId, TargetType,
423        package::manifest::ProcedureExport as PackageProcedureExport,
424    };
425
426    fn build_forest() -> (MastForest, MastNodeId) {
427        let mut forest = MastForest::new();
428        let node_id = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
429            .add_to_forest(&mut forest)
430            .expect("failed to build basic block");
431        forest.make_root(node_id);
432        (forest, node_id)
433    }
434
435    fn absolute_path(name: &str) -> Arc<AstPath> {
436        let path = PathBuf::new(name).expect("invalid path");
437        let path = path.as_path().to_absolute().into_owned();
438        Arc::from(path.into_boxed_path())
439    }
440
441    fn build_library() -> Arc<Library> {
442        let (forest, node_id) = build_forest();
443        let path = absolute_path("test::proc");
444        let export = LibraryProcedureExport::new(node_id, Arc::clone(&path));
445
446        let mut exports = BTreeMap::new();
447        exports.insert(path, LibraryExport::Procedure(export));
448
449        Arc::new(Library::new(Arc::new(forest), exports).expect("failed to build library"))
450    }
451
452    fn build_package() -> Package {
453        let library = build_library();
454        let path = absolute_path("test::proc");
455        let node_id = library.get_export_node_id(path.as_ref());
456        let digest = library.mast_forest()[node_id].digest();
457
458        let export = PackageExport::Procedure(PackageProcedureExport {
459            path: Arc::clone(&path),
460            digest,
461            signature: None,
462            attributes: AttributeSet::default(),
463        });
464
465        let manifest =
466            PackageManifest::new([export]).expect("test package manifest should be valid");
467
468        Package {
469            name: PackageId::from("test_pkg"),
470            version: crate::Version::new(0, 0, 0),
471            description: None,
472            kind: TargetType::Library,
473            mast: library,
474            manifest,
475            sections: Vec::new(),
476        }
477    }
478
479    fn build_dependency() -> Dependency {
480        Dependency {
481            name: PackageId::from("dep"),
482            kind: TargetType::Library,
483            version: crate::Version::new(1, 0, 0),
484            digest: Default::default(),
485        }
486    }
487
488    fn package_bytes_with_sections_count(count: usize) -> Vec<u8> {
489        let package = build_package();
490        let mut bytes = Vec::new();
491
492        bytes.write_bytes(MAGIC_PACKAGE);
493        bytes.write_bytes(&VERSION);
494        package.name.write_into(&mut bytes);
495        package.version.to_string().write_into(&mut bytes);
496        package.description.write_into(&mut bytes);
497        bytes.write_u8(package.kind.into());
498        package.mast.write_into(&mut bytes);
499        package.manifest.write_into(&mut bytes);
500        bytes.write_usize(count);
501
502        bytes
503    }
504
505    #[test]
506    fn package_content_digest_changes_when_identity_fields_change() {
507        let package = build_package();
508        let digest = package.content_digest();
509
510        let renamed = Package {
511            name: PackageId::from("renamed_pkg"),
512            ..package.clone()
513        };
514        assert_ne!(digest, renamed.content_digest());
515
516        let versioned = Package {
517            version: crate::Version::new(1, 2, 3),
518            ..package.clone()
519        };
520        assert_ne!(digest, versioned.content_digest());
521
522        let executable = Package { kind: TargetType::Executable, ..package };
523        assert_ne!(digest, executable.content_digest());
524    }
525
526    #[test]
527    fn package_content_digest_changes_when_manifest_changes() {
528        let package = build_package();
529        let digest = package.content_digest();
530
531        let mut with_dependency = package;
532        with_dependency
533            .manifest
534            .add_dependency(Dependency {
535                name: PackageId::from("dep_pkg"),
536                kind: TargetType::Library,
537                version: crate::Version::new(1, 0, 0),
538                digest: Word::from([1_u32, 2, 3, 4]),
539            })
540            .expect("test dependency should be unique");
541        assert_ne!(digest, with_dependency.content_digest());
542    }
543
544    #[test]
545    fn package_content_digest_changes_when_account_component_metadata_changes() {
546        let package = build_package();
547        let digest = package.content_digest();
548
549        let with_metadata = Package {
550            sections: vec![Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, vec![1, 2, 3, 4])],
551            ..package.clone()
552        };
553        assert_ne!(digest, with_metadata.content_digest());
554
555        let with_different_metadata = Package {
556            sections: vec![Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, vec![4, 3, 2, 1])],
557            ..package
558        };
559        assert_ne!(with_metadata.content_digest(), with_different_metadata.content_digest());
560    }
561
562    #[test]
563    fn package_content_digest_ignores_description_and_opaque_custom_sections_for_now() {
564        let package = build_package();
565        let digest = package.content_digest();
566
567        let described = Package {
568            description: Some(String::from("human-facing package description")),
569            ..package.clone()
570        };
571        assert_eq!(digest, described.content_digest());
572
573        let with_section = Package {
574            sections: vec![Section::new(
575                SectionId::custom("opaque").expect("valid custom section id"),
576                vec![1, 2, 3, 4],
577            )],
578            ..package
579        };
580        assert_eq!(digest, with_section.content_digest());
581    }
582
583    #[test]
584    fn package_manifest_rejects_over_budget_dependencies() {
585        let mut bytes = Vec::new();
586        bytes.write_usize(0);
587        bytes.write_usize(2);
588
589        let mut reader = BudgetedReader::new(SliceReader::new(&bytes), 2);
590        let err = PackageManifest::read_from(&mut reader).unwrap_err();
591        assert!(matches!(err, DeserializationError::InvalidValue(_)));
592    }
593
594    #[test]
595    fn package_rejects_over_budget_sections() {
596        let bytes = package_bytes_with_sections_count(2);
597        let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len());
598        let err = Package::read_from(&mut reader).unwrap_err();
599        assert!(matches!(err, DeserializationError::InvalidValue(_)));
600    }
601
602    #[test]
603    fn package_read_from_bytes_rejects_fuzzed_oom_payload() {
604        // This fuzz payload encodes counts large enough to cause excessive allocation or read work.
605        // If this starts succeeding, package byte-slice deserialization is no longer budgeted.
606        let payload = [
607            0x4d, 0x41, 0x53, 0x50, 0x00, 0x04, 0x00, 0x00, 0x11, 0x74, 0x65, 0x73, 0x74, 0x5f,
608            0x70, 0x6b, 0x67, 0x0b, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x00, 0x00, 0x4d, 0x41, 0x53,
609            0x54, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x17, 0x03, 0x22,
610            0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
611            0x00, 0x00, 0x30, 0x2f, 0x08, 0x0a, 0x21, 0xa9, 0xb6, 0xf6, 0x1a, 0x52, 0x30, 0xc5,
612            0x64, 0xc7, 0xdb, 0x4d, 0x83, 0x0b, 0x32, 0x58, 0x89, 0x88, 0xb2, 0x78, 0x69, 0xbb,
613            0x23, 0xa6, 0x18, 0x9c, 0xc9, 0x35, 0x2d, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
614            0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
615            0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x0c, 0x00, 0x3a, 0x3a, 0x74, 0x65, 0x73,
616            0x74, 0x3a, 0x3a, 0x70, 0x72, 0x6f, 0x63, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03,
617            0x0f, 0x03, 0x0f, 0x01, 0x00, 0x00, 0x17, 0x03, 0x22, 0x01, 0x00, 0x00, 0x00, 0x01,
618            0x00, 0x00, 0x00, 0x00, 0x00, 0x9c, 0xc9, 0x35, 0x2d, 0x01, 0x00, 0x03, 0x0f, 0x03,
619            0x0f, 0x01, 0x01, 0x01,
620        ];
621
622        let result = Package::read_from_bytes(&payload);
623        assert!(result.is_err());
624
625        // Wrapped fuzz inputs must use the generic budgeted entry point; otherwise the outer
626        // collection length can drive unbounded work before the inner package fails.
627        let mut vec_payload = vec![0];
628        vec_payload.extend_from_slice(&1000u64.to_le_bytes());
629        let budget = vec_payload.len().saturating_mul(PACKAGE_BYTE_READ_BUDGET_MULTIPLIER);
630        let result = Vec::<Package>::read_from_bytes_with_budget(&vec_payload, budget);
631        assert!(result.is_err());
632
633        let mut option_payload = vec![1];
634        option_payload.extend_from_slice(&payload);
635        let budget = option_payload.len().saturating_mul(PACKAGE_BYTE_READ_BUDGET_MULTIPLIER);
636        let result = Option::<Package>::read_from_bytes_with_budget(&option_payload, budget);
637        assert!(result.is_err());
638    }
639
640    #[test]
641    fn package_manifest_new_rejects_duplicate_export_paths() {
642        let library = build_library();
643        let path = absolute_path("test::proc");
644        let node_id = library.get_export_node_id(path.as_ref());
645        let digest = library.mast_forest()[node_id].digest();
646        let export = PackageExport::Procedure(PackageProcedureExport {
647            path: path.clone(),
648            digest,
649            signature: None,
650            attributes: AttributeSet::default(),
651        });
652
653        let err = PackageManifest::new([export.clone(), export])
654            .expect_err("duplicate export paths should be rejected by constructors");
655        assert_eq!(err, ManifestValidationError::DuplicateExport(path));
656    }
657
658    #[test]
659    fn package_manifest_add_dependency_rejects_duplicate_dependencies() {
660        let mut manifest =
661            PackageManifest::new([]).expect("empty package manifest should be valid");
662        let dependency = build_dependency();
663
664        manifest
665            .add_dependency(dependency.clone())
666            .expect("first dependency should be accepted");
667        let err = manifest
668            .add_dependency(dependency)
669            .expect_err("duplicate dependencies should be rejected by helpers");
670        assert_eq!(err, ManifestValidationError::DuplicateDependency(PackageId::from("dep")));
671    }
672
673    #[test]
674    fn package_manifest_rejects_duplicate_export_paths() {
675        let library = build_library();
676        let path = absolute_path("test::proc");
677        let node_id = library.get_export_node_id(path.as_ref());
678        let digest = library.mast_forest()[node_id].digest();
679        let export = PackageExport::Procedure(PackageProcedureExport {
680            path,
681            digest,
682            signature: None,
683            attributes: AttributeSet::default(),
684        });
685
686        let mut bytes = Vec::new();
687        bytes.write_usize(2);
688        export.write_into(&mut bytes);
689        export.write_into(&mut bytes);
690        bytes.write_usize(0);
691
692        let mut reader = SliceReader::new(&bytes);
693        let err = PackageManifest::read_from(&mut reader)
694            .expect_err("duplicate export paths should be rejected during deserialization");
695        assert!(matches!(err, DeserializationError::InvalidValue(_)));
696    }
697
698    #[test]
699    fn package_manifest_rejects_duplicate_dependencies() {
700        let dependency = build_dependency();
701
702        let mut bytes = Vec::new();
703        bytes.write_usize(0);
704        bytes.write_usize(2);
705        dependency.write_into(&mut bytes);
706        dependency.write_into(&mut bytes);
707
708        let mut reader = SliceReader::new(&bytes);
709        let err = PackageManifest::read_from(&mut reader)
710            .expect_err("duplicate dependencies should be rejected during deserialization");
711        assert!(matches!(err, DeserializationError::InvalidValue(_)));
712    }
713
714    #[cfg(feature = "serde")]
715    #[test]
716    fn serde_package_manifest_rejects_duplicate_export_paths() {
717        let library = build_library();
718        let path = absolute_path("test::proc");
719        let node_id = library.get_export_node_id(path.as_ref());
720        let digest = library.mast_forest()[node_id].digest();
721        let export = PackageExport::Procedure(PackageProcedureExport {
722            path,
723            digest,
724            signature: None,
725            attributes: AttributeSet::default(),
726        });
727        let export = to_value(&export).expect("export should serialize");
728
729        let manifest = serde_json::to_string(&json!({
730            "exports": [export.clone(), export],
731            "dependencies": [],
732        }))
733        .expect("manifest should serialize to JSON");
734        let err = serde_json::from_str::<PackageManifest>(&manifest)
735            .expect_err("serde deserialization should reject duplicate export paths");
736        let message = err.to_string();
737        assert!(message.contains("duplicate export path"));
738    }
739
740    #[cfg(feature = "serde")]
741    #[test]
742    fn serde_package_manifest_rejects_duplicate_dependencies() {
743        let dependency = to_value(build_dependency()).expect("dependency should serialize");
744
745        let manifest = serde_json::to_string(&json!({
746            "exports": [],
747            "dependencies": [dependency.clone(), dependency],
748        }))
749        .expect("manifest should serialize to JSON");
750        let err = serde_json::from_str::<PackageManifest>(&manifest)
751            .expect_err("serde deserialization should reject duplicate dependencies");
752        let message = err.to_string();
753        assert!(message.contains("duplicate dependency"));
754    }
755}