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 {
90                path: source.path.to_owned(),
91                size: source.bytes.len() as u64,
92                sha256,
93                executable,
94            });
95        }
96        files.sort_by(|left, right| left.path.cmp(&right.path));
97        let manifest = Manifest { schema: 1, identity, files };
98        validate(&manifest, limits)?;
99        let bytes = archive::encode(&manifest, &contents, limits)?;
100        Self::inspect(&bytes, limits)
101    }
102
103    #[tracing::instrument(level = "debug", skip_all)]
104    pub fn inspect(bytes: &[u8], limits: Limits) -> Result<Self, Error> {
105        let (manifest, contents) = archive::decode(bytes, limits)?;
106        let mut declarations = BTreeMap::new();
107        let mut owners = BTreeSet::new();
108        for file in &manifest.files {
109            let content = &bytes[contents[&file.path].clone()];
110            if let Some(executable) = &file.executable {
111                let facility = match executable.facility {
112                    Facility::Sidecar => harmos_domain::inventory::Facility::Sidecar,
113                    Facility::Guest => harmos_domain::inventory::Facility::Guest,
114                };
115                let declaration = Declaration::inspect(content, facility).map_err(|error| invalid(error.to_string()))?;
116                if !owners.insert((executable.facility, declaration.id.clone(), &executable.target)) {
117                    return Err(invalid("duplicate executable facility, owner, and target"));
118                }
119                declarations.insert(file.path.clone(), declaration);
120            }
121        }
122        Ok(Self {
123            manifest,
124            bytes: bytes.to_vec(),
125            contents,
126            declarations,
127        })
128    }
129
130    pub fn manifest(&self) -> &Manifest {
131        &self.manifest
132    }
133    pub fn bytes(&self) -> &[u8] {
134        &self.bytes
135    }
136
137    pub fn declaration(&self, path: &str) -> Option<&Declaration> {
138        self.declarations.get(path)
139    }
140
141    pub fn file(&self, path: &str) -> Option<&[u8]> {
142        self.contents.get(path).map(|range| &self.bytes[range.clone()])
143    }
144
145    pub fn extract(&self, parent: &Path) -> Result<tempfile::TempDir, Error> {
146        let directory = archive::staging(parent)?;
147        for file in &self.manifest.files {
148            let path = directory.path().join(&file.path);
149            std::fs::create_dir_all(path.parent().expect("a file has a staging parent"))?;
150            let mut output = std::fs::OpenOptions::new().write(true).create_new(true).open(path)?;
151            output.write_all(self.file(&file.path).expect("verified file exists"))?;
152            archive::permissions(&output, file.executable.as_ref().is_some_and(|value| value.facility == Facility::Sidecar))?;
153        }
154        let path = directory.path().join("manifest.json");
155        let mut output = std::fs::OpenOptions::new().write(true).create_new(true).open(path)?;
156        serde_json::to_writer(&mut output, &self.manifest)?;
157        archive::permissions(&output, false)?;
158        Ok(directory)
159    }
160}
161
162/* === Public Functions === */
163
164/* === Private Functions === */
165
166fn invalid(message: impl Into<String>) -> Error {
167    Error::Invalid(message.into())
168}
169
170fn validate(manifest: &Manifest, limits: Limits) -> Result<(), Error> {
171    if manifest.schema != 1 {
172        return Err(invalid("unsupported schema"));
173    }
174    let name = &manifest.identity.name;
175    if name.is_empty() || !name.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) {
176        return Err(invalid("invalid Cargo package name"));
177    }
178    if manifest.files.len() > limits.files {
179        return Err(Error::Limit("file count"));
180    }
181    let mut paths = BTreeSet::new();
182    let mut previous = None;
183    for file in &manifest.files {
184        archive::path(&file.path)?;
185        if previous.is_some_and(|value: &str| value >= file.path.as_str()) {
186            return Err(invalid("files are not in unique path order"));
187        }
188        previous = Some(&file.path);
189        if !paths.insert(file.path.to_ascii_lowercase()) {
190            return Err(invalid("case-insensitive file collision"));
191        }
192        if let Some(executable) = &file.executable {
193            if executable.target.is_empty()
194                || executable.target.len() > 128
195                || !executable.target.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
196            {
197                return Err(invalid("invalid executable target"));
198            }
199            if executable.registrations.len() > limits.registrations {
200                return Err(Error::Limit("registration count"));
201            }
202            let mut previous = None;
203            for registration in &executable.registrations {
204                if !harmos_domain::artifact_subset(&registration.namespace)
205                    || registration.schema == 0
206                    || previous.is_some_and(|value: &str| value >= registration.namespace.as_str())
207                {
208                    return Err(invalid("invalid, duplicate, or unordered registration"));
209                }
210                previous = Some(&registration.namespace);
211                serde_json::from_slice::<&serde_json::value::RawValue>(&registration.payload)?;
212            }
213        }
214    }
215    for path in &paths {
216        for (offset, _) in path.match_indices('/') {
217            if paths.contains(&path[..offset]) {
218                return Err(invalid("file path is another file's parent"));
219            }
220        }
221    }
222    Ok(())
223}
224
225/* === Tests === */