#3692: a glossary is written from the model when the privacy settings ask for changes

Finding 72. An unchanged glossary was carried byte for byte, judged by a
fingerprint of its entries that leaves out the document's privacy settings,
so a save with "remove personal information" or "remove date and time" kept
the authors and dates of the glossary's tracked changes. With either setting
on, the glossary is now written from the model, which applies them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin Ball
2026-10-22 16:06:09 +01:00
co-authored by Claude Opus 5.5
parent 23b9721720
commit 01c78899ce
2 changed files with 32 additions and 1 deletions
+4 -1
View File
@@ -268,7 +268,10 @@ pub(crate) fn write_parts(doc: &Document, flavor: Flavor) -> Result<Vec<(String,
// The glossary: kept verbatim while its entries are as read and its parts were kept, else
// written from the model (before the notes: references its entries hold are collected too).
if glossary::edited(doc) || (!doc.glossary.is_empty() && !wr.embeds.kept_glossary()) {
// Also written from the model when the privacy settings ask for changes: kept bytes would
// carry the names and dates of its tracked changes (#3692).
let private = doc.settings.remove_personal_info || doc.settings.remove_date_time;
if private || glossary::edited(doc) || (!doc.glossary.is_empty() && !wr.embeds.kept_glossary()) {
wr.embeds.drop_glossary = true;
wr.glossary(&mut entries, &mut overrides, &mut rels);
}
+28
View File
@@ -234,6 +234,34 @@ fn edited_glossary_is_written_from_the_model() {
assert_eq!(twice.get(&target), files.get(&target));
}
#[test]
fn an_unchanged_glossary_follows_the_privacy_settings() {
// An untouched glossary was written back byte for byte, so the names and dates of its
// tracked changes survived a save that removes personal information (#3692).
let tracked = r#"<w:docPart><w:docPartPr><w:name w:val="Clause"/><w:category><w:name w:val="General"/><w:gallery w:val="docParts"/></w:category></w:docPartPr><w:docPartBody><w:p><w:ins w:id="1" w:author="Quill Ramsey" w:date="2026-03-04T05:06:07Z"><w:r><w:t>Added clause</w:t></w:r></w:ins></w:p></w:docPartBody></w:docPart>"#;
let bytes = package(
&[("rId7", GLOSSARY_REL, "glossary/document.xml")],
&[
("[Content_Types].xml", content_types(&[("/word/glossary/document.xml", GLOSSARY_CT)])),
("word/glossary/document.xml", glossary_xml(tracked)),
],
);
let doc = wordcraft_docx::read(&bytes).unwrap();
let as_read = save(&doc);
let kept = text(&as_read, &glossary_target(&as_read).expect("a glossary"));
assert!(kept.contains("Quill Ramsey"), "the fixture's glossary is kept verbatim: {kept}");
for (personal, dates) in [(true, false), (false, true), (true, true)] {
let mut private = doc.clone();
private.settings.remove_personal_info = personal;
private.settings.remove_date_time = dates;
let files = save(&private);
let g = text(&files, &glossary_target(&files).expect("a glossary"));
assert!(g.contains("Added clause"), "{g}");
assert_eq!(g.contains("Quill Ramsey"), !personal, "{g}");
assert_eq!(g.contains("2026-03-04T05:06:07Z"), !dates, "{g}");
}
}
/// The relationships part of a part in `word/`.
fn glossary_rels(part: &str) -> String {
let (dir, file) = part.rsplit_once('/').unwrap();