1use crate::{
7 error::Error as ArchiveError,
8 zip::{Member, MemberId, Names},
9};
10use structured_asn1::{
11 memory::{MemoryError, OwnedVec, Work},
12 range::RangeSource,
13};
14use structured_cms::stream::{AsyncSource, Receiver, Refused};
15use structured_xades::xml::{Access, AccessError, Name, Node, Snapshot, Xml};
16
17pub const ODF: &str = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Format {
23 Odf,
25 Ocf,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum FaultKind {
32 Structure,
34 Missing,
36 Version,
38 Target,
40 Duplicate,
42 Forbidden,
44 Undeclared,
46 Media,
48 Unassessed,
50}
51
52#[derive(Debug, Clone, Copy)]
54pub struct Fault<N, D, G> {
55 pub node: N,
57 pub member: Option<MemberId<D, G>>,
59 pub kind: FaultKind,
61}
62
63pub enum Target<'v, 's, S: RangeSource> {
65 Absent,
67 Package,
69 Directory(OwnedVec<u8>),
71 Members(Names<'v, 's, S>),
73}
74
75pub struct Entry<'v, 's, N, S: RangeSource> {
77 node: N,
78 target: Target<'v, 's, S>,
79 media: Option<OwnedVec<u8>>,
80}
81impl<'v, 's, N: Copy, S: RangeSource> Entry<'v, 's, N, S> {
82 #[must_use]
84 pub fn node(&self) -> N {
85 self.node
86 }
87 #[must_use]
89 pub fn target(&self) -> &Target<'v, 's, S> {
90 &self.target
91 }
92 #[must_use]
94 pub fn media_type(&self) -> Option<&[u8]> {
95 self.media.as_deref()
96 }
97}
98
99#[derive(Debug)]
101pub enum Error<E, N, SE> {
102 Xml(AccessError<E, N>),
104 Archive(ArchiveError<SE>),
106 Resource {
108 node: N,
110 cause: MemoryError,
112 },
113}
114
115pub struct Reading<'v, 's, N, D, G, S: RangeSource> {
118 source: Member<'v, 's, S>,
119 snapshot: Snapshot<D, G>,
120 root: N,
121 format: Format,
122 entries: OwnedVec<Entry<'v, 's, N, S>>,
123 faults: OwnedVec<Fault<N, S::Document, S::Generation>>,
124}
125impl<'v, 's, N: Copy, D: Copy, G: Copy, S: RangeSource> Reading<'v, 's, N, D, G, S> {
126 #[must_use]
128 pub fn source(&self) -> &Member<'v, 's, S> {
129 &self.source
130 }
131 #[must_use]
133 pub fn snapshot(&self) -> Snapshot<D, G> {
134 self.snapshot
135 }
136 #[must_use]
138 pub fn root(&self) -> N {
139 self.root
140 }
141 #[must_use]
143 pub fn format(&self) -> Format {
144 self.format
145 }
146 #[must_use]
148 pub fn entries(&self) -> &[Entry<'v, 's, N, S>] {
149 &self.entries
150 }
151 #[must_use]
153 pub fn faults(&self) -> &[Fault<N, S::Document, S::Generation>] {
154 &self.faults
155 }
156
157 pub async fn check_root_media<W: Work>(
165 &self,
166 work: &W,
167 ) -> Result<RootMedia, crate::zip::ReadFailure<S::Error, S::Document, S::Generation>> {
168 let archive = self.source.archive();
169 let execution = archive.session().query(work);
170 let failure = |cause| crate::zip::ReadFailure::Processing {
171 member: self.source.id(),
172 cause,
173 };
174 archive
175 .admit(&execution, self.source.central_span())
176 .map_err(failure)?;
177 if self.format != Format::Odf {
178 return Ok(RootMedia::NotApplicable);
179 }
180 let names = archive.find(b"mimetype", &execution).map_err(failure)?;
181 let mut root = None;
182 for entry in &self.entries {
183 execution.charge_work(1).map_err(|cause| {
184 failure(crate::error::resource(self.source.central_span(), cause))
185 })?;
186 if matches!(entry.target, Target::Package) {
187 if root.is_some() {
188 return Ok(RootMedia::Unavailable);
189 }
190 root = Some(entry);
191 }
192 }
193 if names.is_empty() && root.is_none() {
194 return Ok(RootMedia::Absent);
195 }
196 if names.len() != 1 {
197 return Ok(RootMedia::Unavailable);
198 }
199 let Some(expected) = root.and_then(Entry::media_type) else {
200 return Ok(RootMedia::Unavailable);
201 };
202 let member = names
203 .member(0, &execution)
204 .map_err(failure)?
205 .expect("one exact original candidate");
206 let mut source = member.source(&execution)?;
207 let mut receiver = Compare {
208 expected,
209 at: 0,
210 differs: false,
211 work: &execution,
212 refused: None,
213 };
214 let result = source.stream(&mut receiver, &execution).await;
215 if let Some(cause) = receiver.refused {
216 return Err(failure(crate::error::resource(
217 member.central_span(),
218 cause,
219 )));
220 }
221 result?;
222 archive
223 .admit(&execution, self.source.central_span())
224 .map_err(failure)?;
225 Ok(if receiver.differs || receiver.at != expected.len() {
226 RootMedia::Differs
227 } else {
228 RootMedia::Matches
229 })
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum RootMedia {
236 NotApplicable,
238 Absent,
240 Unavailable,
242 Matches,
244 Differs,
246}
247
248struct Compare<'a, W: Work> {
249 expected: &'a [u8],
250 at: usize,
251 differs: bool,
252 work: &'a W,
253 refused: Option<MemoryError>,
254}
255impl<W: Work> Receiver for Compare<'_, W> {
256 fn write(&mut self, bytes: &[u8]) -> Result<(), Refused> {
257 if self.refused.is_some() {
258 return Err(Refused);
259 }
260 if let Err(cause) = self.work.charge_work(bytes.len() as u64) {
261 self.refused = Some(cause);
262 return Err(Refused);
263 }
264 if self.differs {
265 return Ok(());
266 }
267 if bytes.len() > self.expected.len() - self.at {
268 self.differs = true;
269 return Ok(());
270 }
271 let end = self.at + bytes.len();
272 if self.expected[self.at..end] != *bytes {
273 self.differs = true;
274 }
275 self.at = end;
276 Ok(())
277 }
278}
279
280type ResultOf<T, X, S> =
281 Result<T, Error<<X as Xml>::Error, <X as Xml>::Id, <S as RangeSource>::Error>>;
282
283pub type InspectionResult<'v, 's, X, S> = ResultOf<
285 Reading<'v, 's, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, S>,
286 X,
287 S,
288>;
289
290#[expect(
291 clippy::too_many_lines,
292 reason = "ODF and OCF share one ordered inventory walk and duplicate map; splitting the closure would obscure which exact source declarations discharge the aggregate census"
293)]
294pub(crate) fn inspect<'v, 's, X: Xml, S: RangeSource, W: Work>(
295 base: Member<'v, 's, S>,
296 access: &Access<'_, X, W>,
297 root: X::Id,
298 format: Format,
299) -> InspectionResult<'v, 's, X, S> {
300 let work = access.execution();
301 let archive = base.archive();
302 archive
303 .admit(work, base.central_span())
304 .map_err(Error::Archive)?;
305 let namespace = match format {
306 Format::Odf => ODF,
307 Format::Ocf => crate::xml::OCF,
308 };
309 let has_name = |node, local| {
310 access
311 .has_name(node, Name { namespace, local })
312 .map_err(Error::Xml)
313 };
314 let mut faults = work.vector();
315 let mut entries = work.vector();
316 let refusal = |cause| Error::Resource { node: root, cause };
317 let fault = |faults: &mut OwnedVec<_, _>, node, member, kind| {
318 faults
319 .try_push(Fault { node, member, kind })
320 .map_err(refusal)
321 };
322 let root_name = match format {
323 Format::Odf => "manifest",
324 Format::Ocf => "container",
325 };
326 if !has_name(root, root_name)? {
327 fault(&mut faults, root, None, FaultKind::Structure)?;
328 }
329 let version_namespace = if format == Format::Odf { ODF } else { "" };
330 let valid = access
331 .attribute(
332 root,
333 Name {
334 namespace: version_namespace,
335 local: "version",
336 },
337 |value| {
338 let Some(value) = value else {
339 return Ok(false);
340 };
341 work.charge_work(value.len() as u64)?;
342 Ok(value.trim_matches([' ', '\t', '\r', '\n'])
343 == if format == Format::Odf { "1.2" } else { "1.0" })
344 },
345 )
346 .map_err(Error::Xml)?
347 .map_err(refusal)?;
348 if !valid {
349 fault(&mut faults, root, None, FaultKind::Version)?;
350 }
351 let mut parents = work.vector();
352 if format == Format::Ocf {
353 let mut groups = 0usize;
354 walk::<X, S, W>(access, root, |node, element| {
355 if element && has_name(node, "rootfiles")? {
356 groups += 1;
357 parents.try_push(node).map_err(refusal)
358 } else {
359 fault(&mut faults, node, None, FaultKind::Structure)
360 }
361 })?;
362 if groups != 1 {
363 fault(
364 &mut faults,
365 root,
366 None,
367 if groups == 0 {
368 FaultKind::Missing
369 } else {
370 FaultKind::Duplicate
371 },
372 )?;
373 }
374 } else {
375 parents.try_push(root).map_err(refusal)?;
376 }
377 let mut declared = work.map();
378 let mut package = false;
379 for parent in parents {
380 walk::<X, S, W>(access, parent, |node, element| {
381 let entry_name = if format == Format::Odf {
382 "file-entry"
383 } else {
384 "rootfile"
385 };
386 if !element || !has_name(node, entry_name)? {
387 return fault(&mut faults, node, None, FaultKind::Structure);
388 }
389 let attr_namespace = if format == Format::Odf { ODF } else { "" };
390 let target = access
394 .attribute(
395 node,
396 Name {
397 namespace: attr_namespace,
398 local: "full-path",
399 },
400 |value| {
401 let Some(value) = value else {
402 return Ok(Target::Absent);
403 };
404 work.charge_work(value.len() as u64)
405 .map_err(|cause| Error::Resource { node, cause })?;
406 if value == "/" && format == Format::Odf {
407 return Ok(Target::Package);
408 }
409 if value.ends_with('/') && format == Format::Odf {
410 let mut path = work.vector();
411 path.try_extend_from_slice(value.as_bytes())
412 .map_err(|cause| Error::Resource { node, cause })?;
413 return Ok(Target::Directory(path.into_owned()));
414 }
415 archive
416 .find(value.as_bytes(), work)
417 .map(Target::Members)
418 .map_err(Error::Archive)
419 },
420 )
421 .map_err(Error::Xml)??;
422 match &target {
423 Target::Absent => fault(&mut faults, node, None, FaultKind::Missing)?,
424 Target::Package => {
425 if package {
426 fault(&mut faults, node, None, FaultKind::Duplicate)?;
427 }
428 package = true;
429 }
430 Target::Members(names) => {
431 if names.len() != 1 {
432 fault(&mut faults, node, None, FaultKind::Target)?;
433 }
434 for ordinal in 0..names.len() {
435 let member = names
436 .member(ordinal, work)
437 .map_err(Error::Archive)?
438 .expect("exact-name candidate count bounds its ordinal");
439 if declared
440 .try_insert(member.id().ordinal(), ())
441 .map_err(refusal)?
442 .is_some()
443 {
444 fault(&mut faults, node, Some(member.id()), FaultKind::Duplicate)?;
445 }
446 if format == Format::Odf
447 && (member.name() == b"mimetype"
448 || member.name() == b"META-INF/manifest.xml")
449 {
450 fault(&mut faults, node, Some(member.id()), FaultKind::Forbidden)?;
451 }
452 }
453 }
454 Target::Directory(_) => {}
455 }
456 let media = access
457 .attribute(
458 node,
459 Name {
460 namespace: attr_namespace,
461 local: "media-type",
462 },
463 |value| {
464 let Some(value) = value else {
465 return Ok(None);
466 };
467 let mut bytes = work.vector();
468 bytes.try_extend_from_slice(value.as_bytes())?;
469 Ok(Some(bytes.into_owned()))
470 },
471 )
472 .map_err(Error::Xml)?
473 .map_err(refusal)?;
474 match &media {
475 None => fault(&mut faults, node, None, FaultKind::Missing)?,
476 Some(bytes) if !bytes.is_empty() => {
477 let mut syntax = structured_cms::mime::Syntax::<true>::new();
478 let text = core::str::from_utf8(bytes).expect("XML attribute bytes are UTF-8");
479 syntax.feed(text, work).map_err(refusal)?;
480 if !syntax.complete() {
481 fault(&mut faults, node, None, FaultKind::Media)?;
482 }
483 }
484 Some(_) => {}
485 }
486 walk::<X, S, W>(access, node, |child, element| {
487 fault(
488 &mut faults,
489 child,
490 None,
491 if element {
492 FaultKind::Unassessed
493 } else {
494 FaultKind::Structure
495 },
496 )
497 })?;
498 entries
499 .try_push(Entry {
500 node,
501 target,
502 media,
503 })
504 .map_err(refusal)
505 })?;
506 }
507 if entries.is_empty() {
508 fault(&mut faults, root, None, FaultKind::Missing)?;
509 }
510 if format == Format::Odf {
511 for ordinal in 0..archive.len() {
515 let member = archive
516 .member(ordinal, work)
517 .map_err(Error::Archive)?
518 .expect("archive length bounds member ordinal");
519 let name = member.name();
520 work.charge_work(name.len() as u64).map_err(refusal)?;
521 if name == b"mimetype" {
522 if !package {
523 fault(&mut faults, root, Some(member.id()), FaultKind::Missing)?;
524 }
525 } else if !name.starts_with(b"META-INF/")
526 && !name.ends_with(b"/")
527 && declared.get(&ordinal, work).map_err(refusal)?.is_none()
528 {
529 fault(&mut faults, root, Some(member.id()), FaultKind::Undeclared)?;
530 }
531 }
532 }
533 access.inspect(root, |_| ()).map_err(Error::Xml)?;
534 archive
535 .admit(work, base.central_span())
536 .map_err(Error::Archive)?;
537 Ok(Reading {
538 source: base,
539 snapshot: access.snapshot(),
540 root,
541 format,
542 entries: entries.into_owned(),
543 faults: faults.into_owned(),
544 })
545}
546
547fn walk<X: Xml, S: RangeSource, W: Work>(
548 access: &Access<'_, X, W>,
549 parent: X::Id,
550 mut visit: impl FnMut(X::Id, bool) -> ResultOf<(), X, S>,
551) -> ResultOf<(), X, S> {
552 let mut next = access.first_child(parent).map_err(Error::Xml)?;
553 while let Some(node) = next {
554 next = access.next_sibling(parent, node).map_err(Error::Xml)?;
555 let kind = access
556 .inspect(node, |value| match value {
557 Node::Element(_) => Ok(Some(true)),
558 Node::Other => Ok(None),
559 Node::Text(text) => {
560 access.execution().charge_work(text.len() as u64)?;
561 Ok(
562 if text
563 .bytes()
564 .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
565 {
566 None
567 } else {
568 Some(false)
569 },
570 )
571 }
572 })
573 .map_err(Error::Xml)?
574 .map_err(|cause| Error::Resource { node, cause })?;
575 if let Some(element) = kind {
576 visit(node, element)?;
577 }
578 }
579 Ok(())
580}