cdx_core/document/
verification.rs1use crate::content::{Block, Text};
2use crate::{DocumentId, Hasher, Result};
3
4use super::Document;
5
6impl Document {
7 pub fn compute_id(&self) -> Result<DocumentId> {
23 let content_value = serde_json::to_value(&self.content)?;
27 let metadata_value = serde_json::json!({
28 "title": self.dublin_core.terms.title,
29 "creator": serde_json::to_value(&self.dublin_core.terms.creator)?,
30 "subject": serde_json::to_value(&self.dublin_core.terms.subject)?,
31 "description": self.dublin_core.terms.description,
32 "language": self.dublin_core.terms.language,
33 });
34
35 let hashable = serde_json::json!({
36 "content": content_value,
37 "metadata": metadata_value,
38 });
39
40 let canonical = json_canon::to_string(&hashable)?;
41
42 Ok(Hasher::hash(
43 self.manifest.hash_algorithm,
44 canonical.as_bytes(),
45 ))
46 }
47
48 pub fn verify(&self) -> Result<VerificationReport> {
58 let mut report = VerificationReport {
59 content_valid: true,
60 id_valid: true,
61 errors: Vec::new(),
62 };
63
64 if !self.manifest.content.hash.is_pending() {
67 let content_json = serde_json::to_vec_pretty(&self.content)?;
68 let actual_hash = Hasher::hash(self.manifest.content.hash.algorithm(), &content_json);
69
70 if actual_hash != self.manifest.content.hash {
71 report.content_valid = false;
72 report.errors.push(format!(
73 "Content hash mismatch: expected {}, got {}",
74 self.manifest.content.hash, actual_hash
75 ));
76 }
77 }
78
79 if !self.manifest.id.is_pending() {
81 let computed_id = self.compute_id()?;
82 if computed_id != self.manifest.id {
83 report.id_valid = false;
84 report.errors.push(format!(
85 "Document ID mismatch: expected {}, got {}",
86 self.manifest.id, computed_id
87 ));
88 }
89 }
90
91 Ok(report)
92 }
93
94 #[must_use]
107 pub fn validate_extensions(&self) -> ExtensionValidationReport {
108 let declared_namespaces: Vec<String> = self
110 .manifest
111 .extensions
112 .iter()
113 .map(|e| e.namespace().to_string())
114 .collect();
115
116 let mut used = std::collections::HashSet::new();
118 Self::collect_extension_namespaces(&self.content.blocks, &mut used);
119
120 let mut used_namespaces: Vec<String> = used.iter().cloned().collect();
121 used_namespaces.sort();
122
123 let mut undeclared = Vec::new();
125 let mut warnings = Vec::new();
126 for namespace in &used_namespaces {
127 if !self.manifest.has_extension(namespace) {
128 undeclared.push(namespace.clone());
129 warnings.push(format!(
130 "Extension namespace '{namespace}' is used but not declared in manifest"
131 ));
132 }
133 }
134
135 ExtensionValidationReport {
136 used_namespaces,
137 declared_namespaces,
138 undeclared,
139 unsupported_required: Vec::new(),
140 warnings,
141 }
142 }
143
144 fn collect_extension_namespaces(
146 blocks: &[Block],
147 namespaces: &mut std::collections::HashSet<String>,
148 ) {
149 for block in blocks {
150 if let Some(ext) = block.as_extension() {
152 namespaces.insert(ext.namespace.clone());
153 }
154
155 match block {
157 Block::Paragraph { children, .. }
158 | Block::Heading { children, .. }
159 | Block::CodeBlock { children, .. }
160 | Block::DefinitionTerm { children, .. } => {
161 Self::collect_marks_namespaces(children, namespaces);
162 }
163 Block::List { children, .. }
164 | Block::ListItem { children, .. }
165 | Block::Blockquote { children, .. }
166 | Block::Table { children, .. }
167 | Block::TableRow { children, .. }
168 | Block::DefinitionItem { children, .. }
169 | Block::DefinitionDescription { children, .. } => {
170 Self::collect_extension_namespaces(children, namespaces);
171 }
172 Block::DefinitionList(dl) => {
173 Self::collect_extension_namespaces(&dl.children, namespaces);
174 }
175 Block::TableCell(cell) => {
176 Self::collect_marks_namespaces(&cell.children, namespaces);
177 }
178 Block::Figure(fig) => {
179 Self::collect_extension_namespaces(&fig.children, namespaces);
180 }
181 Block::FigCaption(fc) => {
182 Self::collect_marks_namespaces(&fc.children, namespaces);
183 }
184 Block::Admonition(adm) => {
185 Self::collect_extension_namespaces(&adm.children, namespaces);
186 }
187 Block::Extension(ext) => {
188 Self::collect_extension_namespaces(&ext.children, namespaces);
190 }
191 Block::HorizontalRule { .. }
193 | Block::Image(_)
194 | Block::Math(_)
195 | Block::Break { .. }
196 | Block::Measurement(_)
197 | Block::Signature(_)
198 | Block::Svg(_)
199 | Block::Barcode(_) => {}
200 }
201 }
202 }
203
204 fn collect_marks_namespaces(
206 texts: &[Text],
207 namespaces: &mut std::collections::HashSet<String>,
208 ) {
209 for text in texts {
210 for mark in &text.marks {
211 if let Some(ext) = mark.as_extension() {
212 namespaces.insert(ext.namespace.clone());
213 }
214 }
215 }
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct VerificationReport {
222 pub content_valid: bool,
224 pub id_valid: bool,
226 pub errors: Vec<String>,
228}
229
230impl VerificationReport {
231 #[must_use]
233 pub fn is_valid(&self) -> bool {
234 self.content_valid && self.id_valid && self.errors.is_empty()
235 }
236}
237
238#[derive(Debug, Clone, Default)]
243pub struct ExtensionValidationReport {
244 pub used_namespaces: Vec<String>,
246 pub declared_namespaces: Vec<String>,
248 pub undeclared: Vec<String>,
250 pub unsupported_required: Vec<String>,
253 pub warnings: Vec<String>,
255}
256
257impl ExtensionValidationReport {
258 #[must_use]
260 pub fn is_valid(&self) -> bool {
261 self.undeclared.is_empty() && self.unsupported_required.is_empty()
262 }
263
264 #[must_use]
266 pub fn has_warnings(&self) -> bool {
267 !self.warnings.is_empty()
268 }
269}