All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.robtimus.obfuscation.xml.WritingObfuscatingXMLParser Maven / Gradle / Ivy

The newest version!
/*
 * WritingObfuscatingXMLParser.java
 * Copyright 2022 Rob Spoor
 *
 * Licensed 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 com.github.robtimus.obfuscation.xml;

import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.skipLeadingWhitespace;
import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.skipTrailingWhitespace;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import org.codehaus.stax2.DTDInfo;
import org.codehaus.stax2.XMLStreamReader2;
import com.github.robtimus.obfuscation.xml.XMLObfuscator.ElementConfigurer.ObfuscationMode;

//Do not implement XMLStreamParser, the mechanism is too different
final class WritingObfuscatingXMLParser {

    private final XMLStreamReader xmlStreamReader;
    private final XMLStreamWriter xmlStreamWriter;

    private final Map elements;
    private final Map qualifiedElements;
    private final Map attributes;
    private final Map qualifiedAttributes;

    private final Deque currentElements = new ArrayDeque<>();
    private final StringBuilder currentText = new StringBuilder();
    private TextType currentTextType = TextType.NONE;
    private boolean obfuscateCurrentText;

    WritingObfuscatingXMLParser(XMLStreamReader xmlStreamReader, XMLStreamWriter xmlStreamWriter,
            Map elements, Map qualifiedElements,
            Map attributes, Map qualifiedAttributes) {

        this.xmlStreamReader = xmlStreamReader;
        this.xmlStreamWriter = xmlStreamWriter;
        this.elements = elements;
        this.qualifiedElements = qualifiedElements;
        this.attributes = attributes;
        this.qualifiedAttributes = qualifiedAttributes;
    }

    void initialize() throws XMLStreamException {
        startDocument();
    }

    boolean hasNext() throws XMLStreamException {
        return xmlStreamReader.hasNext();
    }

    void processNext() throws XMLStreamException {
        int event = nextEvent();
        switch (event) {
            case XMLStreamConstants.START_ELEMENT:
                startElement();
                break;
            case XMLStreamConstants.END_ELEMENT:
                endElement();
                break;
            case XMLStreamConstants.PROCESSING_INSTRUCTION:
                processingInstruction();
                break;
            case XMLStreamConstants.CHARACTERS:
                characters();
                break;
            case XMLStreamConstants.COMMENT:
                comment();
                break;
            case XMLStreamConstants.SPACE:
                space();
                break;
            // case XMLStreamConstants.START_DOCUMENT handled in initialize
            case XMLStreamConstants.END_DOCUMENT:
                endDocument();
                break;
            // case XMLStreamConstants.ENTITY_REFERENCE not supported; entities are resolved
            // case XMLStreamConstants.ATTRIBUTE handled in startElement
            case XMLStreamConstants.DTD:
                dtd();
                break;
            case XMLStreamConstants.CDATA:
                cdata();
                break;
            // case XMLStreamConstants.NAMESPACE handled in startElement
            // case XMLStreamConstants.NOTATION_DECLARATION not supported
            // case XMLStreamConstants.ENTITY_DECLARATION not supported
            default:
                break;
        }
    }

    private int nextEvent() throws XMLStreamException {
        try {
            int event = xmlStreamReader.next();
            if (event != XMLStreamConstants.CHARACTERS && event != XMLStreamConstants.CDATA) {
                finishLatestText();
            }
            return event;

        } catch (XMLStreamException e) {
            finishLatestText();
            throw e;
        }
    }

    private void startElement() throws XMLStreamException {
        QName name = xmlStreamReader.getName();

        ObfuscatedElement currentElement = currentElements.peekLast();
        if (currentElement == null || currentElement.allowsOverriding()) {
            // either not obfuscating any element, or the element allows overriding obfuscation - check the element itself
            ElementConfig config = configForElement(name);
            if (config != null) {
                currentElement = new ObfuscatedElement(config);
                currentElements.addLast(currentElement);
                currentElement.depth++;
            } else if (currentElement != null) {
                currentElement.depth++;
            }
        } else {
            // nested in an element that's being obfuscated
            currentElement.depth++;
        }

        xmlStreamWriter.writeStartElement(name.getPrefix(), name.getLocalPart(), name.getNamespaceURI());

        writeAttributes(name);
    }

    private void writeAttributes(QName elementName) throws XMLStreamException {
        int attributeCount = xmlStreamReader.getAttributeCount();
        for (int i = 0; i < attributeCount; i++) {
            QName attributeName = xmlStreamReader.getAttributeName(i);
            String attributeValue = xmlStreamReader.getAttributeValue(i);

            AttributeConfig attributeConfig = configForAttribute(attributeName);
            if (attributeConfig != null) {
                attributeValue = attributeConfig.obfuscator(elementName).obfuscateText(attributeValue).toString();
            }
            xmlStreamWriter.writeAttribute(attributeName.getPrefix(), attributeName.getNamespaceURI(), attributeName.getLocalPart(), attributeValue);
        }
    }

    private ElementConfig configForElement(QName elementName) {
        ElementConfig config = qualifiedElements.get(elementName);
        if (config == null) {
            config = elements.get(elementName.getLocalPart());
        }
        return config;
    }

    private AttributeConfig configForAttribute(QName attributeName) {
        AttributeConfig config = qualifiedAttributes.get(attributeName);
        if (config == null) {
            config = attributes.get(attributeName.getLocalPart());
        }
        return config;
    }

    private void endElement() throws XMLStreamException {
        xmlStreamWriter.writeEndElement();

        if (!currentElements.isEmpty()) {
            ObfuscatedElement currentElement = currentElements.getLast();
            currentElement.depth--;
            if (currentElement.depth == 0) {
                // done with the element
                currentElements.removeLast();
            }
            // else nested in an element that's being obfuscated
        }
        // else currently no element is being obfuscated
    }

    private void processingInstruction() throws XMLStreamException {
        String target = xmlStreamReader.getPITarget();
        String data = xmlStreamReader.getPIData();
        if (data == null || data.isEmpty()) {
            xmlStreamWriter.writeProcessingInstruction(target);
        } else {
            xmlStreamWriter.writeProcessingInstruction(target, data);
        }
    }

    private void characters() throws XMLStreamException {
        if (currentTextType != TextType.CHARACTERS) {
            // including CDATA -> CHARACTERS
            finishLatestText();
        }

        String text = xmlStreamReader.getText();
        if (currentElements.isEmpty()) {
            // not obfuscating anything
            xmlStreamWriter.writeCharacters(text);
            return;
        }
        ObfuscatedElement currentElement = currentElements.getLast();
        if (!currentElement.obfuscateNestedElements() && currentElement.depth != 1) {
            // nested inside an element that is configured to not have nested elements obfuscated, don't obfuscate
            xmlStreamWriter.writeCharacters(text);
            return;
        }
        if (!currentElement.config.performObfuscation) {
            // the obfuscator is Obfuscator.none(), which means we don't need to obfuscate
            xmlStreamWriter.writeCharacters(text);
            return;
        }
        // the text should be obfuscated, but not at this time
        currentText.append(text);
        currentTextType = TextType.CHARACTERS;
        obfuscateCurrentText = true;
    }

    private void comment() throws XMLStreamException {
        String comment = xmlStreamReader.getText();
        xmlStreamWriter.writeComment(comment);
    }

    private void space() throws XMLStreamException {
        String space = xmlStreamReader.getText();
        xmlStreamWriter.writeCharacters(space);
    }

    private void startDocument() throws XMLStreamException {
        String encoding = xmlStreamReader.getCharacterEncodingScheme();
        String version = xmlStreamReader.getVersion();
        xmlStreamWriter.writeStartDocument(encoding, version);
    }

    private void endDocument() throws XMLStreamException {
        // write the end document even if the destination's limit has been reached
        xmlStreamWriter.writeEndDocument();
    }

    private void dtd() throws XMLStreamException {
        // Woodstox returns the internal subset, but expects a full DTD - build it ourselves
        String dtd = getDTD((XMLStreamReader2) xmlStreamReader);
        xmlStreamWriter.writeDTD(dtd);
    }

    @SuppressWarnings("nls")
    static String getDTD(XMLStreamReader2 xmlStreamReader) throws XMLStreamException {
        DTDInfo dtdInfo = xmlStreamReader.getDTDInfo();
        String publicId = dtdInfo.getDTDPublicId();
        String systemId = dtdInfo.getDTDSystemId();
        String rootName = dtdInfo.getDTDRootName();
        String internalSubset = dtdInfo.getDTDInternalSubset();

        StringBuilder dtd = new StringBuilder()
                .append("');
        return dtd.toString();
    }

    private void cdata() throws XMLStreamException {
        if (currentTextType != TextType.CDATA) {
            // including CHARACTERS -> CDATA
            finishLatestText();
        }

        // always capture all CDATA text, to prevent multiple CDATA blocks after each other if the text becomes too large
        currentText.append(xmlStreamReader.getText());
        currentTextType = TextType.CDATA;

        if (currentElements.isEmpty()) {
            // not obfuscating anything
            obfuscateCurrentText = false;
            return;
        }
        ObfuscatedElement currentElement = currentElements.getLast();
        if (!currentElement.obfuscateNestedElements() && currentElement.depth != 1) {
            // nested inside an element that is configured to not have nested elements obfuscated, don't obfuscate
            obfuscateCurrentText = false;
            return;
        }
        if (!currentElement.config.performObfuscation) {
            // the obfuscator is Obfuscator.none(), which means we don't need to obfuscate
            obfuscateCurrentText = false;
            return;
        }
        // the text should be obfuscated, but not at this time
        obfuscateCurrentText = true;
    }

    private void finishLatestText() throws XMLStreamException {
        if (currentTextType == TextType.NONE) {
            // no need to finish anything
            return;
        }
        if (obfuscateCurrentText) {
            ObfuscatedElement currentElement = currentElements.getLast();
            int textLength = currentText.length();
            int obfuscationStart = skipLeadingWhitespace(currentText, 0, textLength);
            int obfuscationEnd = skipTrailingWhitespace(currentText, obfuscationStart, textLength);

            if (obfuscationStart == 0 && obfuscationEnd == textLength) {
                // obfuscate all
                String obfuscatedText = currentElement.config.obfuscator.obfuscateText(currentText, obfuscationStart, obfuscationEnd).toString();
                currentTextType.writeText(xmlStreamWriter, obfuscatedText);
            } else if (obfuscationStart == obfuscationEnd) {
                // obfuscate nothing
                currentTextType.writeText(xmlStreamWriter, currentText.toString());
            } else {
                StringBuilder text = new StringBuilder();
                text.append(currentText, 0, obfuscationStart);
                currentElement.config.obfuscator.obfuscateText(currentText, obfuscationStart, obfuscationEnd, text);
                text.append(currentText, obfuscationEnd, textLength);
                currentTextType.writeText(xmlStreamWriter, text.toString());
            }
        } else {
            // CDATA; CHARACTERS that don't need obfuscation are written out immediately
            currentTextType.writeText(xmlStreamWriter, currentText.toString());
        }
        currentText.delete(0, currentText.length());
        currentTextType = TextType.NONE;
        obfuscateCurrentText = false;
    }

    void flush() throws XMLStreamException {
        xmlStreamWriter.flush();
    }

    private static final class ObfuscatedElement {

        private final ElementConfig config;
        private int depth;

        private ObfuscatedElement(ElementConfig config) {
            this.config = config;
            this.depth = 0;
        }

        private boolean allowsOverriding() {
            return config.forNestedElements != ObfuscationMode.INHERIT;
        }

        private boolean obfuscateNestedElements() {
            return config.forNestedElements != ObfuscationMode.EXCLUDE;
        }
    }

    private enum TextType {
        CHARACTERS(XMLStreamWriter::writeCharacters),
        CDATA(XMLStreamWriter::writeCData),
        NONE,
        ;

        private final TextWriter textWriter;

        TextType() {
            this(null);
        }

        TextType(TextWriter textWriter) {
            this.textWriter = textWriter;
        }

        void writeText(XMLStreamWriter xmlStreamWriter, String text) throws XMLStreamException {
            textWriter.writeText(xmlStreamWriter, text);
        }

        private interface TextWriter {

            void writeText(XMLStreamWriter xmlStreamWriter, String text) throws XMLStreamException;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy