1use std::{
4 collections::BTreeMap,
5 fs::File,
6 io::{Cursor, Read, Seek},
7 path::Path,
8};
9
10use zip::{ZipArchive, result::ZipError};
11
12use crate::{
13 BeamModule, BeamSet, ContentHash, ExtractionLimits, Manifest, PackageError,
14 builder::is_safe_logical_name,
15 extraction::ExtractionBudget,
16 hash::{has_explicit_timeout_identity, verified_content_hash},
17 namespace::deployed_name,
18 version::WorkflowVersion,
19};
20
21const MANIFEST_ENTRY: &str = "manifest.json";
22const BEAM_PREFIX: &str = "beam/";
23const BEAM_SUFFIX: &str = ".beam";
24const SOURCE_PREFIX: &str = "src/";
25const SOURCE_SUFFIX: &str = ".gleam";
26
27#[derive(Clone, Debug, PartialEq)]
33pub struct Package {
34 manifest: Manifest,
35 beams: BeamSet,
36 source: BTreeMap<String, Vec<u8>>,
37 content_hash: ContentHash,
38}
39
40impl Package {
41 pub fn load_from_path(
52 path: impl AsRef<Path>,
53 limits: ExtractionLimits,
54 ) -> Result<Self, PackageError> {
55 let file =
56 File::open(path).map_err(|source| PackageError::ArchiveRead(ZipError::Io(source)))?;
57 Self::load_from_reader(file, limits)
58 }
59
60 pub fn load_from_bytes(
71 bytes: impl AsRef<[u8]>,
72 limits: ExtractionLimits,
73 ) -> Result<Self, PackageError> {
74 Self::load_from_reader(Cursor::new(bytes.as_ref()), limits)
75 }
76
77 fn load_from_reader<R>(reader: R, limits: ExtractionLimits) -> Result<Self, PackageError>
78 where
79 R: Read + Seek,
80 {
81 let mut archive = ZipArchive::new(reader).map_err(PackageError::ArchiveRead)?;
82 let mut budget = limits.budget();
83 let manifest = read_manifest(&mut archive, &mut budget)?;
84 manifest.check_format_version()?;
85
86 let (beams, source) = read_archive_entries(&mut archive, &mut budget)?;
87 let content_hash = verified_content_hash(&beams, &manifest)?;
88
89 if beams.get(&manifest.entry_module).is_none() {
90 return Err(PackageError::MissingEntryModule {
91 module: manifest.entry_module.clone(),
92 });
93 }
94
95 Ok(Self {
96 manifest,
97 beams,
98 source,
99 content_hash,
100 })
101 }
102
103 #[must_use]
105 pub const fn manifest(&self) -> &Manifest {
106 &self.manifest
107 }
108
109 #[must_use]
111 pub const fn beams(&self) -> &BeamSet {
112 &self.beams
113 }
114
115 #[must_use]
117 pub const fn source(&self) -> &BTreeMap<String, Vec<u8>> {
118 &self.source
119 }
120
121 #[must_use]
123 pub const fn content_hash(&self) -> &ContentHash {
124 &self.content_hash
125 }
126
127 #[must_use]
129 pub fn version_record(&self) -> WorkflowVersion {
130 WorkflowVersion {
131 entry_module: self.manifest.entry_module.clone(),
132 content_hash: self.content_hash.clone(),
133 activities: self.manifest.activities.clone(),
134 input_schema: self.manifest.input_schema.clone(),
135 output_schema: self.manifest.output_schema.clone(),
136 }
137 }
138
139 #[must_use]
144 pub fn deployed_modules(&self) -> Vec<(String, &[u8])> {
145 self.beams
146 .iter()
147 .map(|module| {
148 (
149 deployed_name(module.name(), &self.content_hash),
150 module.bytes(),
151 )
152 })
153 .collect()
154 }
155
156 #[must_use]
158 pub fn deployed_entry_module(&self) -> String {
159 deployed_name(&self.manifest.entry_module, &self.content_hash)
160 }
161
162 pub fn to_archive_bytes(&self) -> Result<Vec<u8>, PackageError> {
177 let mut builder = crate::PackageBuilder::with_source(
178 self.manifest.clone(),
179 self.beams.clone(),
180 self.source.clone(),
181 );
182 if has_explicit_timeout_identity(&self.beams, &self.manifest, &self.content_hash) {
183 builder = builder.with_explicit_timeout_identity();
184 }
185 builder.write_to_bytes()
186 }
187
188 #[cfg(any(test, feature = "test-support"))]
189 #[doc(hidden)]
190 #[must_use]
191 pub fn from_validated_parts_for_test(
192 manifest: Manifest,
193 beams: BeamSet,
194 source: BTreeMap<String, Vec<u8>>,
195 content_hash: ContentHash,
196 ) -> Self {
197 Self {
198 manifest,
199 beams,
200 source,
201 content_hash,
202 }
203 }
204}
205
206fn read_manifest<R>(
207 archive: &mut ZipArchive<R>,
208 budget: &mut ExtractionBudget,
209) -> Result<Manifest, PackageError>
210where
211 R: Read + Seek,
212{
213 let mut manifest_file = match archive.by_name(MANIFEST_ENTRY) {
214 Ok(file) => file,
215 Err(ZipError::FileNotFound) => return Err(PackageError::MissingManifest),
216 Err(error) => return Err(PackageError::ArchiveRead(error)),
217 };
218
219 let manifest_bytes = budget.read_entry(&mut manifest_file)?;
220
221 serde_json::from_slice(&manifest_bytes).map_err(|source| PackageError::ManifestParse { source })
222}
223
224fn read_archive_entries<R>(
225 archive: &mut ZipArchive<R>,
226 budget: &mut ExtractionBudget,
227) -> Result<(BeamSet, BTreeMap<String, Vec<u8>>), PackageError>
228where
229 R: Read + Seek,
230{
231 let mut modules = Vec::new();
232 let mut source = BTreeMap::new();
233
234 for index in 0..archive.len() {
235 let mut file = archive.by_index(index).map_err(PackageError::ArchiveRead)?;
236 if file.is_dir() {
237 continue;
238 }
239
240 let entry = file.name().to_owned();
241 if entry == MANIFEST_ENTRY {
242 continue;
243 }
244
245 if entry.starts_with(BEAM_PREFIX) {
246 let logical = logical_name_from_entry(&entry, BEAM_PREFIX, BEAM_SUFFIX)?;
247 let bytes = budget.read_entry(&mut file)?;
248 modules.push(BeamModule::new(logical, bytes));
249 } else if entry.starts_with(SOURCE_PREFIX) {
250 let logical = logical_name_from_entry(&entry, SOURCE_PREFIX, SOURCE_SUFFIX)?;
251 let bytes = budget.read_entry(&mut file)?;
252 if source.insert(logical, bytes).is_some() {
253 return Err(PackageError::MalformedBeamEntry { entry });
254 }
255 }
256 }
257
258 let beams = BeamSet::new(modules)?;
259 Ok((beams, source))
260}
261
262fn logical_name_from_entry(
263 entry: &str,
264 prefix: &str,
265 suffix: &str,
266) -> Result<String, PackageError> {
267 let Some(without_prefix) = entry.strip_prefix(prefix) else {
268 return Err(PackageError::MalformedBeamEntry {
269 entry: entry.to_owned(),
270 });
271 };
272 let Some(logical) = without_prefix.strip_suffix(suffix) else {
273 return Err(PackageError::MalformedBeamEntry {
274 entry: entry.to_owned(),
275 });
276 };
277
278 if is_safe_logical_name(logical) {
279 Ok(logical.to_owned())
280 } else {
281 Err(PackageError::MalformedBeamEntry {
282 entry: entry.to_owned(),
283 })
284 }
285}
286
287#[cfg(test)]
288#[path = "package_tests.rs"]
289mod tests;