1use crate::{BundleError, Diagnostic, FORMAT_VERSION, Result};
2use cedar_policy::EntityTypeName;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs;
6use std::path::{Component, Path, PathBuf};
7
8fn default_vec<T>() -> Vec<T> {
9 Vec::new()
10}
11
12#[derive(Debug, Deserialize)]
13#[serde(deny_unknown_fields)]
14struct RawModuleManifest {
15 format_version: u32,
16 name: String,
17 namespace: String,
18 #[serde(default = "default_vec")]
19 imports: Vec<String>,
20 #[serde(default = "default_vec")]
21 policies: Vec<String>,
22 #[serde(default = "default_vec")]
23 schemas: Vec<String>,
24 #[serde(default = "default_vec")]
25 labels: Vec<String>,
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct ModuleManifest {
31 format_version: u32,
32 name: String,
33 namespace: String,
34 imports: Vec<String>,
35 policies: Vec<String>,
36 schemas: Vec<String>,
37 labels: Vec<String>,
38 #[serde(skip)]
39 path: PathBuf,
40 #[serde(skip)]
41 directory: PathBuf,
42}
43
44impl ModuleManifest {
45 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
47 let requested_path = path.as_ref();
48 let bytes =
49 fs::read(requested_path).map_err(|error| BundleError::io(requested_path, error))?;
50 let source = std::str::from_utf8(&bytes).map_err(|error| BundleError::Manifest {
51 path: requested_path.to_path_buf(),
52 message: error.to_string(),
53 })?;
54 let raw: RawModuleManifest =
55 toml::from_str(source).map_err(|error| BundleError::Manifest {
56 path: requested_path.to_path_buf(),
57 message: error.to_string(),
58 })?;
59
60 let path = requested_path
61 .canonicalize()
62 .map_err(|error| BundleError::io(requested_path, error))?;
63 let directory = path.parent().map(Path::to_path_buf).ok_or_else(|| {
64 BundleError::Validation(vec![Diagnostic::error(
65 "manifest.no_parent",
66 "module manifest must have a parent directory",
67 )])
68 })?;
69
70 let mut diagnostics = Vec::new();
71 if raw.format_version != FORMAT_VERSION {
72 diagnostics.push(Diagnostic::error(
73 "manifest.unsupported_version",
74 format!(
75 "module format_version {} is unsupported; expected {FORMAT_VERSION}",
76 raw.format_version
77 ),
78 ));
79 }
80 if raw.name.trim().is_empty() {
81 diagnostics.push(Diagnostic::error(
82 "manifest.empty_name",
83 "module name must not be empty",
84 ));
85 }
86 validate_namespace("module namespace", &raw.namespace, &mut diagnostics);
87 let mut imports = HashSet::new();
88 for import in &raw.imports {
89 validate_namespace("module import", import, &mut diagnostics);
90 if !imports.insert(import) {
91 diagnostics.push(Diagnostic::error(
92 "manifest.duplicate_import",
93 format!("duplicate import {import:?}"),
94 ));
95 }
96 }
97 if raw.imports.iter().any(|import| import == &raw.namespace) {
98 diagnostics.push(Diagnostic::error(
99 "manifest.self_import",
100 "a module cannot import its own namespace",
101 ));
102 }
103
104 for input in raw.policies.iter().chain(&raw.schemas).chain(&raw.labels) {
105 validate_input_path(&directory, input, &mut diagnostics);
106 }
107
108 if diagnostics.is_empty() {
109 Ok(Self {
110 format_version: raw.format_version,
111 name: raw.name,
112 namespace: raw.namespace,
113 imports: raw.imports,
114 policies: raw.policies,
115 schemas: raw.schemas,
116 labels: raw.labels,
117 path,
118 directory,
119 })
120 } else {
121 Err(BundleError::Validation(
122 diagnostics
123 .into_iter()
124 .map(|diagnostic| {
125 diagnostic
126 .in_module(raw.name.clone())
127 .at_path(requested_path.display().to_string())
128 })
129 .collect(),
130 ))
131 }
132 }
133
134 pub fn format_version(&self) -> u32 {
135 self.format_version
136 }
137
138 pub fn name(&self) -> &str {
139 &self.name
140 }
141
142 pub fn namespace(&self) -> &str {
143 &self.namespace
144 }
145
146 pub fn imports(&self) -> &[String] {
147 &self.imports
148 }
149
150 pub fn policies(&self) -> &[String] {
151 &self.policies
152 }
153
154 pub fn schemas(&self) -> &[String] {
155 &self.schemas
156 }
157
158 pub fn labels(&self) -> &[String] {
159 &self.labels
160 }
161
162 pub fn path(&self) -> &Path {
163 &self.path
164 }
165
166 pub(crate) fn input_path(&self, relative: &str) -> PathBuf {
167 self.directory.join(relative)
168 }
169}
170
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(rename_all = "lowercase")]
174pub enum ModuleRole {
175 #[default]
176 Ordinary,
177 Global,
178}
179
180#[derive(Debug, Deserialize)]
181#[serde(deny_unknown_fields)]
182struct RawModuleSelection {
183 manifest: String,
184 #[serde(default)]
185 role: ModuleRole,
186}
187
188#[derive(Debug, Clone, Serialize)]
190pub struct ModuleSelection {
191 manifest: ModuleManifest,
192 role: ModuleRole,
193}
194
195impl ModuleSelection {
196 pub fn manifest(&self) -> &ModuleManifest {
197 &self.manifest
198 }
199
200 pub fn role(&self) -> ModuleRole {
201 self.role
202 }
203}
204
205#[derive(Debug, Deserialize)]
206#[serde(deny_unknown_fields)]
207struct RawBundleManifest {
208 format_version: u32,
209 name: String,
210 modules: Vec<RawModuleSelection>,
211}
212
213#[derive(Debug, Clone, Serialize)]
215pub struct BundleManifest {
216 format_version: u32,
217 name: String,
218 modules: Vec<ModuleSelection>,
219 #[serde(skip)]
220 path: PathBuf,
221}
222
223impl BundleManifest {
224 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
226 let requested_path = path.as_ref();
227 let bytes =
228 fs::read(requested_path).map_err(|error| BundleError::io(requested_path, error))?;
229 let source = std::str::from_utf8(&bytes).map_err(|error| BundleError::Manifest {
230 path: requested_path.to_path_buf(),
231 message: error.to_string(),
232 })?;
233 let raw: RawBundleManifest =
234 toml::from_str(source).map_err(|error| BundleError::Manifest {
235 path: requested_path.to_path_buf(),
236 message: error.to_string(),
237 })?;
238 let path = requested_path
239 .canonicalize()
240 .map_err(|error| BundleError::io(requested_path, error))?;
241 let directory = path.parent().ok_or_else(|| {
242 BundleError::Validation(vec![Diagnostic::error(
243 "manifest.no_parent",
244 "bundle manifest must have a parent directory",
245 )])
246 })?;
247
248 let mut diagnostics = Vec::new();
249 if raw.format_version != FORMAT_VERSION {
250 diagnostics.push(Diagnostic::error(
251 "manifest.unsupported_version",
252 format!(
253 "bundle format_version {} is unsupported; expected {FORMAT_VERSION}",
254 raw.format_version
255 ),
256 ));
257 }
258 if raw.name.trim().is_empty() {
259 diagnostics.push(Diagnostic::error(
260 "manifest.empty_name",
261 "bundle name must not be empty",
262 ));
263 }
264 if raw.modules.is_empty() {
265 diagnostics.push(Diagnostic::error(
266 "manifest.empty_modules",
267 "bundle must select at least one module",
268 ));
269 }
270
271 let mut modules = Vec::with_capacity(raw.modules.len());
272 for selected in raw.modules {
273 let selected_path = Path::new(&selected.manifest);
274 if selected_path.is_absolute() {
275 diagnostics.push(Diagnostic::error(
276 "manifest.absolute_module_path",
277 format!(
278 "module manifest path {:?} must be relative",
279 selected.manifest
280 ),
281 ));
282 continue;
283 }
284 match ModuleManifest::from_path(directory.join(selected_path)) {
285 Ok(manifest) => modules.push(ModuleSelection {
286 manifest,
287 role: selected.role,
288 }),
289 Err(BundleError::Validation(mut nested)) => diagnostics.append(&mut nested),
290 Err(error) => return Err(error),
291 }
292 }
293
294 validate_module_set(&modules, &mut diagnostics);
295 if diagnostics.is_empty() {
296 modules.sort_by(|left, right| left.manifest.name.cmp(&right.manifest.name));
297 Ok(Self {
298 format_version: raw.format_version,
299 name: raw.name,
300 modules,
301 path,
302 })
303 } else {
304 Err(BundleError::Validation(diagnostics))
305 }
306 }
307
308 pub fn format_version(&self) -> u32 {
309 self.format_version
310 }
311
312 pub fn name(&self) -> &str {
313 &self.name
314 }
315
316 pub fn modules(&self) -> &[ModuleSelection] {
317 &self.modules
318 }
319
320 pub fn path(&self) -> &Path {
321 &self.path
322 }
323
324 pub(crate) fn for_single_module(module: ModuleManifest) -> Self {
325 Self {
326 format_version: FORMAT_VERSION,
327 name: module.name.clone(),
328 path: module.path.clone(),
329 modules: vec![ModuleSelection {
330 manifest: module,
331 role: ModuleRole::Ordinary,
332 }],
333 }
334 }
335}
336
337fn validate_namespace(label: &str, value: &str, diagnostics: &mut Vec<Diagnostic>) {
338 if value.trim().is_empty() {
339 diagnostics.push(Diagnostic::error(
340 "manifest.empty_namespace",
341 format!("{label} must not be empty"),
342 ));
343 } else if value.parse::<EntityTypeName>().is_err() {
344 diagnostics.push(Diagnostic::error(
345 "manifest.invalid_namespace",
346 format!("{label} {value:?} is not a valid Cedar name"),
347 ));
348 }
349}
350
351fn validate_input_path(directory: &Path, input: &str, diagnostics: &mut Vec<Diagnostic>) {
352 let input_path = Path::new(input);
353 if input.is_empty()
354 || input_path.is_absolute()
355 || input_path.components().any(|component| {
356 matches!(
357 component,
358 Component::ParentDir | Component::RootDir | Component::Prefix(_)
359 )
360 })
361 || input.contains(['*', '?', '[', ']'])
362 {
363 diagnostics.push(Diagnostic::error(
364 "manifest.invalid_input_path",
365 format!("module input path {input:?} must be an explicit relative path"),
366 ));
367 return;
368 }
369
370 let full_path = directory.join(input_path);
371 match full_path.canonicalize() {
372 Ok(canonical) if canonical.starts_with(directory) && canonical.is_file() => {}
373 Ok(_) => diagnostics.push(Diagnostic::error(
374 "manifest.input_escape",
375 format!("module input path {input:?} escapes the module directory"),
376 )),
377 Err(error) => diagnostics.push(Diagnostic::error(
378 "manifest.input_unreadable",
379 format!("module input path {input:?} cannot be resolved: {error}"),
380 )),
381 }
382}
383
384fn validate_module_set(modules: &[ModuleSelection], diagnostics: &mut Vec<Diagnostic>) {
385 let mut names = HashSet::with_capacity(modules.len());
386 for module in modules {
387 if !names.insert(module.manifest.name.as_str()) {
388 diagnostics.push(Diagnostic::error(
389 "manifest.duplicate_module_name",
390 format!("duplicate module name {:?}", module.manifest.name),
391 ));
392 }
393 }
394
395 let mut ordered_namespaces = modules
396 .iter()
397 .map(|module| module.manifest.namespace.as_str())
398 .collect::<Vec<_>>();
399 ordered_namespaces.sort_unstable();
400 for pair in ordered_namespaces.windows(2) {
401 if namespaces_overlap(pair[0], pair[1]) {
402 diagnostics.push(Diagnostic::error(
403 "manifest.overlapping_namespaces",
404 format!(
405 "module namespace roots {:?} and {:?} overlap",
406 pair[0], pair[1]
407 ),
408 ));
409 }
410 }
411
412 let namespaces = modules
413 .iter()
414 .map(|module| module.manifest.namespace.as_str())
415 .collect::<HashSet<_>>();
416 for module in modules {
417 for import in &module.manifest.imports {
418 if !namespaces.contains(import.as_str()) {
419 diagnostics.push(
420 Diagnostic::error(
421 "manifest.unresolved_import",
422 format!("import {import:?} does not exactly match a selected module"),
423 )
424 .in_module(module.manifest.name.clone()),
425 );
426 }
427 }
428 }
429}
430
431pub(crate) fn namespace_owns(root: &str, name: &str) -> bool {
432 name == root
433 || name
434 .strip_prefix(root)
435 .is_some_and(|suffix| suffix.starts_with("::"))
436}
437
438fn namespaces_overlap(left: &str, right: &str) -> bool {
439 namespace_owns(left, right) || namespace_owns(right, left)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn namespace_prefixes_are_segment_aware() {
448 assert!(namespaces_overlap("A::B", "A::B::C"));
449 assert!(!namespaces_overlap("A::B", "A::Bee"));
450 }
451}