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]
139 pub fn has_declared_timeout(&self) -> bool {
140 has_explicit_timeout_identity(&self.beams, &self.manifest, &self.content_hash)
141 }
142
143 #[must_use]
149 pub fn declared_timeout(&self) -> Option<std::time::Duration> {
150 self.declared_entry_timeout(self.manifest.timeout)
151 }
152
153 #[must_use]
165 pub fn declared_entry_timeout(
166 &self,
167 entry_timeout: Option<std::time::Duration>,
168 ) -> Option<std::time::Duration> {
169 if self.has_declared_timeout() {
170 entry_timeout
171 } else {
172 None
173 }
174 }
175
176 #[must_use]
178 pub fn version_record(&self) -> WorkflowVersion {
179 WorkflowVersion {
180 entry_module: self.manifest.entry_module.clone(),
181 content_hash: self.content_hash.clone(),
182 activities: self.manifest.activities.clone(),
183 input_schema: self.manifest.input_schema.clone(),
184 output_schema: self.manifest.output_schema.clone(),
185 }
186 }
187
188 #[must_use]
193 pub fn deployed_modules(&self) -> Vec<(String, &[u8])> {
194 self.beams
195 .iter()
196 .map(|module| {
197 (
198 deployed_name(module.name(), &self.content_hash),
199 module.bytes(),
200 )
201 })
202 .collect()
203 }
204
205 #[must_use]
207 pub fn deployed_entry_module(&self) -> String {
208 deployed_name(&self.manifest.entry_module, &self.content_hash)
209 }
210
211 pub fn to_archive_bytes(&self) -> Result<Vec<u8>, PackageError> {
226 let mut builder = crate::PackageBuilder::with_source(
227 self.manifest.clone(),
228 self.beams.clone(),
229 self.source.clone(),
230 );
231 if has_explicit_timeout_identity(&self.beams, &self.manifest, &self.content_hash) {
232 builder = builder.with_explicit_timeout_identity();
233 }
234 builder.write_to_bytes()
235 }
236
237 #[cfg(any(test, feature = "test-support"))]
238 #[doc(hidden)]
239 #[must_use]
240 pub fn from_validated_parts_for_test(
241 manifest: Manifest,
242 beams: BeamSet,
243 source: BTreeMap<String, Vec<u8>>,
244 content_hash: ContentHash,
245 ) -> Self {
246 Self {
247 manifest,
248 beams,
249 source,
250 content_hash,
251 }
252 }
253}
254
255fn read_manifest<R>(
256 archive: &mut ZipArchive<R>,
257 budget: &mut ExtractionBudget,
258) -> Result<Manifest, PackageError>
259where
260 R: Read + Seek,
261{
262 let mut manifest_file = match archive.by_name(MANIFEST_ENTRY) {
263 Ok(file) => file,
264 Err(ZipError::FileNotFound) => return Err(PackageError::MissingManifest),
265 Err(error) => return Err(PackageError::ArchiveRead(error)),
266 };
267
268 let manifest_bytes = budget.read_entry(&mut manifest_file)?;
269
270 serde_json::from_slice(&manifest_bytes).map_err(|source| PackageError::ManifestParse { source })
271}
272
273fn read_archive_entries<R>(
274 archive: &mut ZipArchive<R>,
275 budget: &mut ExtractionBudget,
276) -> Result<(BeamSet, BTreeMap<String, Vec<u8>>), PackageError>
277where
278 R: Read + Seek,
279{
280 let mut modules = Vec::new();
281 let mut source = BTreeMap::new();
282
283 for index in 0..archive.len() {
284 let mut file = archive.by_index(index).map_err(PackageError::ArchiveRead)?;
285 if file.is_dir() {
286 continue;
287 }
288
289 let entry = file.name().to_owned();
290 if entry == MANIFEST_ENTRY {
291 continue;
292 }
293
294 if entry.starts_with(BEAM_PREFIX) {
295 let logical = logical_name_from_entry(&entry, BEAM_PREFIX, BEAM_SUFFIX)?;
296 let bytes = budget.read_entry(&mut file)?;
297 modules.push(BeamModule::new(logical, bytes));
298 } else if entry.starts_with(SOURCE_PREFIX) {
299 let logical = logical_name_from_entry(&entry, SOURCE_PREFIX, SOURCE_SUFFIX)?;
300 let bytes = budget.read_entry(&mut file)?;
301 if source.insert(logical, bytes).is_some() {
302 return Err(PackageError::MalformedBeamEntry { entry });
303 }
304 }
305 }
306
307 let beams = BeamSet::new(modules)?;
308 Ok((beams, source))
309}
310
311fn logical_name_from_entry(
312 entry: &str,
313 prefix: &str,
314 suffix: &str,
315) -> Result<String, PackageError> {
316 let Some(without_prefix) = entry.strip_prefix(prefix) else {
317 return Err(PackageError::MalformedBeamEntry {
318 entry: entry.to_owned(),
319 });
320 };
321 let Some(logical) = without_prefix.strip_suffix(suffix) else {
322 return Err(PackageError::MalformedBeamEntry {
323 entry: entry.to_owned(),
324 });
325 };
326
327 if is_safe_logical_name(logical) {
328 Ok(logical.to_owned())
329 } else {
330 Err(PackageError::MalformedBeamEntry {
331 entry: entry.to_owned(),
332 })
333 }
334}
335
336#[cfg(test)]
337#[path = "package_tests.rs"]
338mod tests;