harmos_packaging/bundle/
mod.rs1mod archive;
13mod error;
14mod manifest;
15mod metadata;
16
17pub use error::Error;
20pub use manifest::{Executable, Facility, File, Identity, Limits, Manifest, PackageMetadata};
21
22use std::collections::{BTreeMap, BTreeSet};
26use std::io::Write;
27use std::ops::Range;
28use std::path::Path;
29use harmos_domain::inventory::Declaration;
33
34pub 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
55impl 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
162fn 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(®istration.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(®istration.namespace);
211 serde_json::from_slice::<&serde_json::value::RawValue>(®istration.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