1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use thiserror::Error;
7
8use crate::{BundleRelativePath, FileKind, FileRecord, Manifest, SchemaVersion};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Bundle {
12 manifest: Manifest,
13 content: BTreeMap<BundleRelativePath, Vec<u8>>,
14}
15
16impl Bundle {
17 pub fn from_parts(
23 manifest: Manifest,
24 content: BTreeMap<BundleRelativePath, Vec<u8>>,
25 ) -> Result<Self, BundleError> {
26 let bundle = Self { manifest, content };
27 bundle.verify()?;
28 Ok(bundle)
29 }
30
31 pub fn from_files(
37 mut manifest: Manifest,
38 files: BTreeMap<BundleRelativePath, (FileKind, Vec<u8>)>,
39 ) -> Result<Self, BundleError> {
40 SchemaVersion::new(manifest.schema.major(), manifest.schema.minor())?;
41 let mut content = BTreeMap::new();
42 for (path, (kind, bytes)) in files {
43 manifest.files.insert(
44 path.clone(),
45 FileRecord {
46 kind,
47 mode: 0o600,
48 sha256: digest(&bytes),
49 },
50 );
51 content.insert(path, bytes);
52 }
53 let bundle = Self { manifest, content };
54 bundle.verify()?;
55 Ok(bundle)
56 }
57
58 pub fn from_files_with_modes(
64 mut manifest: Manifest,
65 files: BTreeMap<BundleRelativePath, (FileKind, u32, Vec<u8>)>,
66 ) -> Result<Self, BundleError> {
67 SchemaVersion::new(manifest.schema.major(), manifest.schema.minor())?;
68 let mut content = BTreeMap::new();
69 for (path, (kind, mode, bytes)) in files {
70 manifest.files.insert(
71 path.clone(),
72 FileRecord {
73 kind,
74 mode: mode & 0o777,
75 sha256: digest(&bytes),
76 },
77 );
78 content.insert(path, bytes);
79 }
80 let bundle = Self { manifest, content };
81 bundle.verify()?;
82 Ok(bundle)
83 }
84
85 #[must_use]
86 pub fn manifest(&self) -> &Manifest {
87 &self.manifest
88 }
89
90 #[must_use]
91 pub fn files(&self) -> &BTreeMap<BundleRelativePath, Vec<u8>> {
92 &self.content
93 }
94
95 #[must_use]
96 pub fn content_hash(&self) -> String {
97 let mut canonical = Vec::new();
98 for (path, content) in &self.content {
99 canonical.extend_from_slice(path.to_string().as_bytes());
100 canonical.push(0);
101 canonical.extend_from_slice(content);
102 canonical.push(0xff);
103 }
104 digest(&canonical)
105 }
106
107 pub fn verify(&self) -> Result<(), BundleError> {
113 SchemaVersion::new(self.manifest.schema.major(), self.manifest.schema.minor())?;
114 for (path, record) in &self.manifest.files {
115 let bytes = self
116 .content
117 .get(path)
118 .ok_or_else(|| BundleError::MissingFile(path.clone()))?;
119 if digest(bytes) != record.sha256 {
120 return Err(BundleError::HashMismatch { path: path.clone() });
121 }
122 }
123 if let Some(path) = self
124 .content
125 .keys()
126 .find(|path| !self.manifest.files.contains_key(*path))
127 {
128 return Err(BundleError::UnexpectedFile(path.clone()));
129 }
130 Ok(())
131 }
132
133 pub fn to_yaml_bytes(&self) -> Result<Vec<u8>, BundleError> {
139 self.verify()?;
140 let content = self
141 .content
142 .iter()
143 .map(|(path, bytes)| (path.clone(), hex::encode(bytes)))
144 .collect();
145 let wire = BundleWire {
146 manifest: self.manifest.clone(),
147 content,
148 };
149 Ok(serde_yaml::to_string(&wire)?.into_bytes())
150 }
151
152 pub fn from_yaml_bytes(input: &[u8]) -> Result<Self, BundleError> {
158 let wire: BundleWire = serde_yaml::from_slice(input)?;
159 SchemaVersion::new(wire.manifest.schema.major(), wire.manifest.schema.minor())?;
160 let content = wire
161 .content
162 .into_iter()
163 .map(|(path, encoded)| {
164 hex::decode(encoded)
165 .map(|bytes| (path, bytes))
166 .map_err(BundleError::InvalidHex)
167 })
168 .collect::<Result<BTreeMap<_, _>, _>>()?;
169 let bundle = Self {
170 manifest: wire.manifest,
171 content,
172 };
173 bundle.verify()?;
174 Ok(bundle)
175 }
176}
177
178#[derive(Debug, Serialize, Deserialize)]
179struct BundleWire {
180 manifest: Manifest,
181 content: BTreeMap<BundleRelativePath, String>,
182}
183
184fn digest(bytes: &[u8]) -> String {
185 hex::encode(Sha256::digest(bytes))
186}
187
188#[derive(Debug, Error)]
189pub enum BundleError {
190 #[error("unsupported bundle schema major: {0}")]
191 UnsupportedMajor(u16),
192 #[error("bundle file is missing: {0}")]
193 MissingFile(BundleRelativePath),
194 #[error("bundle contains undeclared file: {0}")]
195 UnexpectedFile(BundleRelativePath),
196 #[error("bundle hash mismatch: {path}")]
197 HashMismatch { path: BundleRelativePath },
198 #[error("bundle content is not valid hexadecimal")]
199 InvalidHex(#[source] hex::FromHexError),
200 #[error("bundle path is invalid")]
201 Path(#[from] crate::BundlePathError),
202 #[error("bundle YAML is invalid")]
203 Yaml(#[from] serde_yaml::Error),
204 #[error("bundle directory already exists: {0}")]
205 OutputExists(PathBuf),
206 #[error("bundle directory contains an unsafe symbolic link: {0}")]
207 DirectorySymlink(PathBuf),
208 #[error("bundle directory contains an undeclared file: {0}")]
209 UnexpectedDiskFile(PathBuf),
210 #[error("bundle secret reference file does not match the manifest")]
211 SecretReferenceMismatch,
212 #[error("bundle file mode does not match the manifest: {0}")]
213 ModeMismatch(PathBuf),
214 #[error("bundle filesystem operation failed at {path}")]
215 Io {
216 path: PathBuf,
217 #[source]
218 source: std::io::Error,
219 },
220}