mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
XLSX: [Content_Types].xml is the first zip entry, workbook parts precede document properties (#1090)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
84acf6293b
commit
48696f9bcf
@@ -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) |
|
||||
|
||||
@@ -18,6 +18,7 @@ mod smooth_roundtrip;
|
||||
mod trendline_roundtrip;
|
||||
mod xls_tests;
|
||||
mod xlsb_tests;
|
||||
mod zip_order;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user