Skip to main content

harmos_packaging/bundle/
mod.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12mod archive;
13mod error;
14mod manifest;
15mod metadata;
16
17/* === Re-Exports === */
18
19pub use error::Error;
20pub use manifest::{Executable, Facility, File, Identity, Limits, Manifest, PackageMetadata};
21
22/* === Imports === */
23
24// Standard Library Imports
25use std::collections::{BTreeMap, BTreeSet};
26use std::io::Write;
27use std::ops::Range;
28use std::path::Path;
29// Third Party Imports
30
31// Local Imports
32use harmos_domain::inventory::Declaration;
33
34/* === Constants === */
35
36/* === Macros === */
37
38/* === Type Aliases === */
39
40/* === Structs === */
41
42pub struct Source<'a> {
43    pub path: &'a str,
44    pub bytes: &'a [u8],
45    pub executable: Option<Executable>,
46}
47
48pub struct Bundle {
49    manifest: Manifest,
50    bytes: Vec<u8>,
51    contents: BTreeMap<String, Range<usize>>,
52    declarations: BTreeMap<String, Declaration>,
53}
54
55/* === Enums === */
56
57/* === Traits === */
58
59/* === Implementations === */
60
61impl Bundle {
62    #[tracing::instrument(level = "debug", skip_all)]
63    pub fn build(identity: Identity, sources: &[Source<'_>], limits: Limits) -> Result<Self, Error> {
64        if sources.len() > limits.files {
65            return Err(Error::Limit("file count"));
66        }
67        let mut files = Vec::with_capacity(sources.len());
68        let mut contents = BTreeMap::new();
69        let mut total = 0usize;
70        let mut metadata_bytes = metadata::identity_size(&identity, limits.manifest_bytes)?;
71        for source in sources {
72            archive::path(source.path)?;
73            total = total.checked_add(source.bytes.len()).ok_or(Error::Limit("total file bytes"))?;
74            if source.bytes.len() > limits.file_bytes || total > limits.total_file_bytes {
75                return Err(Error::Limit("file bytes"));
76            }
77            if contents.insert(source.path.to_owned(), source.bytes).is_some() {
78                return Err(invalid("duplicate file path"));
79            }
80            if source.executable.as_ref().is_some_and(|value| value.registrations.len() > limits.registrations) {
81                return Err(Error::Limit("registration count"));
82            }
83            let sha256 = crate::digest(source.bytes);
84            metadata::file_size(source, &sha256, !files.is_empty(), &mut metadata_bytes, limits.manifest_bytes)?;
85            let mut executable = source.executable.clone();
86            if let Some(executable) = &mut executable {
87                executable.registrations.sort_by(|left, right| left.namespace.cmp(&right.namespace));
88            }
89            files.push(File { path: source.path.to_owned(), size: source.bytes.len() as u64, sha256, executable });
90        }
91        files.sort_by(|left, right| left.path.cmp(&right.path));
92        let manifest = Manifest { schema: 1, identity, files };
93        validate(&manifest, limits)?;
94        let bytes = archive::encode(&manifest, &contents, limits)?;
95        Self::inspect(&bytes, limits)
96    }
97
98    #[tracing::instrument(level = "debug", skip_all)]
99    pub fn inspect(bytes: &[u8], limits: Limits) -> Result<Self, Error> {
100        let (manifest, contents) = archive::decode(bytes, limits)?;
101        let mut declarations = BTreeMap::new();
102        let mut owners = BTreeSet::new();
103        for file in &manifest.files {
104            let content = &bytes[contents[&file.path].clone()];
105            if let Some(executable) = &file.executable {
106                let facility = match executable.facility {
107                    Facility::Sidecar => harmos_domain::inventory::Facility::Sidecar,
108                    Facility::Guest => harmos_domain::inventory::Facility::Guest,
109                };
110                let declaration = Declaration::inspect(content, facility).map_err(|error| invalid(error.to_string()))?;
111                if !owners.insert((executable.facility, declaration.id.clone(), &executable.target)) {
112                    return Err(invalid("duplicate executable facility, owner, and target"));
113                }
114                declarations.insert(file.path.clone(), declaration);
115            }
116        }
117        Ok(Self { manifest, bytes: bytes.to_vec(), contents, declarations })
118    }
119
120    pub fn manifest(&self) -> &Manifest {
121        &self.manifest
122    }
123    pub fn bytes(&self) -> &[u8] {
124        &self.bytes
125    }
126
127    pub fn declaration(&self, path: &str) -> Option<&Declaration> {
128        self.declarations.get(path)
129    }
130
131    pub fn file(&self, path: &str) -> Option<&[u8]> {
132        self.contents.get(path).map(|range| &self.bytes[range.clone()])
133    }
134
135    pub fn extract(&self, parent: &Path) -> Result<tempfile::TempDir, Error> {
136        let directory = archive::staging(parent)?;
137        for file in &self.manifest.files {
138            let path = directory.path().join(&file.path);
139            std::fs::create_dir_all(path.parent().expect("a file has a staging parent"))?;
140            let mut output = std::fs::OpenOptions::new().write(true).create_new(true).open(path)?;
141            output.write_all(self.file(&file.path).expect("verified file exists"))?;
142            archive::permissions(&output, file.executable.as_ref().is_some_and(|value| value.facility == Facility::Sidecar))?;
143        }
144        let path = directory.path().join("manifest.json");
145        let mut output = std::fs::OpenOptions::new().write(true).create_new(true).open(path)?;
146        serde_json::to_writer(&mut output, &self.manifest)?;
147        archive::permissions(&output, false)?;
148        Ok(directory)
149    }
150}
151
152/* === Public Functions === */
153
154/* === Private Functions === */
155
156fn invalid(message: impl Into<String>) -> Error {
157    Error::Invalid(message.into())
158}
159
160fn validate(manifest: &Manifest, limits: Limits) -> Result<(), Error> {
161    if manifest.schema != 1 {
162        return Err(invalid("unsupported schema"));
163    }
164    let name = &manifest.identity.name;
165    if name.is_empty() || !name.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) {
166        return Err(invalid("invalid Cargo package name"));
167    }
168    if manifest.files.len() > limits.files {
169        return Err(Error::Limit("file count"));
170    }
171    let mut paths = BTreeSet::new();
172    let mut previous = None;
173    for file in &manifest.files {
174        archive::path(&file.path)?;
175        if previous.is_some_and(|value: &str| value >= file.path.as_str()) {
176            return Err(invalid("files are not in unique path order"));
177        }
178        previous = Some(&file.path);
179        if !paths.insert(file.path.to_ascii_lowercase()) {
180            return Err(invalid("case-insensitive file collision"));
181        }
182        if let Some(executable) = &file.executable {
183            if executable.target.is_empty()
184                || executable.target.len() > 128
185                || !executable.target.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
186            {
187                return Err(invalid("invalid executable target"));
188            }
189            if executable.registrations.len() > limits.registrations {
190                return Err(Error::Limit("registration count"));
191            }
192            let mut previous = None;
193            for registration in &executable.registrations {
194                if !harmos_domain::artifact_subset(&registration.namespace)
195                    || registration.schema == 0
196                    || previous.is_some_and(|value: &str| value >= registration.namespace.as_str())
197                {
198                    return Err(invalid("invalid, duplicate, or unordered registration"));
199                }
200                previous = Some(&registration.namespace);
201                serde_json::from_slice::<&serde_json::value::RawValue>(&registration.payload)?;
202            }
203        }
204    }
205    for path in &paths {
206        for (offset, _) in path.match_indices('/') {
207            if paths.contains(&path[..offset]) {
208                return Err(invalid("file path is another file's parent"));
209            }
210        }
211    }
212    Ok(())
213}
214
215/* === Tests === */