1use crate::render_report;
10use crate::DeviceHistory;
11use forensicnomicon::report::Finding;
12
13fn crc32(data: &[u8]) -> u32 {
15 let mut crc: u32 = 0xFFFF_FFFF;
16 for &byte in data {
17 crc ^= u32::from(byte);
18 for _ in 0..8 {
19 let mask = (crc & 1).wrapping_neg(); crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
21 }
22 }
23 !crc
24}
25
26fn xml_escape(text: &str) -> String {
28 text.replace('&', "&")
29 .replace('<', "<")
30 .replace('>', ">")
31 .replace('"', """)
32 .replace('\'', "'")
33}
34
35fn zip_stored(parts: &[(&str, Vec<u8>)]) -> Vec<u8> {
37 let mut out = Vec::new();
38 let mut central = Vec::new();
39 for (name, data) in parts {
40 let crc = crc32(data);
41 let size = data.len() as u32;
42 let offset = out.len() as u32;
43 out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
45 out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]); out.extend_from_slice(&crc.to_le_bytes());
47 out.extend_from_slice(&size.to_le_bytes()); out.extend_from_slice(&size.to_le_bytes());
49 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
50 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(name.as_bytes());
52 out.extend_from_slice(data);
53 central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
55 central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]); central.extend_from_slice(&crc.to_le_bytes());
57 central.extend_from_slice(&size.to_le_bytes());
58 central.extend_from_slice(&size.to_le_bytes());
59 central.extend_from_slice(&(name.len() as u16).to_le_bytes());
60 central.extend_from_slice(&[0u8; 8]); central.extend_from_slice(&0u32.to_le_bytes()); central.extend_from_slice(&offset.to_le_bytes());
63 central.extend_from_slice(name.as_bytes());
64 }
65 let cd_offset = out.len() as u32;
66 let cd_len = central.len() as u32;
67 let count = parts.len() as u16;
68 out.extend_from_slice(¢ral);
69 out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
71 out.extend_from_slice(&[0, 0, 0, 0]); out.extend_from_slice(&count.to_le_bytes());
73 out.extend_from_slice(&count.to_le_bytes());
74 out.extend_from_slice(&cd_len.to_le_bytes());
75 out.extend_from_slice(&cd_offset.to_le_bytes());
76 out.extend_from_slice(&0u16.to_le_bytes()); out
78}
79
80const CONTENT_TYPES: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
81<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
82
83const RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
84<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
85
86#[must_use]
88pub fn render_docx(histories: &[DeviceHistory], findings: &[Finding]) -> Vec<u8> {
89 let mut body = String::from(
90 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
91<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>"#,
92 );
93 for line in render_report(histories, findings).lines() {
94 body.push_str("<w:p><w:r><w:t xml:space=\"preserve\">");
95 body.push_str(&xml_escape(line));
96 body.push_str("</w:t></w:r></w:p>");
97 }
98 body.push_str("</w:body></w:document>");
99
100 zip_stored(&[
101 ("[Content_Types].xml", CONTENT_TYPES.as_bytes().to_vec()),
102 ("_rels/.rels", RELS.as_bytes().to_vec()),
103 ("word/document.xml", body.into_bytes()),
104 ])
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::model::{Attribute, DeviceKey, Provenance, SourceKind, Value};
111 use crate::{audit, correlate, Claim};
112
113 #[test]
114 fn crc32_matches_the_standard_test_vector() {
115 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
116 }
117
118 #[test]
119 fn xml_escape_covers_all_five_entities() {
120 assert_eq!(
121 xml_escape("a&b<c>d\"e'f"),
122 "a&b<c>d"e'f"
123 );
124 }
125
126 #[test]
127 fn docx_is_a_valid_zip_carrying_the_document_part() {
128 let claims = [Claim {
129 device: DeviceKey("SN1".into()),
130 attribute: Attribute::VolumeName,
131 value: Value::Text("A&B<C>".into()), provenance: Provenance {
133 source: SourceKind::MountedDevices,
134 locator: "m".into(),
135 },
136 }];
137 let histories = correlate(&claims);
138 let docx = render_docx(&histories, &audit(&histories));
139 assert_eq!(&docx[..4], b"PK\x03\x04", "starts with a ZIP local header");
140 let as_text = String::from_utf8_lossy(&docx);
141 assert!(
142 as_text.contains("word/document.xml"),
143 "carries the document part"
144 );
145 assert!(as_text.contains("[Content_Types].xml"));
146 assert!(
147 as_text.contains("A&B<C>"),
148 "evidence text is XML-escaped"
149 );
150 assert_eq!(
151 &docx[docx.len() - 22..docx.len() - 18],
152 b"PK\x05\x06",
153 "EOCD present"
154 );
155 }
156}