- 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('\\') { - 1001
return Err(escape()); - 1002
} - 1003
match decoded.as_str() { - 1004
"" | "." => {} - 1005
".." => { - 1006
if segments.pop().is_none() { - 1007
return Err(escape()); - 1008
} - 1009
} - 1010
_ => segments.push(segment), - 1011
} - 1012
} - 1013
if segments.is_empty() { - 1014
return Err(escape()); - 1015
} - 1016
Ok(segments.join("/")) - 1017
} - 1018
- 1019
/// `scheme:` before any `/`, per RFC 3986 (`http:`, `mailto:`, `file:`). - 1020
fn has_uri_scheme(target: &str) -> bool { - 1021
let Some((scheme, _)) = target.split_once(':') else { - 1022
return false; - 1023
}; - 1024
!scheme.is_empty() - 1025
&& !scheme.contains('/') - 1026
&& scheme.starts_with(|character: char| character.is_ascii_alphabetic()) - 1027
&& scheme - 1028
.chars() - 1029
.all(|character| character.is_ascii_alphanumeric() || "+-.".contains(character)) - 1030
} - 1031
- 1032
fn percent_decode(value: &str) -> Option<String> { - 1033
let bytes = value.as_bytes(); - 1034
let mut out = Vec::with_capacity(bytes.len()); - 1035
let mut index = 0; - 1036
while index < bytes.len() { - 1037
if bytes[index] == b'%' { - 1038
let hex = value.get(index + 1..index + 3)?; - 1039
out.push(u8::from_str_radix(hex, 16).ok()?); - 1040
index += 3; - 1041
} else { - 1042
out.push(bytes[index]); - 1043
index += 1; - 1044
} - 1045
} - 1046
String::from_utf8(out).ok() - 1047
} - 1048
- 1049
/// Microsoft Information Protection labels recorded as custom properties - 1050
/// (`MSIP_Label_<guid>_Name`). Labels are read to narrow egress only (O9). - 1051
fn sensitivity_labels(bytes: &[u8], part: &str, limits: &Limits) -> Result<Vec<String>, Error> { - 1052
let mut labels = Vec::new(); - 1053
let mut current: Option<String> = None; - 1054
let mut text = String::new(); - 1055
xml::walk(bytes, part, limits, |event| { - 1056
match event { - 1057
XmlEvent::Open(element) if element.local() == "property" => { - 1058
current = element - 1059
.attr("name") - 1060
.filter(|name| name.starts_with("MSIP_Label_") && name.ends_with("_Name")) - 1061
.map(str::to_string); - 1062
text.clear(); - 1063
} - 1064
XmlEvent::Text(value) if current.is_some() => text.push_str(&value), - 1065
XmlEvent::Close(name) if xml::local_name(&name) == "property" => { - 1066
let label = current.take().map(|_| text.trim().to_string()); - 1067
labels.extend(label.filter(|label| !label.is_empty())); - 1068
} - 1069
_ => {} - 1070
} - 1071
Ok(()) - 1072
})?; - 1073
labels.sort(); - 1074
labels.dedup(); - 1075
Ok(labels) - 1076
} - 1077
- 1078
#[cfg(test)] - 1079
#[allow(clippy::unwrap_used, clippy::expect_used)] - 1080
mod tests { - 1081
use super::*; - 1082
- 1083
#[test] - 1084
fn part_names_refuse_traversal_and_odd_shapes() { - 1085
for bad in [ - 1086
"../evil.xml", - 1087
"word/../../evil.xml", - 1088
"/abs.xml", - 1089
"word\\document.xml", - 1090
"word//document.xml", - 1091
"C:/x.xml", - 1092
"word/./document.xml", - 1093
"word/document.xml.", - 1094
"a\u{0}b", - 1095
] { - 1096
assert!(validate_part_name(bad).is_err(), "{bad} should be refused"); - 1097
} - 1098
validate_part_name("word/document.xml").unwrap(); - 1099
validate_part_name("[Content_Types].xml").unwrap(); - 1100
} - 1101
- 1102
#[test] - 1103
fn targets_resolve_relative_to_source_and_never_escape() { - 1104
assert_eq!( - 1105
resolve_target("word/document.xml", "media/image1.png").unwrap(), - 1106
"word/media/image1.png" - 1107
); - 1108
assert_eq!( - 1109
resolve_target("ppt/slides/slide1.xml", "../slideLayouts/slideLayout1.xml").unwrap(), - 1110
"ppt/slideLayouts/slideLayout1.xml" - 1111
); - 1112
assert_eq!( - 1113
resolve_target("", "/word/document.xml").unwrap(), - 1114
"word/document.xml" - 1115
); - 1116
assert_eq!( - 1117
resolve_target("", "word/a%20b.xml").unwrap(), - 1118
"word/a%20b.xml" - 1119
); - 1120
assert_eq!(part_key("word/a%20b.xml"), part_key("/Word/A B.xml")); - 1121
assert!(resolve_target("word/document.xml", "../../etc/passwd").is_err()); - 1122
assert!(resolve_target("word/document.xml", "%2e%2e/%2E%2E/x").is_err()); - 1123
assert!(resolve_target("", "..").is_err()); - 1124
assert!(resolve_target("", "a%2fb").is_err()); - 1125
assert!(has_uri_scheme("mailto:a@b.c") && has_uri_scheme("https://x")); - 1126
assert!(!has_uri_scheme("media/a:b.png") && !has_uri_scheme("../x.xml")); - 1127
} - 1128
- 1129
#[test] - 1130
fn rels_names_round_trip() { - 1131
for source in ["", "word/document.xml", "ppt/slides/slide1.xml"] { - 1132
assert_eq!(rels_source(&rels_part_name(source)).unwrap(), source); - 1133
} - 1134
assert_eq!(rels_source("word/document.xml"), None); - 1135
} - 1136
- 1137
#[test] - 1138
fn every_extension_maps_back_from_its_format() { - 1139
for extension in crate::EXTENSIONS { - 1140
assert!(crate::is_openxml_path(&format!("x.{extension}"))); - 1141
} - 1142
let format = - 1143
Format::from_main_content_type("application/vnd.ms-excel.sheet.macroEnabled.main+xml") - 1144
.unwrap(); - 1145
assert_eq!(format.extension(), "xlsm"); - 1146
assert!( - 1147
Format::from_main_content_type( - 1148
"application/vnd.ms-excel.sheet.binary.macroEnabled.main" - 1149
) - 1150
.is_err() - 1151
); - 1152
} - 1153
} - 1154
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.