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 " and ' 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, "<", 4); 0272 destination += 4; 0273 break; 0274 case 62: // > 0275 memcpy(destination, ">", 4); 0276 destination += 4; 0277 break; 0278 case 34: // " 0279 memcpy(destination, """, 6); 0280 destination += 6; 0281 break; 0282 #if 0 // needed? 0283 case 39: // ' 0284 memcpy(destination, "'", 6); 0285 destination += 6; 0286 break; 0287 #endif 0288 case 38: // & 0289 memcpy(destination, "&", 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 }