📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-23 16:03:00 +00:00
parent c4c6a41c21
commit 59e15f8999
557 changed files with 10501 additions and 3168 deletions
@@ -1,6 +1,6 @@
{
"name": "antigravity-bundle-documents-presentations",
"version": "13.1.0",
"version": "13.1.1",
"description": "Editorial \"Documents & Presentations\" bundle for Claude Code from Antigravity Awesome Skills.",
"author": {
"name": "sickn33 and contributors",
@@ -1,6 +1,6 @@
{
"name": "agyb-documents-presentations",
"version": "13.1.0",
"version": "13.1.1",
"description": "Install the \"Documents & Presentations\" editorial skill bundle from Antigravity Awesome Skills.",
"author": {
"name": "sickn33 and contributors",
@@ -16,6 +16,17 @@ import zipfile
from pathlib import Path
def validate_input_tree(input_dir: Path):
root = input_dir.resolve(strict=True)
for path in input_dir.rglob("*"):
if path.is_symlink():
raise ValueError(f"Refusing to pack symlink: {path}")
try:
path.resolve(strict=True).relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Refusing to pack path outside input directory: {path}") from None
def main():
parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
parser.add_argument("input_directory", help="Unpacked Office document directory")
@@ -60,6 +71,7 @@ def pack_document(input_dir, output_file, validate=False):
raise ValueError(f"{input_dir} is not a directory")
if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
validate_input_tree(input_dir)
# Work in temporary directory to avoid modifying original
with tempfile.TemporaryDirectory() as temp_dir:
@@ -8,6 +8,11 @@ import sys
import zipfile
from pathlib import Path
MAX_ARCHIVE_MEMBERS = 5000
MAX_MEMBER_SIZE = 100 * 1024 * 1024
MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024
MAX_COMPRESSION_RATIO = 1000
def _is_zip_symlink(member: zipfile.ZipInfo) -> bool:
return stat.S_ISLNK(member.external_attr >> 16)
@@ -29,19 +34,35 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro
shutil.copyfileobj(source, target)
def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path):
members = archive.infolist()
if len(members) > MAX_ARCHIVE_MEMBERS:
raise ValueError("Archive contains too many entries")
total_size = 0
for member in members:
if _is_zip_symlink(member):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if not _is_safe_destination(output_root, member.filename):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if member.file_size > MAX_MEMBER_SIZE:
raise ValueError(f"Archive entry too large: {member.filename}")
total_size += member.file_size
if total_size > MAX_TOTAL_UNCOMPRESSED:
raise ValueError("Archive uncompressed size is too large")
if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO:
raise ValueError(f"Archive entry compression ratio too high: {member.filename}")
return members
def extract_archive_safely(input_file: str | Path, output_dir: str | Path):
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
output_root = output_path.resolve()
with zipfile.ZipFile(input_file) as archive:
for member in archive.infolist():
if _is_zip_symlink(member):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if not _is_safe_destination(output_root, member.filename):
raise ValueError(f"Unsafe archive entry: {member.filename}")
for member in archive.infolist():
for member in _validate_archive_members(archive, output_root):
_extract_member(archive, member, output_path)
@@ -3,11 +3,37 @@ Base validator with common validation logic for document files.
"""
import re
import shutil
from pathlib import Path
import lxml.etree
def hardened_xml_parser():
return lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False)
def parse_xml(source, **kwargs):
return lxml.etree.parse(source, parser=hardened_xml_parser(), **kwargs)
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class BaseSchemaValidator:
"""Base validator with common validation logic for document files."""
@@ -131,7 +157,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
# Try to parse the XML file
lxml.etree.parse(str(xml_file))
parse_xml(str(xml_file))
except lxml.etree.XMLSyntaxError as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
@@ -159,7 +185,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
for attr_val in [
@@ -190,7 +216,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
file_ids = {} # Track IDs that must be unique within this file
# Remove all mc:AlternateContent elements from the tree
@@ -310,7 +336,7 @@ class BaseSchemaValidator:
for rels_file in rels_files:
try:
# Parse relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
# Get the directory where this .rels file is located
rels_dir = rels_file.parent
@@ -411,7 +437,7 @@ class BaseSchemaValidator:
try:
# Parse the .rels file to get valid relationship IDs and their types
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
rid_to_type = {}
for rel in rels_root.findall(
@@ -434,7 +460,7 @@ class BaseSchemaValidator:
rid_to_type[rid] = type_name
# Parse the XML file to find all r:id references
xml_root = lxml.etree.parse(str(xml_file)).getroot()
xml_root = parse_xml(str(xml_file)).getroot()
# Find all elements with r:id attributes
for elem in xml_root.iter():
@@ -531,7 +557,7 @@ class BaseSchemaValidator:
try:
# Parse and get all declared parts and extensions
root = lxml.etree.parse(str(content_types_file)).getroot()
root = parse_xml(str(content_types_file)).getroot()
declared_parts = set()
declared_extensions = set()
@@ -593,7 +619,7 @@ class BaseSchemaValidator:
continue
try:
root_tag = lxml.etree.parse(str(xml_file)).getroot().tag
root_tag = parse_xml(str(xml_file)).getroot().tag
root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
if root_name in declarable_roots and path_str not in declared_parts:
@@ -832,15 +858,12 @@ class BaseSchemaValidator:
try:
# Load schema
with open(schema_path, "rb") as xsd_file:
parser = lxml.etree.XMLParser()
xsd_doc = lxml.etree.parse(
xsd_file, parser=parser, base_url=str(schema_path)
)
xsd_doc = parse_xml(xsd_file, base_url=str(schema_path))
schema = lxml.etree.XMLSchema(xsd_doc)
# Load and preprocess XML
with open(xml_file, "r") as f:
xml_doc = lxml.etree.parse(f)
xml_doc = parse_xml(f)
xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
@@ -888,7 +911,7 @@ class BaseSchemaValidator:
# Extract original file
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_path)
safe_extract_all(zip_ref, temp_path)
# Find corresponding file in original
original_xml_file = temp_path / relative_path
@@ -3,12 +3,31 @@ Validator for Word document XML files against XSD schemas.
"""
import re
import shutil
import tempfile
import zipfile
from pathlib import Path
import lxml.etree
from .base import BaseSchemaValidator
from .base import BaseSchemaValidator, parse_xml
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class DOCXSchemaValidator(BaseSchemaValidator):
@@ -81,7 +100,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Find all w:t elements
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
@@ -134,7 +153,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Find all w:t elements that are descendants of w:del elements
namespaces = {"w": self.WORD_2006_NAMESPACE}
@@ -180,7 +199,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
@@ -198,11 +217,11 @@ class DOCXSchemaValidator(BaseSchemaValidator):
with tempfile.TemporaryDirectory() as temp_dir:
# Unpack original docx
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_dir)
safe_extract_all(zip_ref, temp_dir)
# Parse document.xml
doc_xml_path = temp_dir + "/word/document.xml"
root = lxml.etree.parse(doc_xml_path).getroot()
root = parse_xml(doc_xml_path).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
@@ -225,7 +244,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
namespaces = {"w": self.WORD_2006_NAMESPACE}
# Find w:delText in w:ins that are NOT within w:del
@@ -4,7 +4,7 @@ Validator for PowerPoint presentation XML files against XSD schemas.
import re
from .base import BaseSchemaValidator
from .base import BaseSchemaValidator, parse_xml
class PPTXSchemaValidator(BaseSchemaValidator):
@@ -86,7 +86,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Check all elements for ID attributes
for elem in root.iter():
@@ -142,7 +142,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for slide_master in slide_masters:
try:
# Parse the slide master file
root = lxml.etree.parse(str(slide_master)).getroot()
root = parse_xml(str(slide_master)).getroot()
# Find the corresponding _rels file for this slide master
rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
@@ -155,7 +155,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
continue
# Parse the relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
# Build a set of valid relationship IDs that point to slide layouts
valid_layout_rids = set()
@@ -209,7 +209,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for rels_file in slide_rels_files:
try:
root = lxml.etree.parse(str(rels_file)).getroot()
root = parse_xml(str(rels_file)).getroot()
# Find all slideLayout relationships
layout_rels = [
@@ -258,7 +258,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for rels_file in slide_rels_files:
try:
# Parse the relationships file
root = lxml.etree.parse(str(rels_file)).getroot()
root = parse_xml(str(rels_file)).getroot()
# Find all notesSlide relationships
for rel in root.findall(
@@ -2,11 +2,31 @@
Validator for tracked changes in Word documents.
"""
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from defusedxml import ElementTree as ET
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class RedliningValidator:
"""Validator for tracked changes in Word documents."""
@@ -29,8 +49,6 @@ class RedliningValidator:
# First, check if there are any tracked changes by Claude to validate
try:
import xml.etree.ElementTree as ET
tree = ET.parse(modified_file)
root = tree.getroot()
@@ -67,7 +85,7 @@ class RedliningValidator:
# Unpack original docx
try:
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
zip_ref.extractall(temp_path)
safe_extract_all(zip_ref, temp_path)
except Exception as e:
print(f"FAILED - Error unpacking original docx: {e}")
return False
@@ -81,8 +99,6 @@ class RedliningValidator:
# Parse both XML files using xml.etree.ElementTree for redlining validation
try:
import xml.etree.ElementTree as ET
modified_tree = ET.parse(modified_file)
modified_root = modified_tree.getroot()
original_tree = ET.parse(original_file)
@@ -16,6 +16,17 @@ import zipfile
from pathlib import Path
def validate_input_tree(input_dir: Path):
root = input_dir.resolve(strict=True)
for path in input_dir.rglob("*"):
if path.is_symlink():
raise ValueError(f"Refusing to pack symlink: {path}")
try:
path.resolve(strict=True).relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Refusing to pack path outside input directory: {path}") from None
def main():
parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
parser.add_argument("input_directory", help="Unpacked Office document directory")
@@ -60,6 +71,7 @@ def pack_document(input_dir, output_file, validate=False):
raise ValueError(f"{input_dir} is not a directory")
if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
validate_input_tree(input_dir)
# Work in temporary directory to avoid modifying original
with tempfile.TemporaryDirectory() as temp_dir:
@@ -8,6 +8,11 @@ import sys
import zipfile
from pathlib import Path
MAX_ARCHIVE_MEMBERS = 5000
MAX_MEMBER_SIZE = 100 * 1024 * 1024
MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024
MAX_COMPRESSION_RATIO = 1000
def _is_zip_symlink(member: zipfile.ZipInfo) -> bool:
return stat.S_ISLNK(member.external_attr >> 16)
@@ -29,19 +34,35 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro
shutil.copyfileobj(source, target)
def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path):
members = archive.infolist()
if len(members) > MAX_ARCHIVE_MEMBERS:
raise ValueError("Archive contains too many entries")
total_size = 0
for member in members:
if _is_zip_symlink(member):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if not _is_safe_destination(output_root, member.filename):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if member.file_size > MAX_MEMBER_SIZE:
raise ValueError(f"Archive entry too large: {member.filename}")
total_size += member.file_size
if total_size > MAX_TOTAL_UNCOMPRESSED:
raise ValueError("Archive uncompressed size is too large")
if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO:
raise ValueError(f"Archive entry compression ratio too high: {member.filename}")
return members
def extract_archive_safely(input_file: str | Path, output_dir: str | Path):
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
output_root = output_path.resolve()
with zipfile.ZipFile(input_file) as archive:
for member in archive.infolist():
if _is_zip_symlink(member):
raise ValueError(f"Unsafe archive entry: {member.filename}")
if not _is_safe_destination(output_root, member.filename):
raise ValueError(f"Unsafe archive entry: {member.filename}")
for member in archive.infolist():
for member in _validate_archive_members(archive, output_root):
_extract_member(archive, member, output_path)
@@ -3,11 +3,37 @@ Base validator with common validation logic for document files.
"""
import re
import shutil
from pathlib import Path
import lxml.etree
def hardened_xml_parser():
return lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False)
def parse_xml(source, **kwargs):
return lxml.etree.parse(source, parser=hardened_xml_parser(), **kwargs)
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class BaseSchemaValidator:
"""Base validator with common validation logic for document files."""
@@ -131,7 +157,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
# Try to parse the XML file
lxml.etree.parse(str(xml_file))
parse_xml(str(xml_file))
except lxml.etree.XMLSyntaxError as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
@@ -159,7 +185,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
for attr_val in [
@@ -190,7 +216,7 @@ class BaseSchemaValidator:
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
file_ids = {} # Track IDs that must be unique within this file
# Remove all mc:AlternateContent elements from the tree
@@ -310,7 +336,7 @@ class BaseSchemaValidator:
for rels_file in rels_files:
try:
# Parse relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
# Get the directory where this .rels file is located
rels_dir = rels_file.parent
@@ -411,7 +437,7 @@ class BaseSchemaValidator:
try:
# Parse the .rels file to get valid relationship IDs and their types
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
rid_to_type = {}
for rel in rels_root.findall(
@@ -434,7 +460,7 @@ class BaseSchemaValidator:
rid_to_type[rid] = type_name
# Parse the XML file to find all r:id references
xml_root = lxml.etree.parse(str(xml_file)).getroot()
xml_root = parse_xml(str(xml_file)).getroot()
# Find all elements with r:id attributes
for elem in xml_root.iter():
@@ -531,7 +557,7 @@ class BaseSchemaValidator:
try:
# Parse and get all declared parts and extensions
root = lxml.etree.parse(str(content_types_file)).getroot()
root = parse_xml(str(content_types_file)).getroot()
declared_parts = set()
declared_extensions = set()
@@ -593,7 +619,7 @@ class BaseSchemaValidator:
continue
try:
root_tag = lxml.etree.parse(str(xml_file)).getroot().tag
root_tag = parse_xml(str(xml_file)).getroot().tag
root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
if root_name in declarable_roots and path_str not in declared_parts:
@@ -832,15 +858,12 @@ class BaseSchemaValidator:
try:
# Load schema
with open(schema_path, "rb") as xsd_file:
parser = lxml.etree.XMLParser()
xsd_doc = lxml.etree.parse(
xsd_file, parser=parser, base_url=str(schema_path)
)
xsd_doc = parse_xml(xsd_file, base_url=str(schema_path))
schema = lxml.etree.XMLSchema(xsd_doc)
# Load and preprocess XML
with open(xml_file, "r") as f:
xml_doc = lxml.etree.parse(f)
xml_doc = parse_xml(f)
xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
@@ -888,7 +911,7 @@ class BaseSchemaValidator:
# Extract original file
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_path)
safe_extract_all(zip_ref, temp_path)
# Find corresponding file in original
original_xml_file = temp_path / relative_path
@@ -3,12 +3,31 @@ Validator for Word document XML files against XSD schemas.
"""
import re
import shutil
import tempfile
import zipfile
from pathlib import Path
import lxml.etree
from .base import BaseSchemaValidator
from .base import BaseSchemaValidator, parse_xml
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class DOCXSchemaValidator(BaseSchemaValidator):
@@ -81,7 +100,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Find all w:t elements
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
@@ -134,7 +153,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Find all w:t elements that are descendants of w:del elements
namespaces = {"w": self.WORD_2006_NAMESPACE}
@@ -180,7 +199,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
@@ -198,11 +217,11 @@ class DOCXSchemaValidator(BaseSchemaValidator):
with tempfile.TemporaryDirectory() as temp_dir:
# Unpack original docx
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_dir)
safe_extract_all(zip_ref, temp_dir)
# Parse document.xml
doc_xml_path = temp_dir + "/word/document.xml"
root = lxml.etree.parse(doc_xml_path).getroot()
root = parse_xml(doc_xml_path).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
@@ -225,7 +244,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
namespaces = {"w": self.WORD_2006_NAMESPACE}
# Find w:delText in w:ins that are NOT within w:del
@@ -4,7 +4,7 @@ Validator for PowerPoint presentation XML files against XSD schemas.
import re
from .base import BaseSchemaValidator
from .base import BaseSchemaValidator, parse_xml
class PPTXSchemaValidator(BaseSchemaValidator):
@@ -86,7 +86,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
root = parse_xml(str(xml_file)).getroot()
# Check all elements for ID attributes
for elem in root.iter():
@@ -142,7 +142,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for slide_master in slide_masters:
try:
# Parse the slide master file
root = lxml.etree.parse(str(slide_master)).getroot()
root = parse_xml(str(slide_master)).getroot()
# Find the corresponding _rels file for this slide master
rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
@@ -155,7 +155,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
continue
# Parse the relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rels_root = parse_xml(str(rels_file)).getroot()
# Build a set of valid relationship IDs that point to slide layouts
valid_layout_rids = set()
@@ -209,7 +209,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for rels_file in slide_rels_files:
try:
root = lxml.etree.parse(str(rels_file)).getroot()
root = parse_xml(str(rels_file)).getroot()
# Find all slideLayout relationships
layout_rels = [
@@ -258,7 +258,7 @@ class PPTXSchemaValidator(BaseSchemaValidator):
for rels_file in slide_rels_files:
try:
# Parse the relationships file
root = lxml.etree.parse(str(rels_file)).getroot()
root = parse_xml(str(rels_file)).getroot()
# Find all notesSlide relationships
for rel in root.findall(
@@ -2,11 +2,31 @@
Validator for tracked changes in Word documents.
"""
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from defusedxml import ElementTree as ET
def safe_extract_all(zip_ref, destination):
"""Extract a zip archive without allowing members to escape destination."""
destination = Path(destination).resolve()
for member in zip_ref.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise ValueError(f"Unsafe archive member: {member.filename}") from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zip_ref.open(member) as src, target.open("wb") as dst:
shutil.copyfileobj(src, dst)
class RedliningValidator:
"""Validator for tracked changes in Word documents."""
@@ -29,8 +49,6 @@ class RedliningValidator:
# First, check if there are any tracked changes by Claude to validate
try:
import xml.etree.ElementTree as ET
tree = ET.parse(modified_file)
root = tree.getroot()
@@ -67,7 +85,7 @@ class RedliningValidator:
# Unpack original docx
try:
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
zip_ref.extractall(temp_path)
safe_extract_all(zip_ref, temp_path)
except Exception as e:
print(f"FAILED - Error unpacking original docx: {e}")
return False
@@ -81,8 +99,6 @@ class RedliningValidator:
# Parse both XML files using xml.etree.ElementTree for redlining validation
try:
import xml.etree.ElementTree as ET
modified_tree = ET.parse(modified_file)
modified_root = modified_tree.getroot()
original_tree = ET.parse(original_file)