Merge PR #1156: XLSX: [Content_Types].xml is the first zip entry, workbook parts precede document properties, so libmagic detects the file type (#1090)

This commit is contained in:
aiongg
2026-10-22 02:34:36 +00:00
4 changed files with 41 additions and 4 deletions
+1
View File
@@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | What landed |
|---|---|
| 2026-10-13 | XLSX: `[Content_Types].xml`, `_rels/.rels`, the workbook rels and `xl/workbook.xml` are the first zip entries (document properties last), so libmagic and PHP `finfo` detect the file type even with large properties (#1090) |
| 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
| 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) |
+1
View File
@@ -19,6 +19,7 @@ mod smooth_roundtrip;
mod trendline_roundtrip;
mod xls_tests;
mod xlsb_tests;
mod zip_order;
use std::io::Write;
+33
View File
@@ -0,0 +1,33 @@
//! libmagic (PHP `finfo`, `file`) detects an OOXML package from the first zip entries (#1090).
use gridcraft_model::Workbook;
use crate::write_xlsx;
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
hay.windows(needle.len()).position(|w| w == needle)
}
#[test]
fn package_starts_with_content_types_rels_and_workbook_before_any_large_part() {
let mut wb = Workbook::new();
// A long, hard-to-compress description makes docProps/core.xml big.
wb.props.description =
(0..20_000).map(|i| char::from(b'a' + (i * 7 % 26) as u8)).chain((0..20_000).map(|i| char::from(b'0' + (i * i % 10) as u8))).collect();
let bytes = write_xlsx(&wb).unwrap();
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
let names: Vec<String> = (0..zip.len()).filter_map(|i| zip.by_index(i).ok().map(|f| f.name().to_string())).collect();
assert_eq!(names.first().map(String::as_str), Some("[Content_Types].xml"), "{names:?}");
assert_eq!(names.get(1).map(String::as_str), Some("_rels/.rels"), "{names:?}");
assert_eq!(names.get(2).map(String::as_str), Some("xl/_rels/workbook.xml.rels"), "{names:?}");
assert_eq!(names.get(3).map(String::as_str), Some("xl/workbook.xml"), "{names:?}");
// No data descriptor (general-purpose bit 3) on the first local header, entry name at offset 30.
assert_eq!(bytes.get(..4), Some(&b"PK\x03\x04"[..]));
assert_eq!(bytes.get(6).map(|b| b & 0x08), Some(0));
assert_eq!(bytes.get(30..49), Some(&b"[Content_Types].xml"[..]));
// libmagic looks for an `xl/` entry name within the first few KB: keep it well inside that.
let at = find(&bytes, b"xl/_rels/workbook.xml.rels");
assert!(at.is_some_and(|p| p < 2048), "xl/ entry at {at:?}");
}
+6 -4
View File
@@ -452,13 +452,15 @@ pub fn write_xlsx(wb: &Workbook, kind: XlsxKind) -> Result<Vec<u8>, IoError> {
ct.push_str("</Types>");
let mut zip = ZipOut::new();
zip.add("[Content_Types].xml", ct.as_bytes())?;
// Conventional order: rels, docProps, workbook, the rest.
// Excel's order: rels, workbook rels, workbook, the rest, document properties last. libmagic (PHP
// `finfo`, `file`) recognises an OOXML package only when an `xl/` entry name appears within the
// first few KB, so no user-sized part (document properties, a long description) may precede it.
let rank = |n: &str| match n {
"_rels/.rels" => 0,
n if n.starts_with("docProps/") => 1,
"xl/_rels/workbook.xml.rels" => 1,
"xl/workbook.xml" => 2,
"xl/_rels/workbook.xml.rels" => 3,
_ => 4,
n if n.starts_with("docProps/") => 4,
_ => 3,
};
let mut parts = std::mem::take(&mut out.parts);
parts.sort_by_key(|(n, _)| rank(n));