File indexing completed on 2024-05-12 15:59:59

0001 /* This file is part of the KDE projectz
0002    SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0003    SPDX-FileCopyrightText: 2007 Thomas Zander <zander@kde.org>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "KoXmlWriter.h"
0009 
0010 #include <StoreDebug.h>
0011 #include <QByteArray>
0012 #include <QStack>
0013 #include <float.h>
0014 #include "../global/kis_dom_utils.h"
0015 
0016 static const int s_indentBufferLength = 100;
0017 static const int s_escapeBufferLen = 10000;
0018 
0019 class Q_DECL_HIDDEN KoXmlWriter::Private
0020 {
0021 public:
0022     Private(QIODevice* dev_, int indentLevel = 0)
0023         : dev(dev_)
0024         , baseIndentLevel(indentLevel)
0025     {}
0026 
0027     ~Private() {
0028         delete[] indentBuffer;
0029         delete[] escapeBuffer;
0030         //TODO: look at if we must delete "dev". For me we must delete it otherwise we will leak it
0031     }
0032 
0033     QIODevice* dev;
0034     QStack<Tag> tags;
0035     int baseIndentLevel;
0036 
0037     char* indentBuffer; // maybe make it static, but then it needs a K_GLOBAL_STATIC
0038     // and would eat 1K all the time... Maybe refcount it :)
0039     char* escapeBuffer; // can't really be static if we want to be thread-safe
0040 };
0041 
0042 KoXmlWriter::KoXmlWriter(QIODevice* dev, int indentLevel)
0043         : d(new Private(dev, indentLevel))
0044 {
0045     d->indentBuffer = new char[ s_indentBufferLength ];
0046     memset(d->indentBuffer, ' ', s_indentBufferLength);
0047     *d->indentBuffer = '\n'; // write newline before indentation, in one go
0048 
0049     d->escapeBuffer = new char[s_escapeBufferLen];
0050     if (!d->dev->isOpen())
0051         d->dev->open(QIODevice::WriteOnly);
0052 
0053 }
0054 
0055 KoXmlWriter::~KoXmlWriter()
0056 {
0057     delete d;
0058 }
0059 
0060 void KoXmlWriter::startDocument(const char* rootElemName, const char* publicId, const char* systemId)
0061 {
0062     Q_ASSERT(d->tags.isEmpty());
0063     writeCString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
0064     // There isn't much point in a doctype if there's no DTD to refer to
0065     // (I'm told that files that are validated by a RelaxNG schema cannot refer to the schema)
0066     if (publicId) {
0067         writeCString("<!DOCTYPE ");
0068         writeCString(rootElemName);
0069         writeCString(" PUBLIC \"");
0070         writeCString(publicId);
0071         writeCString("\" \"");
0072         writeCString(systemId);
0073         writeCString("\"");
0074         writeCString(">\n");
0075     }
0076 }
0077 
0078 void KoXmlWriter::endDocument()
0079 {
0080     // just to do exactly like QDom does (newline at end of file).
0081     writeChar('\n');
0082     Q_ASSERT(d->tags.isEmpty());
0083 }
0084 
0085 // returns the value of indentInside of the parent
0086 bool KoXmlWriter::prepareForChild(bool indentInside)
0087 {
0088     if (!d->tags.isEmpty()) {
0089         Tag& parent = d->tags.top();
0090         if (!parent.hasChildren) {
0091             closeStartElement(parent);
0092             parent.hasChildren = true;
0093             parent.lastChildIsText = false;
0094         }
0095         if (parent.indentInside && indentInside) {
0096             writeIndent();
0097         }
0098         return parent.indentInside && indentInside;
0099     }
0100     return indentInside;
0101 }
0102 
0103 void KoXmlWriter::prepareForTextNode()
0104 {
0105     if (d->tags.isEmpty())
0106         return;
0107     Tag& parent = d->tags.top();
0108     if (!parent.hasChildren) {
0109         closeStartElement(parent);
0110         parent.hasChildren = true;
0111         parent.lastChildIsText = true;
0112     }
0113 }
0114 
0115 void KoXmlWriter::startElement(const char* tagName, bool indentInside)
0116 {
0117     Q_ASSERT(tagName != 0);
0118 
0119     // Tell parent that it has children
0120     indentInside = prepareForChild(indentInside);
0121 
0122     d->tags.push(Tag(tagName, indentInside));
0123     writeChar('<');
0124     writeCString(tagName);
0125     //kDebug(s_area) << tagName;
0126 }
0127 
0128 void KoXmlWriter::addCompleteElement(QIODevice* indev)
0129 {
0130     prepareForChild();
0131     const bool wasOpen = indev->isOpen();
0132     // Always (re)open the device in readonly mode, it might be
0133     // already open but for writing, and we need to rewind.
0134     const bool openOk = indev->open(QIODevice::ReadOnly);
0135     Q_ASSERT(openOk);
0136     if (!openOk) {
0137         warnStore << "Failed to re-open the device! wasOpen=" << wasOpen;
0138         return;
0139     }
0140 
0141     QString indentString;
0142     indentString.fill((' '), d->tags.size() + d->baseIndentLevel);
0143     QByteArray indentBuf(indentString.toUtf8());
0144 
0145     QByteArray buffer;
0146     while (!indev->atEnd()) {
0147         buffer = indev->readLine();
0148 
0149         d->dev->write(indentBuf);
0150         d->dev->write(buffer);
0151     }
0152 
0153     if (!wasOpen) {
0154         // Restore initial state
0155         indev->close();
0156     }
0157 }
0158 
0159 void KoXmlWriter::endElement()
0160 {
0161     if (d->tags.isEmpty())
0162         warnStore << "EndElement() was called more times than startElement(). "
0163                      "The generated XML will be invalid! "
0164                      "Please report this bug (by saving the document to another format...)" << endl;
0165 
0166     Tag tag = d->tags.pop();
0167 
0168     if (!tag.hasChildren) {
0169         writeCString("/>");
0170     } else {
0171         if (tag.indentInside && !tag.lastChildIsText) {
0172             writeIndent();
0173         }
0174         writeCString("</");
0175         Q_ASSERT(tag.tagName != 0);
0176         writeCString(tag.tagName);
0177         writeChar('>');
0178     }
0179 }
0180 
0181 void KoXmlWriter::addTextNode(const QByteArray& cstr)
0182 {
0183     // Same as the const char* version below, but here we know the size
0184     prepareForTextNode();
0185     char* escaped = escapeForXML(cstr.constData(), cstr.size());
0186     writeCString(escaped);
0187     if (escaped != d->escapeBuffer)
0188         delete[] escaped;
0189 }
0190 
0191 void KoXmlWriter::addTextNode(const char* cstr)
0192 {
0193     prepareForTextNode();
0194     char* escaped = escapeForXML(cstr, -1);
0195     writeCString(escaped);
0196     if (escaped != d->escapeBuffer)
0197         delete[] escaped;
0198 }
0199 
0200 void KoXmlWriter::addAttribute(const char* attrName, const QByteArray& value)
0201 {
0202     // Same as the const char* one, but here we know the size
0203     writeChar(' ');
0204     writeCString(attrName);
0205     writeCString("=\"");
0206     char* escaped = escapeForXML(value.constData(), value.size());
0207     writeCString(escaped);
0208     if (escaped != d->escapeBuffer)
0209         delete[] escaped;
0210     writeChar('"');
0211 }
0212 
0213 void KoXmlWriter::addAttribute(const char* attrName, const char* value)
0214 {
0215     writeChar(' ');
0216     writeCString(attrName);
0217     writeCString("=\"");
0218     char* escaped = escapeForXML(value, -1);
0219     writeCString(escaped);
0220     if (escaped != d->escapeBuffer)
0221         delete[] escaped;
0222     writeChar('"');
0223 }
0224 
0225 void KoXmlWriter::addAttribute(const char* attrName, double value)
0226 {
0227     addAttribute(attrName, KisDomUtils::toString(value));
0228 }
0229 
0230 void KoXmlWriter::addAttribute(const char* attrName, float value)
0231 {
0232     addAttribute(attrName, KisDomUtils::toString(value));
0233 }
0234 
0235 void KoXmlWriter::writeIndent()
0236 {
0237     // +1 because of the leading '\n'
0238     d->dev->write(d->indentBuffer, qMin(d->tags.size() + d->baseIndentLevel + 1,
0239                                         s_indentBufferLength));
0240 }
0241 
0242 
0243 // In case of a reallocation (ret value != d->buffer), the caller owns the return value,
0244 // it must delete it (with [])
0245 char* KoXmlWriter::escapeForXML(const char* source, int length = -1) const
0246 {
0247     // we're going to be pessimistic on char length; so lets make the outputLength less
0248     // the amount one char can take: 6
0249     char* destBoundary = d->escapeBuffer + s_escapeBufferLen - 6;
0250     char* destination = d->escapeBuffer;
0251     char* output = d->escapeBuffer;
0252     const char* src = source; // src moves, source remains
0253     for (;;) {
0254         if (destination >= destBoundary) {
0255             // When we come to realize that our escaped string is going to
0256             // be bigger than the escape buffer (this shouldn't happen very often...),
0257             // we drop the idea of using it, and we allocate a bigger buffer.
0258             // Note that this if() can only be hit once per call to the method.
0259             if (length == -1)
0260                 length = qstrlen(source);   // expensive...
0261             uint newLength = length * 6 + 1; // worst case. 6 is due to &quot; and &apos;
0262             char* buffer = new char[ newLength ];
0263             destBoundary = buffer + newLength;
0264             uint amountOfCharsAlreadyCopied = destination - d->escapeBuffer;
0265             memcpy(buffer, d->escapeBuffer, amountOfCharsAlreadyCopied);
0266             output = buffer;
0267             destination = buffer + amountOfCharsAlreadyCopied;
0268         }
0269         switch (*src) {
0270         case 60: // <
0271             memcpy(destination, "&lt;", 4);
0272             destination += 4;
0273             break;
0274         case 62: // >
0275             memcpy(destination, "&gt;", 4);
0276             destination += 4;
0277             break;
0278         case 34: // "
0279             memcpy(destination, "&quot;", 6);
0280             destination += 6;
0281             break;
0282 #if 0 // needed?
0283         case 39: // '
0284             memcpy(destination, "&apos;", 6);
0285             destination += 6;
0286             break;
0287 #endif
0288         case 38: // &
0289             memcpy(destination, "&amp;", 5);
0290             destination += 5;
0291             break;
0292         case 0:
0293             *destination = '\0';
0294             return output;
0295         // Control codes accepted in XML 1.0 documents.
0296         case 9:
0297         case 10:
0298         case 13:
0299             *destination++ = *src++;
0300             continue;
0301         default:
0302             // Don't add control codes not accepted in XML 1.0 documents.
0303             if (*src > 0 && *src < 32) {
0304                 ++src;
0305             } else {
0306                 *destination++ = *src++;
0307             }
0308             continue;
0309         }
0310         ++src;
0311     }
0312     // NOTREACHED (see case 0)
0313     return output;
0314 }
0315 
0316 void KoXmlWriter::addManifestEntry(const QString& fullPath, const QString& mediaType)
0317 {
0318     startElement("manifest:file-entry");
0319     addAttribute("manifest:media-type", mediaType);
0320     addAttribute("manifest:full-path", fullPath);
0321     endElement();
0322 }
0323 
0324 // TODO check return value!!!
0325 void KoXmlWriter::writeCString(const char* cstr) {
0326     d->dev->write(cstr, qstrlen(cstr));
0327 }
0328 
0329 // TODO check return value!!!
0330 void KoXmlWriter::writeChar(char c) {
0331     d->dev->putChar(c);
0332 }