Copilot commented on code in PR #3044:
URL: https://github.com/apache/tika/pull/3044#discussion_r3871039342


##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.geogebra;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Property;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.StringUtils;
+
+/**
+ * SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}.
+ * <p>
+ * Extracts the document metadata from the {@code <geogebra>} root and its
+ * {@code <construction>} child (when asked to), and emits the user-visible
+ * text as XHTML paragraphs: the string literals of text object
+ * {@code <expression>}s, the text runs of {@code <content>} elements (inline
+ * text, tables, mind maps), element {@code <caption>}s and macro names and
+ * help texts.
+ */
+class GeoGebraXMLHandler extends DefaultHandler {
+
+    /**
+     * A GeoGebra string literal. GeoGebra writes strings between plain
+     * double quotes without any escaping, so a literal never contains one.
+     */
+    private static final Pattern STRING_LITERAL = 
Pattern.compile("\"([^\"]*)\"");
+
+    private final XHTMLContentHandler xhtml;
+    private final Metadata metadata;
+    private final boolean documentMetadata;
+    private int depth = 0;
+
+    /**
+     * @param xhtml            the handler paragraphs are written to
+     * @param metadata         the metadata tool names are added to
+     * @param documentMetadata whether to also fill the document metadata from
+     *                         the root and construction elements
+     */
+    GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata, boolean 
documentMetadata) {
+        this.xhtml = xhtml;
+        this.metadata = metadata;
+        this.documentMetadata = documentMetadata;
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, 
Attributes attributes)
+            throws SAXException {
+        if (depth == 0 && "geogebra".equals(localName)) {
+            if (documentMetadata) {
+                setIfNotBlank(GeoGebraParser.APP_NAME, 
attributes.getValue("app"));
+                setIfNotBlank(GeoGebraParser.APP_VERSION, 
attributes.getValue("version"));
+                setIfNotBlank(GeoGebraParser.FORMAT_VERSION, 
attributes.getValue("format"));
+                setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id"));
+            }
+        } else if (depth == 1 && "construction".equals(localName)) {

Review Comment:
   `startElement` matches element names using `localName` only. In SAX 
configurations where namespace processing is off, `localName` may be empty and 
`qName` holds the actual element name, which would cause the handler to miss 
all elements and extract no metadata/text. Fix by normalizing the element name 
(e.g., prefer `localName` when non-empty, otherwise use `qName`) before 
comparisons, or compare against both.



##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.geogebra;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Property;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.StringUtils;
+
+/**
+ * SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}.
+ * <p>
+ * Extracts the document metadata from the {@code <geogebra>} root and its
+ * {@code <construction>} child (when asked to), and emits the user-visible
+ * text as XHTML paragraphs: the string literals of text object
+ * {@code <expression>}s, the text runs of {@code <content>} elements (inline
+ * text, tables, mind maps), element {@code <caption>}s and macro names and
+ * help texts.
+ */
+class GeoGebraXMLHandler extends DefaultHandler {
+
+    /**
+     * A GeoGebra string literal. GeoGebra writes strings between plain
+     * double quotes without any escaping, so a literal never contains one.
+     */
+    private static final Pattern STRING_LITERAL = 
Pattern.compile("\"([^\"]*)\"");
+
+    private final XHTMLContentHandler xhtml;
+    private final Metadata metadata;
+    private final boolean documentMetadata;
+    private int depth = 0;
+
+    /**
+     * @param xhtml            the handler paragraphs are written to
+     * @param metadata         the metadata tool names are added to
+     * @param documentMetadata whether to also fill the document metadata from
+     *                         the root and construction elements
+     */
+    GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata, boolean 
documentMetadata) {
+        this.xhtml = xhtml;
+        this.metadata = metadata;
+        this.documentMetadata = documentMetadata;
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, 
Attributes attributes)
+            throws SAXException {
+        if (depth == 0 && "geogebra".equals(localName)) {
+            if (documentMetadata) {
+                setIfNotBlank(GeoGebraParser.APP_NAME, 
attributes.getValue("app"));
+                setIfNotBlank(GeoGebraParser.APP_VERSION, 
attributes.getValue("version"));
+                setIfNotBlank(GeoGebraParser.FORMAT_VERSION, 
attributes.getValue("format"));
+                setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id"));
+            }
+        } else if (depth == 1 && "construction".equals(localName)) {
+            //only the document's own construction; a macro's construction is
+            //nested one level deeper inside its <macro> element
+            if (documentMetadata) {
+                setIfNotBlank(TikaCoreProperties.TITLE, 
attributes.getValue("title"));
+                setIfNotBlank(TikaCoreProperties.CREATOR, 
attributes.getValue("author"));
+                setIfNotBlank(GeoGebraParser.DATE, 
attributes.getValue("date"));
+            }
+        } else if ("expression".equals(localName)) {
+            handleExpression(attributes.getValue("exp"));
+        } else if ("content".equals(localName)) {
+            handleContent(attributes.getValue("val"));
+        } else if ("caption".equals(localName)) {
+            paragraph(attributes.getValue("val"));
+        } else if ("macro".equals(localName)) {

Review Comment:
   `startElement` matches element names using `localName` only. In SAX 
configurations where namespace processing is off, `localName` may be empty and 
`qName` holds the actual element name, which would cause the handler to miss 
all elements and extract no metadata/text. Fix by normalizing the element name 
(e.g., prefer `localName` when non-empty, otherwise use `qName`) before 
comparisons, or compare against both.



##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java:
##########
@@ -0,0 +1,433 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.geogebra;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.BoundedInputStream;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.PageAnchoring;
+import org.apache.tika.metadata.PagedText;
+import org.apache.tika.metadata.Property;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.sax.EmbeddedContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.XMLReaderUtils;
+import org.apache.tika.zip.utils.ZipFileHelper;
+
+/**
+ * Parser for the zip-based GeoGebra formats: worksheets (*.ggb), Notes/Slides
+ * (*.ggs) and tools (*.ggt).
+ * <p>
+ * The construction metadata (title, author, date) and the application
+ * name/version are read from {@code geogebra.xml} (or, for a tool, from
+ * {@code geogebra_macro.xml}), and the user-visible text (text objects, inline
+ * text, captions, tool names and help) is emitted as XHTML paragraphs. For
+ * Notes/Slides, each {@code _slideN/geogebra.xml} becomes a
+ * {@code <div class="slide">}, in the order given by {@code structure.json}.
+ * <p>
+ * The representative rendering of the document, {@code geogebra_thumbnail.png}
+ * at the root of a worksheet or tool, or the first available slide thumbnail
+ * of a Notes/Slides file, is emitted as an embedded document marked with
+ * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL}, so that clients
+ * (e.g. the unpacker's sidecar metadata) can pick it as the preview image.
+ * Thumbnails of the remaining slides are renderings of content that is already
+ * extracted, so they are skipped. The document script
+ * {@code geogebra_javascript.js} is emitted as a
+ * {@link TikaCoreProperties.EmbeddedResourceType#MACRO}, and any other
+ * embedded file (e.g. inserted pictures) as an embedded document.
+ * <p>
+ * A part that cannot be read (an unsupported zip entry, malformed XML) is
+ * recorded in the metadata and skipped; the remaining parts are still parsed.
+ */
+@TikaComponent(name = "geogebra-parser")
+public class GeoGebraParser implements Parser {
+
+    /**
+     * Serial version UID
+     */
+    private static final long serialVersionUID = 2114923339149498692L;
+
+    public static final String GEOGEBRA_PREFIX = "geogebra:";
+
+    /**
+     * The GeoGebra application flavor the file was written with,
+     * e.g. "classic", "notes", "graphing".
+     */
+    public static final Property APP_NAME =
+            Property.internalText(GEOGEBRA_PREFIX + "app-name");
+
+    /**
+     * The GeoGebra application version the file was written with.
+     */
+    public static final Property APP_VERSION =
+            Property.internalText(GEOGEBRA_PREFIX + "app-version");
+
+    /**
+     * The GeoGebra XML format version.
+     */
+    public static final Property FORMAT_VERSION =
+            Property.internalText(GEOGEBRA_PREFIX + "format-version");
+
+    /**
+     * The unique id GeoGebra assigns to the document.
+     */
+    public static final Property ID = Property.internalText(GEOGEBRA_PREFIX + 
"id");
+
+    /**
+     * The free-form date string of the construction. This is user-entered
+     * text, not necessarily a parseable date.
+     */
+    public static final Property DATE = Property.internalText(GEOGEBRA_PREFIX 
+ "date");
+
+    /**
+     * The tool names of the macros in a tool file (or in a worksheet with
+     * embedded macros). The name is the {@code toolName} attribute of the
+     * macro element.
+     */
+    public static final Property TOOL_NAME =
+            Property.internalTextBag(GEOGEBRA_PREFIX + "toolName");
+
+    private static final Set<MediaType> SUPPORTED_TYPES = 
Collections.unmodifiableSet(
+            new 
HashSet<>(Arrays.asList(MediaType.application("vnd.geogebra.file"),
+                    MediaType.application("vnd.geogebra.slides"),
+                    MediaType.application("vnd.geogebra.tool"))));
+
+    private static final String GEOGEBRA_XML = "geogebra.xml";
+    private static final String MACRO_XML = "geogebra_macro.xml";
+    private static final String STRUCTURE_JSON = "structure.json";
+    private static final String THUMBNAIL_PNG = "geogebra_thumbnail.png";
+    private static final String JAVASCRIPT_JS = "geogebra_javascript.js";
+
+    /**
+     * Housekeeping entries at the root or in a slide directory that carry no
+     * user content of their own. The XML files are parsed for text and the
+     * thumbnails handled separately.
+     */
+    private static final Set<String> HOUSEKEEPING_NAMES = 
Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(GEOGEBRA_XML, MACRO_XML, THUMBNAIL_PNG,
+                    "geogebra_defaults2d.xml", "geogebra_defaults3d.xml")));
+
+    private static final String SLIDE_DIR_PREFIX = "_slide";
+
+    private static final Pattern SLIDE_XML_PATTERN =
+            Pattern.compile("^(" + SLIDE_DIR_PREFIX + "\\d+)/" + 
Pattern.quote(GEOGEBRA_XML) + "$");
+
+    /**
+     * structure.json only lists chapters, pages and element ids; a real one is
+     * a few kilobytes.
+     */
+    private static final long MAX_STRUCTURE_JSON_LENGTH = 1024 * 1024;
+
+    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Override
+    public Set<MediaType> getSupportedTypes(ParseContext context) {
+        return SUPPORTED_TYPES;
+    }
+
+    @Override
+    public void parse(TikaInputStream tis, ContentHandler handler, Metadata 
metadata,
+                      ParseContext context) throws IOException, SAXException, 
TikaException {
+        EmbeddedDocumentExtractor embeddedDocumentExtractor =
+                EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+
+        ZipFile zipFile;
+        Object container = tis.getOpenContainer();
+        if (container instanceof ZipFile) {
+            zipFile = (ZipFile) container;
+        } else {
+            zipFile = ZipFileHelper.open(tis, null);
+            tis.setOpenContainer(zipFile);
+        }
+
+        XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, 
context);
+        xhtml.startDocument();
+        List<String> slideIds = getSlideIds(zipFile);
+        ZipArchiveEntry rootXml = zipFile.getEntry(GEOGEBRA_XML);
+        ZipArchiveEntry macroXml = zipFile.getEntry(MACRO_XML);
+        //document metadata comes from the first XML parsed: a worksheet's
+        //geogebra.xml, a tool's geogebra_macro.xml, or the first slide
+        boolean documentMetadataPending = true;
+        if (rootXml != null) {
+            documentMetadataPending = false;
+            parseGeoGebraXml(zipFile, rootXml, xhtml, metadata, true, context);
+        }
+        if (macroXml != null) {
+            //a worksheet with macros carries both XMLs; the macro one only
+            //contributes the tool names then, not the document metadata
+            parseGeoGebraXml(zipFile, macroXml, xhtml, metadata, 
documentMetadataPending, context);
+            documentMetadataPending = false;
+        }
+        Map<String, Integer> pageNumbers = new HashMap<>();
+        if (!slideIds.isEmpty()) {
+            metadata.set(PagedText.N_PAGES, slideIds.size());
+            int page = 1;
+            for (String slideId : slideIds) {
+                pageNumbers.put(slideId, page++);
+                xhtml.startElement("div", "class", "slide");
+                try {
+                    ZipArchiveEntry slideXml = zipFile.getEntry(slideId + "/" 
+ GEOGEBRA_XML);
+                    parseGeoGebraXml(zipFile, slideXml, xhtml, metadata, 
documentMetadataPending,
+                            context);
+                    documentMetadataPending = false;
+                } finally {
+                    xhtml.endElement("div");
+                }
+            }
+        }
+        handleThumbnail(zipFile, slideIds, xhtml, metadata, context, 
embeddedDocumentExtractor);
+        handleOtherEntries(zipFile, pageNumbers, xhtml, metadata, context,
+                embeddedDocumentExtractor);
+        xhtml.endDocument();

Review Comment:
   When `ZipFileHelper.open(tis, null)` is called, this method opens a 
`ZipFile` but doesn’t close it within `parse()`. If `TikaInputStream` isn’t 
closed promptly by a caller (or if this parser is used in a nonstandard 
lifecycle), this can leak file handles/native resources. Consider tracking 
whether the zip was opened here and ensuring it’s closed in a `finally` block 
(while still preserving the “reuse existing open container” behavior).



##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java:
##########
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.detect.zip;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Test case for detecting the zip-based GeoGebra formats by their contents.
+ * The streams are parsed without a resource name, so detection must rely on
+ * the zip entry names, not the *.ggb/*.ggs/*.ggt globs.
+ */
+public class GeoGebraDetectionTest extends TikaTest {
+
+    private List<Metadata> getRecursiveMetadataWithoutName(String fileName) 
throws Exception {
+        try (TikaInputStream tis = TikaInputStream.get(
+                getClass().getResourceAsStream("/test-documents/" + fileName), 
new Metadata())) {
+            return getRecursiveMetadata(tis, AUTO_DETECT_PARSER, new 
Metadata(),
+                    new ParseContext(), true);

Review Comment:
   `getResourceAsStream(...)` can return `null` if the test resource is 
missing/mispackaged, which would fail with a less-informative exception when 
constructing the `TikaInputStream`. Adding an explicit assertion that the 
resource stream is non-null (or using `Objects.requireNonNull` with a clear 
message) will make failures easier to diagnose.



##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.geogebra;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Property;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.StringUtils;
+
+/**
+ * SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}.
+ * <p>
+ * Extracts the document metadata from the {@code <geogebra>} root and its
+ * {@code <construction>} child (when asked to), and emits the user-visible
+ * text as XHTML paragraphs: the string literals of text object
+ * {@code <expression>}s, the text runs of {@code <content>} elements (inline
+ * text, tables, mind maps), element {@code <caption>}s and macro names and
+ * help texts.
+ */
+class GeoGebraXMLHandler extends DefaultHandler {
+
+    /**
+     * A GeoGebra string literal. GeoGebra writes strings between plain
+     * double quotes without any escaping, so a literal never contains one.
+     */
+    private static final Pattern STRING_LITERAL = 
Pattern.compile("\"([^\"]*)\"");
+
+    private final XHTMLContentHandler xhtml;
+    private final Metadata metadata;
+    private final boolean documentMetadata;
+    private int depth = 0;
+
+    /**
+     * @param xhtml            the handler paragraphs are written to
+     * @param metadata         the metadata tool names are added to
+     * @param documentMetadata whether to also fill the document metadata from
+     *                         the root and construction elements
+     */
+    GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata, boolean 
documentMetadata) {
+        this.xhtml = xhtml;
+        this.metadata = metadata;
+        this.documentMetadata = documentMetadata;
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, 
Attributes attributes)
+            throws SAXException {
+        if (depth == 0 && "geogebra".equals(localName)) {
+            if (documentMetadata) {
+                setIfNotBlank(GeoGebraParser.APP_NAME, 
attributes.getValue("app"));
+                setIfNotBlank(GeoGebraParser.APP_VERSION, 
attributes.getValue("version"));
+                setIfNotBlank(GeoGebraParser.FORMAT_VERSION, 
attributes.getValue("format"));
+                setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id"));
+            }
+        } else if (depth == 1 && "construction".equals(localName)) {
+            //only the document's own construction; a macro's construction is
+            //nested one level deeper inside its <macro> element
+            if (documentMetadata) {
+                setIfNotBlank(TikaCoreProperties.TITLE, 
attributes.getValue("title"));
+                setIfNotBlank(TikaCoreProperties.CREATOR, 
attributes.getValue("author"));
+                setIfNotBlank(GeoGebraParser.DATE, 
attributes.getValue("date"));
+            }
+        } else if ("expression".equals(localName)) {
+            handleExpression(attributes.getValue("exp"));
+        } else if ("content".equals(localName)) {
+            handleContent(attributes.getValue("val"));
+        } else if ("caption".equals(localName)) {
+            paragraph(attributes.getValue("val"));
+        } else if ("macro".equals(localName)) {
+            String toolName = attributes.getValue("toolName");
+            if (StringUtils.isBlank(toolName)) {
+                toolName = attributes.getValue("cmdName");
+            }
+            if (!StringUtils.isBlank(toolName)) {
+                metadata.add(GeoGebraParser.TOOL_NAME, toolName.trim());
+            }
+            paragraph(toolName);
+            paragraph(attributes.getValue("toolHelp"));
+        }
+        depth++;
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) {
+        depth--;
+    }
+
+    /**
+     * Emits the string literals of an expression. A text object's expression
+     * is either a single literal like {@code "some text"} or, for a dynamic
+     * text, literals combined with values like {@code "Area = " + a}; the
+     * literals are the user's text, everything else is geometry and skipped.
+     */
+    private void handleExpression(String exp) throws SAXException {
+        if (exp == null || exp.indexOf('"') < 0) {
+            return;
+        }
+        StringBuilder sb = new StringBuilder();
+        Matcher m = STRING_LITERAL.matcher(exp);
+        while (m.find()) {
+            sb.append(m.group(1));
+        }
+        paragraph(sb.toString());
+    }
+
+    /**
+     * Emits the text runs of a rich-text {@code content} value, a JSON array
+     * of text runs like {@code [{"text":"Hello\n"}]}. All {@code text} fields
+     * are collected recursively (tables and mind maps nest them), joined, and
+     * emitted one paragraph per line.
+     */
+    private void handleContent(String val) throws SAXException {
+        if (val == null) {
+            return;
+        }
+        String trimmed = val.trim();
+        if (trimmed.isEmpty() || (trimmed.charAt(0) != '[' && 
trimmed.charAt(0) != '{')) {
+            //not a JSON document; a plain string carries no text runs
+            return;
+        }
+        JsonNode root;
+        try {
+            root = GeoGebraParser.OBJECT_MAPPER.readTree(trimmed);
+        } catch (IOException e) {
+            return;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (String text : root.findValuesAsText("text")) {
+            sb.append(text);
+        }
+        for (String line : sb.toString().split("\r\n|[\r\n]")) {
+            paragraph(line);
+        }
+    }

Review Comment:
   This parses the `content@val` attribute as a full in-memory `JsonNode` tree 
without any size/complexity guard. A crafted file can place an extremely large 
JSON payload in the XML attribute, causing excessive memory usage (tree model + 
concatenation into `StringBuilder`) and potential DoS. Consider adding a hard 
cap (e.g., skip JSON parsing when `trimmed.length()` exceeds a reasonable 
limit), and/or using Jackson streaming (`JsonParser`) to only collect `"text"` 
fields without building the full tree.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to