Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/office/opc.rs

9.8 KiB, 60 runs

created by r1870400018:22587, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Open Packaging Conventions: the two parts every Office document has before it has any content.
2//!
3//! `[Content_Types].xml` says what each part in the archive is, and a `.rels` part says how the parts
4//! refer to one another. A document missing either is one Word declines to open, with a message that
5//! names neither the part nor the reason, so both are built here rather than written out by hand in
6//! three places.
7//!
8//! Relationship ids are `rId1`, `rId2`, and so on. They are *local to the part that owns the rels*,
9//! which is why [`Rels`] hands them out rather than a counter somewhere global: the ids in
10//! `word/_rels/document.xml.rels` have nothing to do with the ids in `_rels/.rels`, and a scheme that
11//! shared them would work until the day two parts both had one.
12//!
13//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
14//! Anthropic Claude
15
16use crate::zip::Zip;
17
18use oxedyne_fe2o3_core::prelude::*;
19use oxedyne_fe2o3_text::xml::Xml;
20use oxedyne_fe2o3_text::xml::write::Out;
21
22use std::collections::BTreeMap;
23
24//// The namespaces, relationship types and content types OOXML fixes. Every
25//// value here is written into the package and read back by other programs, so
26//// none of them is ours to change.
27pub const NS_TYPES: &str = "http://schemas.openxmlformats.org/package/2006/content-types";
28pub const NS_RELS: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
29pub const NS_R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
30
31pub const REL_DOC: &str =
32 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
33pub const REL_STYLES: &str =
34 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
35pub const REL_NUMBERING: &str =
36 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
37pub const REL_HYPERLINK: &str =
38 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
39
40pub const CT_DOCUMENT: &str =
41 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
42pub const CT_STYLES: &str =
43 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
44pub const CT_NUMBERING: &str =
45 "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
46pub const REL_SHEET: &str =
47 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
48pub const REL_STRINGS: &str =
49 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
50
51pub const CT_WORKBOOK: &str =
52 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml";
53pub const CT_SHEET: &str =
54 "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";
55pub const CT_STRINGS: &str =
56 "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml";
57pub const CT_SHEET_STYLES: &str =
58 "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml";
59
60pub const REL_MASTER: &str =
61 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
62pub const REL_SLIDE: &str =
63 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
64pub const REL_LAYOUT: &str =
65 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
66pub const REL_THEME: &str =
67 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
68
69pub const CT_PRESENTATION: &str =
70 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
71pub const CT_SLIDE: &str =
72 "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
73pub const CT_MASTER: &str =
74 "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml";
75pub const CT_LAYOUT: &str =
76 "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml";
77pub const CT_THEME: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
78
79/// The content type of a `.rels` part, which is declared by extension rather than by name.
80pub const CT_RELS: &str = "application/vnd.openxmlformats-package.relationships+xml";
81
82/// One relationship: what it is, and what it points at.
83#[derive(Clone, Debug, PartialEq)]
84pub struct Rel {
85 pub id: String, // unique within the part that owns it
86 pub kind: String, // what the target is for
87 pub target: String, // a part within the package, or a URL where it is external
88 pub external: bool,
89}
90
91/// The relationships one part owns, and the ids it has handed out.
92#[derive(Clone, Debug, Default)]
93pub struct Rels {
94 items: Vec<Rel>, // in the order they were added
95}
96
97impl Rels {
98
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 /// Adds a relationship to another part of the package, giving back the id to refer to it by.
104 pub fn add(&mut self, kind: &str, target: &str) -> String {
105 self.push(kind, target, false)
106 }
107
108 /// Adds a relationship to something outside the package, giving back the id.
109 pub fn add_external(&mut self, kind: &str, target: &str) -> String {
110 self.push(kind, target, true)
111 }
112
113 pub fn items(&self) -> &[Rel] {
114 &self.items
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.items.is_empty()
119 }
120
121 fn push(&mut self, kind: &str, target: &str, external: bool) -> String {
122 let id = fmt!("rId{}", self.items.len() + 1);
123 self.items.push(Rel {
124 id: id.clone(),
125 kind: kind.to_string(),
126 target: target.to_string(),
127 external,
128 });
129 id
130 }
131
132 /// The part as XML.
133 pub fn write(&self) -> Outcome<String> {
134 let mut out = Out::declared();
135 out.open("Relationships", &[("xmlns", NS_RELS)]);
136 for r in &self.items {
137 match r.external {
138 true => out.empty("Relationship", &[
139 ("Id", &r.id),
140 ("Type", &r.kind),
141 ("Target", &r.target),
142 ("TargetMode", "External"),
143 ]),
144 false => out.empty("Relationship", &[
145 ("Id", &r.id),
146 ("Type", &r.kind),
147 ("Target", &r.target),
148 ]),
149 }
150 }
151 res!(out.close("Relationships"));
152 out.finish()
153 }
154}
155
156/// What each part of the package is: by extension for the ones there are many of, and by name for the
157/// ones there are not.
158#[derive(Clone, Debug, Default)]
159pub struct Types {
160 defaults: Vec<(String, String)>, // extension, content type
161 overrides: Vec<(String, String)>, // part name, content type
162}
163
164impl Types {
165
166 /// A package declaring the two defaults every Office document needs: the relationship parts, and
167 /// XML for everything else.
168 pub fn new() -> Self {
169 let mut t = Self::default();
170 t.by_ext("rels", CT_RELS);
171 t.by_ext("xml", "application/xml");
172 t
173 }
174
175 /// Named `by_ext` rather than `default`, which would shadow the trait method of that name on the
176 /// same type and make `Types::default()` mean two things.
177 pub fn by_ext(&mut self, ext: &str, kind: &str) {
178 self.defaults.push((ext.to_string(), kind.to_string()));
179 }
180
181 /// The name is absolute within the package, leading slash and all.
182 pub fn over(&mut self, part: &str, kind: &str) {
183 self.overrides.push((part.to_string(), kind.to_string()));
184 }
185
186 /// The part as XML.
187 pub fn write(&self) -> Outcome<String> {
188 let mut out = Out::declared();
189 out.open("Types", &[("xmlns", NS_TYPES)]);
190 for (ext, kind) in &self.defaults {
191 out.empty("Default", &[("Extension", ext), ("ContentType", kind)]);
192 }
193 for (part, kind) in &self.overrides {
194 out.empty("Override", &[("PartName", part), ("ContentType", kind)]);
195 }
196 res!(out.close("Types"));
197 out.finish()
198 }
199}
200
201// ---------------------------------------------------------------------------
202// Finding a part in a package that was read
203// ---------------------------------------------------------------------------
204
205/// The directory a part sits in, with its trailing slash, so a relative target resolves against it.
206pub fn dir_of(part: &str) -> String {
207 match part.rfind('/') {
208 Some(k) => part[..k + 1].to_string(),
209 None => String::new(),
210 }
211}
212
213/// Where a relationship target actually is within the package.
214pub fn resolve(dir: &str, target: &str) -> String {
215 match target.starts_with('/') {
216 true => target[1..].to_string(),
217 false => fmt!("{}{}", dir, target),
218 }
219}
220
221/// The relationships a part owns, by id: the type, and the resolved target.
222///
223/// A part's relationships live beside it, in a `_rels` directory, in a file named after it. The
224/// package's own are in `_rels/.rels`, which is the same rule with an empty name -- so `""` asks for
225/// them.
226///
227/// A part with no `.rels` beside it has no relationships, which is not an error: most parts have none.
228pub fn rels_of(zip: &Zip, part: &str, cap: u64) -> Outcome<BTreeMap<String, (String, String)>> {
229 let dir = dir_of(part);
230 let name = &part[dir.len()..];
231 let path = fmt!("{}_rels/{}.rels", dir, name);
232 let mut out = BTreeMap::new();
233 if !zip.has(&path) {
234 return Ok(out);
235 }
236 let text = res!(String::from_utf8(res!(zip.content_capped(&path, cap))), Decode, String);
237 let xml = res!(Xml::parse(&text));
238 for rel in res!(xml.root()).children("Relationship") {
239 let id = match rel.attr("Id") {
240 Some(id) => id.to_string(),
241 None => continue,
242 };
243 let kind = rel.attr("Type").unwrap_or("").to_string();
244 let target = rel.attr("Target").unwrap_or("").to_string();
245 let target = match rel.attr("TargetMode") {
246 Some("External") => target,
247 _ => resolve(&dir, &target),
248 };
249 out.insert(id, (kind, target));
250 }
251 Ok(out)
252}
253
254/// The part that IS the document: what `_rels/.rels` points at with [`REL_DOC`].
255///
256/// Named from the package rather than guessed at, because `word/document.xml` is a convention and not a
257/// rule -- a `.docm` names `word/document.xml` too, and a document saved by a generator that used
258/// another name still opens in Word.
259pub fn main_part(zip: &Zip, cap: u64) -> Outcome<String> {
260 let rels = res!(rels_of(zip, "", cap));
261 Ok(res!(rels.values()
262 .find(|(kind, _)| kind == REL_DOC)
263 .map(|(_, t)| t.clone())
264 .filter(|t| zip.has(t))
265 .ok_or_else(|| err!(
266 "The package names no document part, so this is not an Office document. It holds: {}.",
267 zip.names().join(", "); Invalid, Input, Missing))))
268}