- 1
//! L0: the bounded OPC package reader, inspection and raw-copy writer. - 2
- 3
use std::collections::{BTreeMap, HashMap}; - 4
use std::io::{Read, Seek, SeekFrom, Write}; - 5
- 6
use serde::Serialize; - 7
- 8
use crate::xml::{self, XmlEvent}; - 9
use crate::{Error, Limits}; - 10
- 11
const CFB_MAGIC: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]; - 12
const CONTENT_TYPES: &str = "[Content_Types].xml"; - 13
const PACKAGE_RELS: &str = "_rels/.rels"; - 14
- 15
const REL_OFFICE_DOCUMENT: &str = - 16
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"; - 17
const REL_OFFICE_DOCUMENT_STRICT: &str = - 18
"http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"; - 19
const REL_VISIO_DOCUMENT: &str = "http://schemas.microsoft.com/visio/2010/relationships/document"; - 20
pub(crate) const REL_SIGNATURE_ORIGIN: &str = - 21
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"; - 22
- 23
const CT_VBA_PROJECT: &str = "application/vnd.ms-office.vbaproject"; - 24
const CT_ACTIVEX: &str = "application/vnd.ms-office.activex+xml"; - 25
pub(crate) const CT_SIGNATURE: &str = - 26
"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"; - 27
const CT_XLM_MACROSHEET: &str = "application/vnd.ms-excel.macrosheet+xml"; - 28
const CT_XLM_INTL_MACROSHEET: &str = "application/vnd.ms-excel.intlmacrosheet+xml"; - 29
const CT_CUSTOM_XML_PROPS: &str = - 30
"application/vnd.openxmlformats-officedocument.customxmlproperties+xml"; - 31
- 32
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] - 33
#[serde(rename_all = "snake_case")] - 34
pub enum Vocabulary { - 35
Word, - 36
Excel, - 37
PowerPoint, - 38
Visio, - 39
} - 40
- 41
impl Vocabulary { - 42
/// The label with its article: "an Excel workbook". - 43
pub fn with_article(self) -> &'static str { - 44
match self { - 45
Vocabulary::Word => "a Word document", - 46
Vocabulary::Excel => "an Excel workbook", - 47
Vocabulary::PowerPoint => "a PowerPoint presentation", - 48
Vocabulary::Visio => "a Visio drawing", - 49
} - 50
} - 51
- 52
pub fn label(self) -> &'static str { - 53
match self { - 54
Vocabulary::Word => "Word document", - 55
Vocabulary::Excel => "Excel workbook", - 56
Vocabulary::PowerPoint => "PowerPoint presentation", - 57
Vocabulary::Visio => "Visio drawing", - 58
} - 59
} - 60
} - 61
- 62
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] - 63
#[serde(rename_all = "snake_case")] - 64
pub enum FormatKind { - 65
Document, - 66
Template, - 67
Slideshow, - 68
AddIn, - 69
Stencil, - 70
} - 71
- 72
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] - 73
pub struct Format { - 74
pub vocabulary: Vocabulary, - 75
pub kind: FormatKind, - 76
pub macro_enabled: bool, - 77
} - 78
- 79
impl Format { - 80
/// Classifies a main part by its content type. Detection is by content - 81
/// type, never by the file extension (a renamed package lies about it). - 82
pub fn from_main_content_type(content_type: &str) -> Result<Self, Error> { - 83
use FormatKind::*; - 84
use Vocabulary::*; - 85
let lower = content_type.to_ascii_lowercase(); - 86
let (vocabulary, kind, macro_enabled) = match lower.as_str() { - 87
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" => { - 88
(Word, Document, false) - 89
} - 90
"application/vnd.ms-word.document.macroenabled.main+xml" => (Word, Document, true), - 91
"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml" => { - 92
(Word, Template, false) - 93
} - 94
"application/vnd.ms-word.template.macroenabledtemplate.main+xml" => { - 95
(Word, Template, true) - 96
} - 97
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" => { - 98
(Excel, Document, false) - 99
} - 100
"application/vnd.ms-excel.sheet.macroenabled.main+xml" => (Excel, Document, true), - 101
"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" => { - 102
(Excel, Template, false) - 103
} - 104
"application/vnd.ms-excel.template.macroenabled.main+xml" => (Excel, Template, true), - 105
"application/vnd.ms-excel.addin.macroenabled.main+xml" => (Excel, AddIn, true), - 106
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" => { - 107
(PowerPoint, Document, false) - 108
} - 109
"application/vnd.ms-powerpoint.presentation.macroenabled.main+xml" => { - 110
(PowerPoint, Document, true) - 111
} - 112
"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" => { - 113
(PowerPoint, Template, false) - 114
} - 115
"application/vnd.ms-powerpoint.template.macroenabled.main+xml" => { - 116
(PowerPoint, Template, true) - 117
} - 118
"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" => { - 119
(PowerPoint, Slideshow, false) - 120
} - 121
"application/vnd.ms-powerpoint.slideshow.macroenabled.main+xml" => { - 122
(PowerPoint, Slideshow, true) - 123
} - 124
"application/vnd.ms-powerpoint.addin.macroenabled.main+xml" => { - 125
(PowerPoint, AddIn, true) - 126
} - 127
"application/vnd.ms-visio.drawing.main+xml" => (Visio, Document, false), - 128
"application/vnd.ms-visio.drawing.macroenabled.main+xml" => (Visio, Document, true), - 129
"application/vnd.ms-visio.template.main+xml" => (Visio, Template, false), - 130
"application/vnd.ms-visio.template.macroenabled.main+xml" => (Visio, Template, true), - 131
"application/vnd.ms-visio.stencil.main+xml" => (Visio, Stencil, false), - 132
"application/vnd.ms-visio.stencil.macroenabled.main+xml" => (Visio, Stencil, true), - 133
_ => return Err(Error::UnsupportedFormat(content_type.to_string())), - 134
}; - 135
Ok(Self { - 136
vocabulary, - 137
kind, - 138
macro_enabled, - 139
}) - 140
} - 141
- 142
/// The format a file named with `extension` has, if it is one of the - 143
/// family. - 144
pub fn from_extension(extension: &str) -> Option<Self> { - 145
use FormatKind::*; - 146
use Vocabulary::*; - 147
let (vocabulary, kind, macro_enabled) = match extension.to_ascii_lowercase().as_str() { - 148
"docx" => (Word, Document, false), - 149
"docm" => (Word, Document, true), - 150
"dotx" => (Word, Template, false), - 151
"dotm" => (Word, Template, true), - 152
"xlsx" => (Excel, Document, false), - 153
"xlsm" => (Excel, Document, true), - 154
"xltx" => (Excel, Template, false), - 155
"xltm" => (Excel, Template, true), - 156
"xlam" => (Excel, AddIn, true), - 157
"pptx" => (PowerPoint, Document, false), - 158
"pptm" => (PowerPoint, Document, true), - 159
"potx" => (PowerPoint, Template, false), - 160
"potm" => (PowerPoint, Template, true), - 161
"ppsx" => (PowerPoint, Slideshow, false), - 162
"ppsm" => (PowerPoint, Slideshow, true), - 163
"ppam" => (PowerPoint, AddIn, true), - 164
"vsdx" => (Visio, Document, false), - 165
"vsdm" => (Visio, Document, true), - 166
"vstx" => (Visio, Template, false), - 167
"vstm" => (Visio, Template, true), - 168
"vssx" => (Visio, Stencil, false), - 169
"vssm" => (Visio, Stencil, true), - 170
_ => return None, - 171
}; - 172
Some(Self { - 173
vocabulary, - 174
kind, - 175
macro_enabled, - 176
}) - 177
} - 178
- 179
/// The main part's content type for this format. - 180
pub fn main_content_type(self) -> &'static str { - 181
match self.extension() { - 182
"docx" => { - 183
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" - 184
} - 185
"docm" => "application/vnd.ms-word.document.macroEnabled.main+xml", - 186
"dotx" => { - 187
"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml" - 188
} - 189
"dotm" => "application/vnd.ms-word.template.macroEnabledTemplate.main+xml", - 190
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", - 191
"xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.main+xml", - 192
"xltx" => { - 193
"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" - 194
} - 195
"xltm" => "application/vnd.ms-excel.template.macroEnabled.main+xml", - 196
"xlam" => "application/vnd.ms-excel.addin.macroEnabled.main+xml", - 197
"pptx" => { - 198
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" - 199
} - 200
"pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml", - 201
"potx" => { - 202
"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" - 203
} - 204
"potm" => "application/vnd.ms-powerpoint.template.macroEnabled.main+xml", - 205
"ppsx" => { - 206
"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" - 207
} - 208
"ppsm" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.main+xml", - 209
"ppam" => "application/vnd.ms-powerpoint.addin.macroEnabled.main+xml", - 210
"vsdx" => "application/vnd.ms-visio.drawing.main+xml", - 211
"vsdm" => "application/vnd.ms-visio.drawing.macroEnabled.main+xml", - 212
"vstx" => "application/vnd.ms-visio.template.main+xml", - 213
"vstm" => "application/vnd.ms-visio.template.macroEnabled.main+xml", - 214
"vssx" => "application/vnd.ms-visio.stencil.main+xml", - 215
_ => "application/vnd.ms-visio.stencil.macroEnabled.main+xml", - 216
} - 217
} - 218
- 219
/// The one extension that names this format. - 220
pub fn extension(self) -> &'static str { - 221
use FormatKind::*; - 222
use Vocabulary::*; - 223
match (self.vocabulary, self.kind, self.macro_enabled) { - 224
(Word, Template, false) => "dotx", - 225
(Word, Template, true) => "dotm", - 226
(Word, _, false) => "docx", - 227
(Word, _, true) => "docm", - 228
(Excel, Template, false) => "xltx", - 229
(Excel, Template, true) => "xltm", - 230
(Excel, AddIn, _) => "xlam", - 231
(Excel, _, false) => "xlsx", - 232
(Excel, _, true) => "xlsm", - 233
(PowerPoint, Template, false) => "potx", - 234
(PowerPoint, Template, true) => "potm", - 235
(PowerPoint, Slideshow, false) => "ppsx", - 236
(PowerPoint, Slideshow, true) => "ppsm", - 237
(PowerPoint, AddIn, _) => "ppam", - 238
(PowerPoint, _, false) => "pptx", - 239
(PowerPoint, _, true) => "pptm", - 240
(Visio, Template, false) => "vstx", - 241
(Visio, Template, true) => "vstm", - 242
(Visio, Stencil, false) => "vssx", - 243
(Visio, Stencil, true) => "vssm", - 244
(Visio, _, false) => "vsdx", - 245
(Visio, _, true) => "vsdm", - 246
} - 247
} - 248
} - 249
- 250
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] - 251
#[serde(rename_all = "snake_case")] - 252
pub enum Conformance { - 253
Transitional, - 254
Strict, - 255
} - 256
- 257
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 258
pub struct Relationship { - 259
pub id: String, - 260
pub kind: String, - 261
pub target: String, - 262
pub external: bool, - 263
} - 264
- 265
impl Relationship { - 266
/// Last path segment of the relationship type URI (`image`, `comments`). - 267
pub fn short_kind(&self) -> &str { - 268
self.kind.rsplit('/').next().unwrap_or(&self.kind) - 269
} - 270
} - 271
- 272
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 273
pub struct ExternalRelationship { - 274
pub source: String, - 275
pub kind: String, - 276
pub target: String, - 277
} - 278
- 279
#[derive(Debug, Clone, PartialEq, Eq)] - 280
struct Entry { - 281
name: String, - 282
index: usize, - 283
size: u64, - 284
} - 285
- 286
#[derive(Debug, Default)] - 287
struct ContentTypes { - 288
defaults: HashMap<String, String>, - 289
overrides: HashMap<String, String>, - 290
} - 291
- 292
/// What a read of the package observed, for flags and verification. Every - 293
/// field is derived from the package itself; nothing here is executed. - 294
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 295
pub struct Inspection { - 296
pub format: Format, - 297
pub conformance: Conformance, - 298
pub main_part: String, - 299
pub part_count: usize, - 300
pub vba_project: bool, - 301
pub xlm_macro_sheets: usize, - 302
pub activex_controls: usize, - 303
pub signed: bool, - 304
pub embedded_objects: usize, - 305
pub custom_xml_parts: usize, - 306
pub external_relationships: Vec<ExternalRelationship>, - 307
pub sensitivity_labels: Vec<String>, - 308
/// Parts with neither an `Override` nor a `Default` content type. OPC - 309
/// requires one for every part, and Office refuses a package without. - 310
pub untyped_parts: Vec<String>, - 311
} - 312
- 313
impl Inspection { - 314
/// A remote template (`attachedTemplate` with an external target) is a - 315
/// known phishing and payload-delivery vector. - 316
pub fn remote_template(&self) -> bool { - 317
self.external_relationships - 318
.iter() - 319
.any(|relationship| relationship.kind == "attachedTemplate") - 320
} - 321
- 322
/// One-line human flags, empty when nothing is notable. - 323
pub fn flags(&self) -> Vec<String> { - 324
let mut flags = Vec::new(); - 325
if self.conformance == Conformance::Strict { - 326
flags.push("Strict conformance".into()); - 327
} - 328
if self.vba_project { - 329
flags.push("contains a VBA macro project (never executed)".into()); - 330
} else if self.format.macro_enabled { - 331
flags.push("macro-enabled format without a VBA project".into()); - 332
} - 333
if self.xlm_macro_sheets > 0 { - 334
flags.push(format!( - 335
"{} Excel 4.0 (XLM) macro sheet(s) (never executed)", - 336
self.xlm_macro_sheets - 337
)); - 338
} - 339
if self.activex_controls > 0 { - 340
flags.push(format!( - 341
"{} ActiveX control part(s) (never instantiated)", - 342
self.activex_controls - 343
)); - 344
} - 345
if self.signed { - 346
flags.push("digitally signed (signature not verified yet)".into()); - 347
} - 348
if self.embedded_objects > 0 { - 349
flags.push(format!( - 350
"{} embedded object(s) (never activated)", - 351
self.embedded_objects - 352
)); - 353
} - 354
if self.remote_template() { - 355
flags.push("remote template reference (not followed; a known phishing vector)".into()); - 356
} - 357
let other_external = self - 358
.external_relationships - 359
.iter() - 360
.filter(|relationship| relationship.kind != "attachedTemplate") - 361
.count(); - 362
if other_external > 0 { - 363
flags.push(format!( - 364
"{other_external} external link(s) (recorded, never followed)" - 365
)); - 366
} - 367
if !self.sensitivity_labels.is_empty() { - 368
flags.push(format!( - 369
"sensitivity label: {}", - 370
self.sensitivity_labels.join(", ") - 371
)); - 372
} - 373
if !self.untyped_parts.is_empty() { - 374
flags.push(format!( - 375
"{} part(s) without a content type", - 376
self.untyped_parts.len() - 377
)); - 378
} - 379
flags - 380
} - 381
} - 382
- 383
/// An opened package. Reading a part decompresses it through a bounded - 384
/// reader and charges its actual size against the package total. - 385
pub struct Package<R: Read + Seek> { - 386
archive: zip::ZipArchive<R>, - 387
limits: Limits, - 388
entries: Vec<Entry>, - 389
by_name: HashMap<String, usize>, - 390
content_types: ContentTypes, - 391
relationships: BTreeMap<String, Vec<Relationship>>, - 392
consumed: u64, - 393
format: Format, - 394
conformance: Conformance, - 395
main_part: String, - 396
} - 397
- 398
impl<R: Read + Seek> Package<R> { - 399
pub fn open(mut reader: R, limits: Limits) -> Result<Self, Error> { - 400
let mut magic = [0u8; 8]; - 401
let read = read_prefix(&mut reader, &mut magic)?; - 402
if read == magic.len() && magic == CFB_MAGIC { - 403
return Err(Error::CompoundFile); - 404
} - 405
let length = reader - 406
.seek(SeekFrom::End(0)) - 407
.map_err(|error| Error::Io(error.to_string()))?; - 408
if length > limits.max_total_bytes { - 409
return Err(Error::TotalTooLarge); - 410
} - 411
reader - 412
.seek(SeekFrom::Start(0)) - 413
.map_err(|error| Error::Io(error.to_string()))?; - 414
let mut archive = - 415
zip::ZipArchive::new(reader).map_err(|error| Error::NotZip(error.to_string()))?; - 416
if archive.len() > limits.max_entries { - 417
return Err(Error::TooManyEntries(archive.len())); - 418
} - 419
let mut entries = Vec::with_capacity(archive.len()); - 420
let mut by_name = HashMap::with_capacity(archive.len()); - 421
let mut declared_total = 0u64; - 422
for index in 0..archive.len() { - 423
let file = archive - 424
.by_index_raw(index) - 425
.map_err(|error| Error::NotZip(error.to_string()))?; - 426
let name = std::str::from_utf8(file.name_raw()) - 427
.map_err(|_| { - 428
Error::InvalidPartName(String::from_utf8_lossy(file.name_raw()).into_owned()) - 429
})? - 430
.to_string(); - 431
validate_part_name(name.trim_end_matches('/'))?; - 432
if let Some(decoded) = percent_decode(name.trim_end_matches('/')) { - 433
validate_part_name(&decoded)?; - 434
} - 435
if file.encrypted() { - 436
return Err(Error::EncryptedEntry(name)); - 437
} - 438
if !matches!( - 439
file.compression(), - 440
zip::CompressionMethod::Stored | zip::CompressionMethod::Deflated - 441
) { - 442
return Err(Error::UnsupportedCompression(name)); - 443
} - 444
let size = file.size(); - 445
if size > limits.max_part_bytes { - 446
return Err(Error::PartTooLarge(name)); - 447
} - 448
let compressed = file.compressed_size().max(1); - 449
if size > limits.ratio_floor_bytes && size / compressed > limits.max_ratio { - 450
return Err(Error::CompressionRatio(name)); - 451
} - 452
declared_total = declared_total.saturating_add(size); - 453
if declared_total > limits.max_total_bytes { - 454
return Err(Error::TotalTooLarge); - 455
} - 456
if by_name - 457
.insert(part_key(name.trim_end_matches('/')), entries.len()) - 458
.is_some() - 459
{ - 460
return Err(Error::DuplicatePart(name)); - 461
} - 462
drop(file); - 463
// Directory entries are not parts, but they stay indexed for - 464
// collision checks and are raw-copied on write. - 465
entries.push(Entry { name, index, size }); - 466
} - 467
let mut package = Self { - 468
archive, - 469
limits, - 470
entries, - 471
by_name, - 472
content_types: ContentTypes::default(), - 473
relationships: BTreeMap::new(), - 474
consumed: 0, - 475
format: Format { - 476
vocabulary: Vocabulary::Word, - 477
kind: FormatKind::Document, - 478
macro_enabled: false, - 479
}, - 480
conformance: Conformance::Transitional, - 481
main_part: String::new(), - 482
}; - 483
package.content_types = package.read_content_types()?; - 484
let package_rels = package.relationships("")?.to_vec(); - 485
let main = package_rels - 486
.iter() - 487
.find(|relationship| { - 488
!relationship.external - 489
&& matches!( - 490
relationship.kind.as_str(), - 491
REL_OFFICE_DOCUMENT | REL_OFFICE_DOCUMENT_STRICT | REL_VISIO_DOCUMENT - 492
) - 493
}) - 494
.ok_or(Error::NoMainPart)?; - 495
if main.kind == REL_OFFICE_DOCUMENT_STRICT { - 496
package.conformance = Conformance::Strict; - 497
} - 498
let main_part = main.target.clone(); - 499
if !package.has_part(&main_part) { - 500
return Err(Error::MissingPart(main_part)); - 501
} - 502
let content_type = package - 503
.content_type(&main_part) - 504
.ok_or_else(|| Error::UnsupportedFormat(String::new()))?; - 505
package.format = Format::from_main_content_type(&content_type)?; - 506
if package.format.vocabulary == Vocabulary::Visio - 507
&& package.conformance == Conformance::Strict - 508
{ - 509
return Err(Error::UnsupportedFormat(content_type)); - 510
} - 511
package.main_part = main_part; - 512
Ok(package) - 513
} - 514
- 515
pub fn format(&self) -> Format { - 516
self.format - 517
} - 518
- 519
pub fn conformance(&self) -> Conformance { - 520
self.conformance - 521
} - 522
- 523
pub fn limits(&self) -> &Limits { - 524
&self.limits - 525
} - 526
- 527
/// Part name of the main document part, without a leading slash. - 528
pub fn main_part(&self) -> &str { - 529
&self.main_part - 530
} - 531
- 532
/// Part names in archive order, excluding directory entries. - 533
pub fn part_names(&self) -> impl Iterator<Item = &str> { - 534
self.entries - 535
.iter() - 536
.filter(|entry| !entry.name.ends_with('/')) - 537
.map(|entry| entry.name.as_str()) - 538
} - 539
- 540
/// Part names with their uncompressed sizes, as the archive's - 541
/// directory states them, in archive order. - 542
pub fn part_sizes(&self) -> Vec<(String, u64)> { - 543
self.entries - 544
.iter() - 545
.filter(|entry| !entry.name.ends_with('/')) - 546
.map(|entry| (entry.name.clone(), entry.size)) - 547
.collect() - 548
} - 549
- 550
pub fn has_part(&self, name: &str) -> bool { - 551
self.entry(name).is_some() - 552
} - 553
- 554
fn entry(&self, name: &str) -> Option<&Entry> { - 555
self.by_name - 556
.get(&part_key(name)) - 557
.map(|index| &self.entries[*index]) - 558
.filter(|entry| !entry.name.ends_with('/')) - 559
} - 560
- 561
/// Content type of a part: its `Override`, else the `Default` for its - 562
/// extension. - 563
pub fn content_type(&self, name: &str) -> Option<String> { - 564
let key = part_key(name); - 565
if let Some(value) = self.content_types.overrides.get(&key) { - 566
return Some(value.clone()); - 567
} - 568
let extension = key.rsplit_once('.').map(|(_, extension)| extension)?; - 569
self.content_types.defaults.get(extension).cloned() - 570
} - 571
- 572
/// Decompresses one part through a bounded reader. - 573
pub fn read_part(&mut self, name: &str) -> Result<Vec<u8>, Error> { - 574
let entry = self - 575
.entry(name) - 576
.cloned() - 577
.ok_or_else(|| Error::MissingPart(name.to_string()))?; - 578
let file = self - 579
.archive - 580
.by_index(entry.index) - 581
.map_err(|error| Error::Io(error.to_string()))?; - 582
let mut bytes = Vec::with_capacity(entry.size.min(self.limits.max_part_bytes) as usize); - 583
file.take(self.limits.max_part_bytes + 1) - 584
.read_to_end(&mut bytes) - 585
.map_err(|error| Error::Io(format!("{}: {error}", entry.name)))?; - 586
if bytes.len() as u64 > self.limits.max_part_bytes { - 587
return Err(Error::PartTooLarge(entry.name)); - 588
} - 589
self.consumed = self.consumed.saturating_add(bytes.len() as u64); - 590
if self.consumed > self.limits.max_total_bytes { - 591
return Err(Error::TotalTooLarge); - 592
} - 593
Ok(bytes) - 594
} - 595
- 596
/// Relationships whose source is `source` (`""` for the package). - 597
/// A part with no relationships part has none. - 598
pub fn relationships(&mut self, source: &str) -> Result<&[Relationship], Error> { - 599
let source = source.trim_start_matches('/').to_string(); - 600
if !self.relationships.contains_key(&source) { - 601
let rels_part = rels_part_name(&source); - 602
let parsed = if self.has_part(&rels_part) { - 603
if self.relationships.len() >= self.limits.max_relationship_parts { - 604
return Err(Error::TooManyEntries(self.relationships.len())); - 605
} - 606
let bytes = self.read_part(&rels_part)?; - 607
parse_relationships(&bytes, &rels_part, &source, &self.limits)? - 608
} else if source.is_empty() { - 609
return Err(Error::MissingPart(PACKAGE_RELS.into())); - 610
} else { - 611
Vec::new() - 612
}; - 613
self.relationships.insert(source.clone(), parsed); - 614
} - 615
Ok(self - 616
.relationships - 617
.get(&source) - 618
.map(Vec::as_slice) - 619
.unwrap_or(&[])) - 620
} - 621
- 622
/// Internal target of the first relationship of `source` whose type - 623
/// ends with `/<short_kind>`. - 624
pub fn related_part( - 625
&mut self, - 626
source: &str, - 627
short_kind: &str, - 628
) -> Result<Option<String>, Error> { - 629
Ok(self - 630
.relationships(source)? - 631
.iter() - 632
.find(|relationship| !relationship.external && relationship.short_kind() == short_kind) - 633
.map(|relationship| relationship.target.clone())) - 634
} - 635
- 636
/// Internal target of relationship `id` of `source`. - 637
pub fn part_by_relationship_id( - 638
&mut self, - 639
source: &str, - 640
id: &str, - 641
) -> Result<Option<String>, Error> { - 642
Ok(self - 643
.relationships(source)? - 644
.iter() - 645
.find(|relationship| !relationship.external && relationship.id == id) - 646
.map(|relationship| relationship.target.clone())) - 647
} - 648
- 649
/// Parses the main part's root and checks it names the vocabulary the - 650
/// content type claims (`document`, `workbook`, `presentation`, - 651
/// `VisioDocument`). - 652
pub fn check_main_root(&mut self) -> Result<(), Error> { - 653
let expected = match self.format.vocabulary { - 654
Vocabulary::Word => "document", - 655
Vocabulary::Excel => "workbook", - 656
Vocabulary::PowerPoint => "presentation", - 657
Vocabulary::Visio => "VisioDocument", - 658
}; - 659
let main = self.main_part.clone(); - 660
let bytes = self.read_part(&main)?; - 661
let root = xml::root(&bytes, &main, &self.limits)?; - 662
if root.local() != expected { - 663
return Err(Error::Xml { - 664
part: main, - 665
message: format!("root is {}, expected {expected}", root.local()), - 666
}); - 667
} - 668
Ok(()) - 669
} - 670
- 671
/// Everything a reader should flag. Reads every relationship part, the - 672
/// custom properties and nothing else; executes nothing. - 673
pub fn inspect(&mut self) -> Result<Inspection, Error> { - 674
let names: Vec<String> = self.part_names().map(str::to_string).collect(); - 675
let mut inspection = Inspection { - 676
format: self.format, - 677
conformance: self.conformance, - 678
main_part: self.main_part.clone(), - 679
part_count: names.len(), - 680
vba_project: false, - 681
xlm_macro_sheets: 0, - 682
activex_controls: 0, - 683
signed: false, - 684
embedded_objects: 0, - 685
custom_xml_parts: 0, - 686
external_relationships: Vec::new(), - 687
sensitivity_labels: Vec::new(), - 688
untyped_parts: Vec::new(), - 689
}; - 690
for name in &names { - 691
let Some(content_type) = self.content_type(name) else { - 692
inspection.untyped_parts.push(name.clone()); - 693
continue; - 694
}; - 695
let content_type = content_type.to_ascii_lowercase(); - 696
let lower = name.to_ascii_lowercase(); - 697
match content_type.as_str() { - 698
CT_VBA_PROJECT => inspection.vba_project = true, - 699
CT_ACTIVEX => inspection.activex_controls += 1, - 700
CT_SIGNATURE => inspection.signed = true, - 701
CT_XLM_MACROSHEET | CT_XLM_INTL_MACROSHEET => inspection.xlm_macro_sheets += 1, - 702
CT_CUSTOM_XML_PROPS => inspection.custom_xml_parts += 1, - 703
_ => {} - 704
} - 705
if lower.ends_with("vbaproject.bin") { - 706
inspection.vba_project = true; - 707
} - 708
if lower.contains("/embeddings/") { - 709
inspection.embedded_objects += 1; - 710
} - 711
if let Some(source) = rels_source(name) { - 712
for relationship in self.relationships(&source)?.to_vec() { - 713
if relationship.external { - 714
inspection - 715
.external_relationships - 716
.push(ExternalRelationship { - 717
source: if source.is_empty() { - 718
"package".into() - 719
} else { - 720
source.clone() - 721
}, - 722
kind: relationship.short_kind().to_string(), - 723
target: relationship.target.clone(), - 724
}); - 725
} - 726
} - 727
} - 728
} - 729
if self - 730
.relationships("")? - 731
.iter() - 732
.any(|relationship| relationship.kind == REL_SIGNATURE_ORIGIN) - 733
{ - 734
inspection.signed = true; - 735
} - 736
if let Some(custom) = self - 737
.related_part("", "custom-properties")? - 738
.filter(|part| self.has_part(part)) - 739
{ - 740
let bytes = self.read_part(&custom)?; - 741
inspection.sensitivity_labels = sensitivity_labels(&bytes, &custom, &self.limits)?; - 742
} - 743
Ok(inspection) - 744
} - 745
- 746
/// Writes the package to `out`, replacing the parts `edits` maps to - 747
/// bytes, removing the parts it maps to `None`, and copying every other - 748
/// entry's compressed bytes unchanged (O1). Entries keep their archive - 749
/// order; new parts are appended in name order. Written entries carry a - 750
/// fixed timestamp so the same edits always produce the same bytes. - 751
pub fn rewrite<W: Write + Seek>( - 752
&mut self, - 753
out: W, - 754
edits: &BTreeMap<String, Option<Vec<u8>>>, - 755
) -> Result<W, Error> { - 756
let mut pending: BTreeMap<String, &Option<Vec<u8>>> = BTreeMap::new(); - 757
for (name, bytes) in edits { - 758
let name = name.trim_start_matches('/'); - 759
validate_part_name(name)?; - 760
if pending.insert(part_key(name), bytes).is_some() { - 761
return Err(Error::DuplicatePart(name.to_string())); - 762
} - 763
} - 764
let options = zip::write::SimpleFileOptions::default() - 765
.compression_method(zip::CompressionMethod::Deflated) - 766
.last_modified_time(zip::DateTime::default()); - 767
let mut writer = zip::ZipWriter::new(out); - 768
let io = |error: zip::result::ZipError| Error::Io(error.to_string()); - 769
for entry in self.entries.clone() { - 770
match pending.remove(&part_key(entry.name.trim_end_matches('/'))) { - 771
Some(None) => {} - 772
Some(Some(bytes)) => { - 773
writer - 774
.start_file(entry.name.as_str(), options) - 775
.map_err(io)?; - 776
writer - 777
.write_all(bytes) - 778
.map_err(|error| Error::Io(error.to_string()))?; - 779
} - 780
None => { - 781
let file = self.archive.by_index_raw(entry.index).map_err(io)?; - 782
writer.raw_copy_file(file).map_err(io)?; - 783
} - 784
} - 785
} - 786
let added: Vec<(String, &Vec<u8>)> = edits - 787
.iter() - 788
.filter(|(name, _)| pending.contains_key(&part_key(name))) - 789
.filter_map(|(name, bytes)| { - 790
bytes - 791
.as_ref() - 792
.map(|bytes| (name.trim_start_matches('/').to_string(), bytes)) - 793
}) - 794
.collect(); - 795
for (name, bytes) in added { - 796
writer.start_file(name, options).map_err(io)?; - 797
writer - 798
.write_all(bytes) - 799
.map_err(|error| Error::Io(error.to_string()))?; - 800
} - 801
writer.set_raw_comment(self.archive.comment().to_vec().into_boxed_slice()); - 802
writer.finish().map_err(io) - 803
} - 804
- 805
fn read_content_types(&mut self) -> Result<ContentTypes, Error> { - 806
let bytes = self.read_part(CONTENT_TYPES).map_err(|error| match error { - 807
Error::MissingPart(_) => Error::MissingPart(CONTENT_TYPES.into()), - 808
other => other, - 809
})?; - 810
let mut types = ContentTypes::default(); - 811
let mut saw_types_root = false; - 812
xml::walk(&bytes, CONTENT_TYPES, &self.limits, |event| { - 813
if let XmlEvent::Open(element) = event { - 814
match element.local() { - 815
"Types" => saw_types_root = true, - 816
"Default" => { - 817
if let (Some(extension), Some(content_type)) = - 818
(element.attr("Extension"), element.attr("ContentType")) - 819
{ - 820
types - 821
.defaults - 822
.insert(extension.to_ascii_lowercase(), content_type.to_string()); - 823
} - 824
} - 825
"Override" => { - 826
if let (Some(part), Some(content_type)) = - 827
(element.attr("PartName"), element.attr("ContentType")) - 828
{ - 829
types - 830
.overrides - 831
.insert(part_key(part), content_type.to_string()); - 832
} - 833
} - 834
_ => {} - 835
} - 836
} - 837
Ok(()) - 838
})?; - 839
if !saw_types_root { - 840
return Err(Error::Xml { - 841
part: CONTENT_TYPES.into(), - 842
message: "root is not Types".into(), - 843
}); - 844
} - 845
Ok(types) - 846
} - 847
} - 848
- 849
fn read_prefix(reader: &mut impl Read, buffer: &mut [u8]) -> Result<usize, Error> { - 850
let mut filled = 0; - 851
while filled < buffer.len() { - 852
match reader.read(&mut buffer[filled..]) { - 853
Ok(0) => break, - 854
Ok(count) => filled += count, - 855
Err(error) => return Err(Error::Io(error.to_string())), - 856
} - 857
} - 858
Ok(filled) - 859
} - 860
- 861
/// OPC part-name rules plus the refusals O2 names: no traversal, no - 862
/// absolute or drive paths, no backslashes, no empty segments, no control - 863
/// characters. - 864
pub fn validate_part_name(name: &str) -> Result<(), Error> { - 865
let invalid = || Error::InvalidPartName(name.to_string()); - 866
if name.is_empty() || name.starts_with('/') || name.contains('\\') { - 867
return Err(invalid()); - 868
} - 869
if name.chars().any(|character| character.is_control()) { - 870
return Err(invalid()); - 871
} - 872
if name.len() > 1024 { - 873
return Err(invalid()); - 874
} - 875
for segment in name.split('/') { - 876
if segment.is_empty() || segment == "." || segment == ".." || segment.ends_with('.') { - 877
return Err(invalid()); - 878
} - 879
if segment.contains(':') { - 880
return Err(invalid()); - 881
} - 882
} - 883
Ok(()) - 884
} - 885
- 886
/// Lookup key for a part name: no leading slash, percent-decoded (a ZIP - 887
/// item may hold `a%20b.xml` or `a b.xml` for the same part), ASCII - 888
/// case-folded (OPC part names are case-insensitive). - 889
pub(crate) fn part_key(name: &str) -> String { - 890
let trimmed = name.trim_start_matches('/'); - 891
percent_decode(trimmed) - 892
.unwrap_or_else(|| trimmed.to_string()) - 893
.to_ascii_lowercase() - 894
} - 895
- 896
/// `word/document.xml` → `word/_rels/document.xml.rels`; `""` → `_rels/.rels`. - 897
pub(crate) fn rels_part_name(source: &str) -> String { - 898
match source.rsplit_once('/') { - 899
Some((directory, file)) => format!("{directory}/_rels/{file}.rels"), - 900
None if source.is_empty() => PACKAGE_RELS.into(), - 901
None => format!("_rels/{source}.rels"), - 902
} - 903
} - 904
- 905
/// Inverse of [`rels_part_name`]: the source part a relationships part - 906
/// describes, or `None` when `name` is not a relationships part. - 907
pub(crate) fn rels_source(name: &str) -> Option<String> { - 908
let lower = name.to_ascii_lowercase(); - 909
if !lower.ends_with(".rels") { - 910
return None; - 911
} - 912
let (directory, file) = match name.rsplit_once('/') { - 913
Some((directory, file)) => (directory, file), - 914
None => return None, - 915
}; - 916
let file = &file[..file.len() - ".rels".len()]; - 917
let parent = if directory.eq_ignore_ascii_case("_rels") { - 918
"" - 919
} else { - 920
directory.strip_suffix("/_rels")? - 921
}; - 922
Some(if parent.is_empty() { - 923
file.to_string() - 924
} else { - 925
format!("{parent}/{file}") - 926
}) - 927
} - 928
- 929
pub(crate) fn parse_relationships( - 930
bytes: &[u8], - 931
part: &str, - 932
source: &str, - 933
limits: &Limits, - 934
) -> Result<Vec<Relationship>, Error> { - 935
let mut relationships = Vec::new(); - 936
let mut failure = None; - 937
xml::walk(bytes, part, limits, |event| { - 938
if let XmlEvent::Open(element) = event - 939
&& element.local() == "Relationship" - 940
{ - 941
let (Some(id), Some(kind), Some(target)) = ( - 942
element.attr("Id"), - 943
element.attr("Type"), - 944
element.attr("Target"), - 945
) else { - 946
return Ok(()); - 947
}; - 948
// A target with a URI scheme is external whatever TargetMode - 949
// says: it is recorded and never resolved inside the package. - 950
let external = element - 951
.attr("TargetMode") - 952
.is_some_and(|mode| mode.eq_ignore_ascii_case("External")) - 953
|| has_uri_scheme(target); - 954
let target = if external { - 955
target.to_string() - 956
} else { - 957
match resolve_target(source, target) { - 958
Ok(resolved) => resolved, - 959
Err(error) => { - 960
failure.get_or_insert(error); - 961
return Ok(()); - 962
} - 963
} - 964
}; - 965
relationships.push(Relationship { - 966
id: id.to_string(), - 967
kind: kind.to_string(), - 968
target, - 969
external, - 970
}); - 971
} - 972
Ok(()) - 973
})?; - 974
if let Some(error) = failure { - 975
return Err(error); - 976
} - 977
Ok(relationships) - 978
} - 979
- 980
/// Resolves an internal relationship target against its source part, - 981
/// keeping its percent-encoding (lookups decode through [`part_key`]). A - 982
/// target that climbs above the package root, directly or through an - 983
/// encoded `..`, is refused. - 984
pub fn resolve_target(source: &str, target: &str) -> Result<String, Error> { - 985
let target = target.split('#').next().unwrap_or(""); - 986
let escape = || Error::RelationshipEscape(target.to_string()); - 987
if target.contains('\\') || has_uri_scheme(target) { - 988
return Err(escape()); - 989
} - 990
let mut segments: Vec<&str> = if target.starts_with('/') { - 991
Vec::new() - 992
} else { - 993
match source.rsplit_once('/') { - 994
Some((directory, _)) => directory.split('/').collect(), - 995
None => Vec::new(), - 996
} - 997
}; - 998
for segment in target.split('/') { - 999
let decoded = percent_decode(segment).ok_or_else(escape)?; - 1000
if decoded.contains('/') || decoded.contains('\\') {
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.