1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Dirty DOM-like  tree
4  *
5  * Authors:
6  *   Lauris Kaplinski <lauris@kaplinski.com>
7  *   bulia byak <buliabyak@users.sf.net>
8  *
9  * Copyright (C) 1999-2002 Lauris Kaplinski
10  *
11  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12  */
13 
14 #include <cstring>
15 #include <string>
16 #include <stdexcept>
17 
18 #include <libxml/parser.h>
19 #include <libxml/xinclude.h>
20 
21 #include "xml/repr.h"
22 #include "xml/attribute-record.h"
23 #include "xml/rebase-hrefs.h"
24 #include "xml/simple-document.h"
25 #include "xml/text-node.h"
26 #include "xml/node.h"
27 
28 #include "io/sys.h"
29 #include "io/stream/stringstream.h"
30 #include "io/stream/gzipstream.h"
31 #include "io/stream/uristream.h"
32 
33 #include "extension/extension.h"
34 
35 #include "attribute-rel-util.h"
36 #include "attribute-sort-util.h"
37 
38 #include "preferences.h"
39 
40 #include <glibmm/miscutils.h>
41 
42 using Inkscape::IO::Writer;
43 using Inkscape::XML::Document;
44 using Inkscape::XML::SimpleDocument;
45 using Inkscape::XML::Node;
46 using Inkscape::XML::AttributeRecord;
47 using Inkscape::XML::AttributeVector;
48 using Inkscape::XML::rebase_href_attrs;
49 
50 Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns);
51 static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
52 static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
53 static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
54                                               bool add_whitespace, gchar const *default_ns,
55                                               int inlineattrs, int indent,
56                                               gchar const *old_href_abs_base,
57                                               gchar const *new_href_abs_base);
58 
59 static void sp_repr_write_stream_element(Node *repr, Writer &out,
60                                          gint indent_level, bool add_whitespace,
61                                          Glib::QueryQuark elide_prefix,
62                                          const AttributeVector & attributes,
63                                          int inlineattrs, int indent,
64                                          gchar const *old_href_abs_base,
65                                          gchar const *new_href_abs_base);
66 
67 
68 class XmlSource
69 {
70 public:
XmlSource()71     XmlSource()
72         : filename(nullptr),
73           encoding(nullptr),
74           fp(nullptr),
75           firstFewLen(0),
76           LoadEntities(false),
77           cachedData(),
78           cachedPos(0),
79           instr(nullptr),
80           gzin(nullptr)
81     {
82         for (unsigned char & k : firstFew)
83         {
84             k=0;
85         }
86     }
~XmlSource()87     virtual ~XmlSource()
88     {
89         close();
90         if ( encoding ) {
91             g_free(encoding);
92             encoding = nullptr;
93         }
94     }
95 
96     int setFile( char const * filename, bool load_entities );
97 
98     xmlDocPtr readXml();
99 
100     static int readCb( void * context, char * buffer, int len );
101     static int closeCb( void * context );
102 
getEncoding() const103     char const* getEncoding() const { return encoding; }
104     int read( char * buffer, int len );
105     int close();
106 private:
107     const char* filename;
108     char* encoding;
109     FILE* fp;
110     unsigned char firstFew[4];
111     int firstFewLen;
112     bool LoadEntities; // Checks for SYSTEM Entities (requires cached data)
113     std::string cachedData;
114     unsigned int cachedPos;
115     Inkscape::IO::FileInputStream* instr;
116     Inkscape::IO::GzipInputStream* gzin;
117 };
118 
setFile(char const * filename,bool load_entities=false)119 int XmlSource::setFile(char const *filename, bool load_entities=false)
120 {
121     int retVal = -1;
122 
123     this->filename = filename;
124 
125     fp = Inkscape::IO::fopen_utf8name(filename, "r");
126     if ( fp ) {
127         // First peek in the file to see what it is
128         memset( firstFew, 0, sizeof(firstFew) );
129 
130         size_t some = fread( firstFew, 1, 4, fp );
131         if ( fp ) {
132             // first check for compression
133             if ( (some >= 2) && (firstFew[0] == 0x1f) && (firstFew[1] == 0x8b) ) {
134                 //g_message(" the file being read is gzip'd. extract it");
135                 fclose(fp);
136                 fp = nullptr;
137                 fp = Inkscape::IO::fopen_utf8name(filename, "r");
138                 instr = new Inkscape::IO::FileInputStream(fp);
139                 gzin = new Inkscape::IO::GzipInputStream(*instr);
140 
141                 memset( firstFew, 0, sizeof(firstFew) );
142                 some = 0;
143                 int single = 0;
144                 while ( some < 4 && single >= 0 )
145                 {
146                     single = gzin->get();
147                     if ( single >= 0 ) {
148                         firstFew[some++] = 0x0ff & single;
149                     } else {
150                         break;
151                     }
152                 }
153             }
154 
155             int encSkip = 0;
156             if ( (some >= 2) &&(firstFew[0] == 0xfe) && (firstFew[1] == 0xff) ) {
157                 encoding = g_strdup("UTF-16BE");
158                 encSkip = 2;
159             } else if ( (some >= 2) && (firstFew[0] == 0xff) && (firstFew[1] == 0xfe) ) {
160                 encoding = g_strdup("UTF-16LE");
161                 encSkip = 2;
162             } else if ( (some >= 3) && (firstFew[0] == 0xef) && (firstFew[1] == 0xbb) && (firstFew[2] == 0xbf) ) {
163                 encoding = g_strdup("UTF-8");
164                 encSkip = 3;
165             }
166 
167             if ( encSkip ) {
168                 memmove( firstFew, firstFew + encSkip, (some - encSkip) );
169                 some -= encSkip;
170             }
171 
172             firstFewLen = some;
173             retVal = 0; // no error
174         }
175     }
176     if(load_entities) {
177         this->cachedData = std::string("");
178         this->cachedPos = 0;
179 
180         // First get data from file in typical way (cache it all)
181         char *buffer = new char [4096];
182         while(true) {
183             int len = this->read(buffer, 4096);
184             if(len <= 0) break;
185             buffer[len] = 0;
186             this->cachedData += buffer;
187         }
188         delete[] buffer;
189 
190         // Check for SYSTEM or PUBLIC entities and remove them from the cache
191         GMatchInfo *info;
192         gint start, end;
193 
194         GRegex *regex = g_regex_new(
195             "<!ENTITY\\s+[^>\\s]+\\s+(SYSTEM|PUBLIC\\s+\"[^>\"]+\")\\s+\"[^>\"]+\"\\s*>",
196             G_REGEX_CASELESS, G_REGEX_MATCH_NEWLINE_ANY, nullptr);
197 
198         g_regex_match (regex, this->cachedData.c_str(), G_REGEX_MATCH_NEWLINE_ANY, &info);
199 
200         while (g_match_info_matches (info)) {
201             if (g_match_info_fetch_pos (info, 1, &start, &end))
202                 this->cachedData.erase(start, end - start);
203             g_match_info_next (info, nullptr);
204         }
205         g_match_info_free(info);
206         g_regex_unref(regex);
207     }
208     // Do this after loading cache, so reads don't return cache to fill cache.
209     this->LoadEntities = load_entities;
210     return retVal;
211 }
212 
readXml()213 xmlDocPtr XmlSource::readXml()
214 {
215     int parse_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
216 
217     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
218     bool allowNetAccess = prefs->getBool("/options/externalresources/xml/allow_net_access", false);
219     if (!allowNetAccess) parse_options |= XML_PARSE_NONET;
220 
221     // Allow NOENT only if we're filtering out SYSTEM and PUBLIC entities
222     if (LoadEntities)     parse_options |= XML_PARSE_NOENT;
223 
224     auto doc = xmlReadIO( readCb, closeCb, this,
225                       filename, getEncoding(), parse_options);
226 
227     if (doc && doc->properties && xmlXIncludeProcessFlags(doc, XML_PARSE_NOXINCNODE) < 0) {
228         g_warning("XInclude processing failed for %s", filename);
229     }
230 
231     return doc;
232 }
233 
readCb(void * context,char * buffer,int len)234 int XmlSource::readCb( void * context, char * buffer, int len )
235 {
236     int retVal = -1;
237 
238     if ( context ) {
239         XmlSource* self = static_cast<XmlSource*>(context);
240         retVal = self->read( buffer, len );
241     }
242     return retVal;
243 }
244 
closeCb(void * context)245 int XmlSource::closeCb(void * context)
246 {
247     if ( context ) {
248         XmlSource* self = static_cast<XmlSource*>(context);
249         self->close();
250     }
251     return 0;
252 }
253 
read(char * buffer,int len)254 int XmlSource::read( char *buffer, int len )
255 {
256     int retVal = 0;
257     size_t got = 0;
258 
259     if ( LoadEntities ) {
260         if (cachedPos >= cachedData.length()) {
261             return -1;
262         } else {
263             retVal = cachedData.copy(buffer, len, cachedPos);
264             cachedPos += retVal;
265             return retVal; // Do NOT continue.
266         }
267     } else if ( firstFewLen > 0 ) {
268         int some = (len < firstFewLen) ? len : firstFewLen;
269         memcpy( buffer, firstFew, some );
270         if ( len < firstFewLen ) {
271             memmove( firstFew, firstFew + some, (firstFewLen - some) );
272         }
273         firstFewLen -= some;
274         got = some;
275     } else if ( gzin ) {
276         int single = 0;
277         while ( (static_cast<int>(got) < len) && (single >= 0) )
278         {
279             single = gzin->get();
280             if ( single >= 0 ) {
281                 buffer[got++] = 0x0ff & single;
282             } else {
283                 break;
284             }
285         }
286     } else {
287         got = fread( buffer, 1, len, fp );
288     }
289 
290     if ( feof(fp) ) {
291         retVal = got;
292     } else if ( ferror(fp) ) {
293         retVal = -1;
294     } else {
295         retVal = got;
296     }
297 
298     return retVal;
299 }
300 
close()301 int XmlSource::close()
302 {
303     if ( gzin ) {
304         gzin->close();
305         delete gzin;
306         gzin = nullptr;
307     }
308     if ( instr ) {
309         instr->close();
310         fp = nullptr;
311         delete instr;
312         instr = nullptr;
313     }
314     if ( fp ) {
315         fclose(fp);
316         fp = nullptr;
317     }
318     return 0;
319 }
320 
321 /**
322  * Reads XML from a file, and returns the Document.
323  * The default namespace can also be specified, if desired.
324  */
sp_repr_read_file(const gchar * filename,const gchar * default_ns)325 Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns)
326 {
327     xmlDocPtr doc = nullptr;
328     Document * rdoc = nullptr;
329 
330     xmlSubstituteEntitiesDefault(1);
331 
332     g_return_val_if_fail(filename != nullptr, NULL);
333     if (!Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) {
334         g_warning("Can't open file: %s (doesn't exist)", filename);
335         return nullptr;
336     }
337     /* fixme: A file can disappear at any time, including between now and when we actually try to
338      * open it.  Get rid of the above test once we're sure that we correctly handle
339      * non-existence. */
340 
341     // TODO: bulia, please look over
342     gsize bytesRead = 0;
343     gsize bytesWritten = 0;
344     GError* error = nullptr;
345     // TODO: need to replace with our own fopen and reading
346     gchar* localFilename = g_filename_from_utf8(filename, -1, &bytesRead, &bytesWritten, &error);
347     g_return_val_if_fail(localFilename != nullptr, NULL);
348 
349     Inkscape::IO::dump_fopen_call(filename, "N");
350 
351     XmlSource src;
352 
353     if (src.setFile(filename) == 0) {
354         doc = src.readXml();
355         rdoc = sp_repr_do_read(doc, default_ns);
356         // For some reason, failed ns loading results in this
357         // We try a system check version of load with NOENT for adobe
358         if (rdoc && strcmp(rdoc->root()->name(), "ns:svg") == 0) {
359             xmlFreeDoc(doc);
360             src.setFile(filename, true);
361             doc = src.readXml();
362             rdoc = sp_repr_do_read(doc, default_ns);
363         }
364     }
365 
366     if (doc) {
367         xmlFreeDoc(doc);
368     }
369 
370     if (localFilename) {
371         g_free(localFilename);
372     }
373 
374     return rdoc;
375 }
376 
377 /**
378  * Reads and parses XML from a buffer, returning it as an Document
379  */
sp_repr_read_mem(const gchar * buffer,gint length,const gchar * default_ns)380 Document *sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns)
381 {
382     xmlDocPtr doc;
383     Document * rdoc;
384 
385     xmlSubstituteEntitiesDefault(1);
386 
387     g_return_val_if_fail (buffer != nullptr, NULL);
388 
389     int parser_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
390     parser_options |= XML_PARSE_NONET; // TODO: should we allow network access?
391                                        // proper solution would be to check the preference "/options/externalresources/xml/allow_net_access"
392                                        // as done in XmlSource::readXml which gets called by the analogous sp_repr_read_file()
393                                        // but sp_repr_read_mem() seems to be called in locations where Inkscape::Preferences::get() fails badly
394     doc = xmlReadMemory (const_cast<gchar *>(buffer), length, nullptr, nullptr, parser_options);
395 
396     rdoc = sp_repr_do_read (doc, default_ns);
397     if (doc) {
398         xmlFreeDoc (doc);
399     }
400     return rdoc;
401 }
402 
403 /**
404  * Reads and parses XML from a buffer, returning it as an Document
405  */
sp_repr_read_buf(const Glib::ustring & buf,const gchar * default_ns)406 Document *sp_repr_read_buf (const Glib::ustring &buf, const gchar *default_ns)
407 {
408     return sp_repr_read_mem(buf.c_str(), buf.size(), default_ns);
409 }
410 
411 
412 namespace Inkscape {
413 
414 struct compare_quark_ids {
operator ()Inkscape::compare_quark_ids415     bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const {
416         return a.id() < b.id();
417     }
418 };
419 
420 }
421 
422 namespace {
423 
424 typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap;
425 
qname_prefix(Glib::QueryQuark qname)426 Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) {
427     static PrefixMap prefix_map;
428     PrefixMap::iterator iter = prefix_map.find(qname);
429     if ( iter != prefix_map.end() ) {
430         return (*iter).second;
431     } else {
432         gchar const *name_string=g_quark_to_string(qname);
433         gchar const *prefix_end=strchr(name_string, ':');
434         if (prefix_end) {
435             Glib::Quark prefix=Glib::ustring(name_string, prefix_end);
436             prefix_map.insert(PrefixMap::value_type(qname, prefix));
437             return prefix;
438         } else {
439             return GQuark(0);
440         }
441     }
442 }
443 
444 }
445 
446 namespace {
447 
promote_to_namespace(Node * repr,const gchar * prefix)448 void promote_to_namespace(Node *repr, const gchar *prefix) {
449     if ( repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
450         GQuark code = repr->code();
451         if (!qname_prefix(code).id()) {
452             gchar *svg_name = g_strconcat(prefix, ":", g_quark_to_string(code), NULL);
453             repr->setCodeUnsafe(g_quark_from_string(svg_name));
454             g_free(svg_name);
455         }
456         for ( Node *child = repr->firstChild() ; child ; child = child->next() ) {
457             promote_to_namespace(child, prefix);
458         }
459     }
460 }
461 
462 }
463 
464 /**
465  * Reads in a XML file to create a Document
466  */
sp_repr_do_read(xmlDocPtr doc,const gchar * default_ns)467 Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns)
468 {
469     if (doc == nullptr) {
470         return nullptr;
471     }
472     xmlNodePtr node=xmlDocGetRootElement (doc);
473     if (node == nullptr) {
474         return nullptr;
475     }
476 
477     std::map<std::string, std::string> prefix_map;
478 
479     Document *rdoc = new Inkscape::XML::SimpleDocument();
480 
481     Node *root=nullptr;
482     for ( node = doc->children ; node != nullptr ; node = node->next ) {
483         if (node->type == XML_ELEMENT_NODE) {
484             Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
485             rdoc->appendChild(repr);
486             Inkscape::GC::release(repr);
487 
488             if (!root) {
489                 root = repr;
490             } else {
491                 root = nullptr;
492                 break;
493             }
494         } else if ( node->type == XML_COMMENT_NODE || node->type == XML_PI_NODE ) {
495             Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
496             rdoc->appendChild(repr);
497             Inkscape::GC::release(repr);
498         }
499     }
500 
501     if (root != nullptr) {
502         /* promote elements of some XML documents that don't use namespaces
503          * into their default namespace */
504         if ( default_ns && !strchr(root->name(), ':') ) {
505             if ( !strcmp(default_ns, SP_SVG_NS_URI) ) {
506                 promote_to_namespace(root, "svg");
507             }
508             if ( !strcmp(default_ns, INKSCAPE_EXTENSION_URI) ) {
509                 promote_to_namespace(root, INKSCAPE_EXTENSION_NS_NC);
510             }
511         }
512 
513 
514         // Clean unnecessary attributes and style properties from SVG documents. (Controlled by
515         // preferences.)  Note: internal Inkscape svg files will also be cleaned (filters.svg,
516         // icons.svg). How can one tell if a file is internal?
517         if ( !strcmp(root->name(), "svg:svg" ) ) {
518             Inkscape::Preferences *prefs = Inkscape::Preferences::get();
519             bool clean = prefs->getBool("/options/svgoutput/check_on_reading");
520             if( clean ) {
521                 sp_attribute_clean_tree( root );
522             }
523         }
524     }
525 
526     return rdoc;
527 }
528 
sp_repr_qualified_name(gchar * p,gint len,xmlNsPtr ns,const xmlChar * name,const gchar *,std::map<std::string,std::string> & prefix_map)529 gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar */*default_ns*/, std::map<std::string, std::string> &prefix_map)
530 {
531     const xmlChar *prefix;
532     if (ns){
533         if (ns->href ) {
534             prefix = reinterpret_cast<const xmlChar*>( sp_xml_ns_uri_prefix(reinterpret_cast<const gchar*>(ns->href),
535                                                                             reinterpret_cast<const char*>(ns->prefix)) );
536             prefix_map[reinterpret_cast<const char*>(prefix)] = reinterpret_cast<const char*>(ns->href);
537         }
538         else {
539             prefix = nullptr;
540         }
541     }
542     else {
543         prefix = nullptr;
544     }
545 
546     if (prefix) {
547         return g_snprintf (p, len, "%s:%s", reinterpret_cast<const gchar*>(prefix), name);
548     } else {
549         return g_snprintf (p, len, "%s", name);
550     }
551 }
552 
sp_repr_svg_read_node(Document * xml_doc,xmlNodePtr node,const gchar * default_ns,std::map<std::string,std::string> & prefix_map)553 static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map)
554 {
555     xmlAttrPtr prop;
556     xmlNodePtr child;
557     gchar c[256];
558 
559     if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) {
560 
561         if (node->content == nullptr || *(node->content) == '\0') {
562             return nullptr; // empty text node
563         }
564 
565         // Since libxml2 2.9.0, only element nodes are checked, thus check parent.
566         // Note: this only handles XML's rules for white space. SVG's specific rules
567         // are handled in sp-string.cpp.
568         bool preserve = (xmlNodeGetSpacePreserve (node->parent) == 1);
569 
570         xmlChar *p;
571         for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++)
572             ; // skip all whitespace
573 
574         if (!(*p)) { // this is an all-whitespace node, and preserve == default
575             return nullptr; // we do not preserve all-whitespace nodes unless we are asked to
576         }
577 
578         // We keep track of original node type so that CDATA sections are preserved on output.
579         return xml_doc->createTextNode(reinterpret_cast<gchar *>(node->content),
580                                        node->type == XML_CDATA_SECTION_NODE );
581     }
582 
583     if (node->type == XML_COMMENT_NODE) {
584         return xml_doc->createComment(reinterpret_cast<gchar *>(node->content));
585     }
586 
587     if (node->type == XML_PI_NODE) {
588         return xml_doc->createPI(reinterpret_cast<const gchar *>(node->name),
589                                  reinterpret_cast<const gchar *>(node->content));
590     }
591 
592     if (node->type == XML_ENTITY_DECL) {
593         return nullptr;
594     }
595 
596     sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map);
597     Node *repr = xml_doc->createElement(c);
598     /* TODO remember node->ns->prefix if node->ns != NULL */
599 
600     for (prop = node->properties; prop != nullptr; prop = prop->next) {
601         if (prop->children) {
602             sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map);
603             repr->setAttribute(c, reinterpret_cast<gchar*>(prop->children->content));
604             /* TODO remember prop->ns->prefix if prop->ns != NULL */
605         }
606     }
607 
608     if (node->content) {
609         repr->setContent(reinterpret_cast<gchar*>(node->content));
610     }
611 
612     for (child = node->xmlChildrenNode; child != nullptr; child = child->next) {
613         Node *crepr = sp_repr_svg_read_node (xml_doc, child, default_ns, prefix_map);
614         if (crepr) {
615             repr->appendChild(crepr);
616             Inkscape::GC::release(crepr);
617         }
618     }
619 
620     return repr;
621 }
622 
623 
sp_repr_save_writer(Document * doc,Inkscape::IO::Writer * out,gchar const * default_ns,gchar const * old_href_abs_base,gchar const * new_href_abs_base)624 static void sp_repr_save_writer(Document *doc, Inkscape::IO::Writer *out,
625                     gchar const *default_ns,
626                     gchar const *old_href_abs_base,
627                     gchar const *new_href_abs_base)
628 {
629     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
630     bool inlineattrs = prefs->getBool("/options/svgoutput/inlineattrs");
631     int indent = prefs->getInt("/options/svgoutput/indent", 2);
632 
633     /* fixme: do this The Right Way */
634     out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" );
635 
636     const gchar *str = static_cast<Node *>(doc)->attribute("doctype");
637     if (str) {
638         out->writeString( str );
639     }
640 
641     for (Node *repr = sp_repr_document_first_child(doc);
642          repr; repr = repr->next())
643     {
644         Inkscape::XML::NodeType const node_type = repr->type();
645         if ( node_type == Inkscape::XML::NodeType::ELEMENT_NODE ) {
646             sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns, inlineattrs, indent,
647                                               old_href_abs_base, new_href_abs_base);
648         } else {
649             sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0), inlineattrs, indent,
650                                  old_href_abs_base, new_href_abs_base);
651             if ( node_type == Inkscape::XML::NodeType::COMMENT_NODE ) {
652                 out->writeChar('\n');
653             }
654         }
655     }
656 }
657 
658 
sp_repr_save_buf(Document * doc)659 Glib::ustring sp_repr_save_buf(Document *doc)
660 {
661     Inkscape::IO::StringOutputStream souts;
662     Inkscape::IO::OutputStreamWriter outs(souts);
663 
664     sp_repr_save_writer(doc, &outs, SP_INKSCAPE_NS_URI, nullptr, nullptr);
665 
666     outs.close();
667     Glib::ustring buf = souts.getString();
668 
669     return buf;
670 }
671 
672 
sp_repr_save_stream(Document * doc,FILE * fp,gchar const * default_ns,bool compress,gchar const * const old_href_abs_base,gchar const * const new_href_abs_base)673 void sp_repr_save_stream(Document *doc, FILE *fp, gchar const *default_ns, bool compress,
674                     gchar const *const old_href_abs_base,
675                     gchar const *const new_href_abs_base)
676 {
677     Inkscape::IO::FileOutputStream bout(fp);
678     Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : nullptr;
679     Inkscape::IO::OutputStreamWriter *out  = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout );
680 
681     sp_repr_save_writer(doc, out, default_ns, old_href_abs_base, new_href_abs_base);
682 
683     delete out;
684     delete gout;
685 }
686 
687 
688 
689 /**
690  * Returns true if file successfully saved.
691  *
692  * \param filename The actual file to do I/O to, which might be a temp file.
693  *
694  * \param for_filename The base URI [actually filename] to assume for purposes of rewriting
695  *              xlink:href attributes.
696  */
sp_repr_save_rebased_file(Document * doc,gchar const * const filename,gchar const * default_ns,gchar const * old_base,gchar const * for_filename)697 bool sp_repr_save_rebased_file(Document *doc, gchar const *const filename, gchar const *default_ns,
698                           gchar const *old_base, gchar const *for_filename)
699 {
700     if (!filename) {
701         return false;
702     }
703 
704     bool compress;
705     {
706         size_t const filename_len = strlen(filename);
707         compress = ( filename_len > 5
708                      && strcasecmp(".svgz", filename + filename_len - 5) == 0 );
709     }
710 
711     Inkscape::IO::dump_fopen_call( filename, "B" );
712     FILE *file = Inkscape::IO::fopen_utf8name(filename, "w");
713     if (file == nullptr) {
714         return false;
715     }
716 
717     std::string old_href_abs_base;
718     std::string new_href_abs_base;
719 
720     if (old_base) {
721         old_href_abs_base = old_base;
722         if (!Glib::path_is_absolute(old_href_abs_base)) {
723             old_href_abs_base = Glib::build_filename(Glib::get_current_dir(), old_href_abs_base);
724         }
725     }
726 
727     if (for_filename) {
728         if (Glib::path_is_absolute(for_filename)) {
729             new_href_abs_base = Glib::path_get_dirname(for_filename);
730         } else {
731             std::string const cwd = Glib::get_current_dir();
732             std::string const for_abs_filename = Glib::build_filename(cwd, for_filename);
733             new_href_abs_base = Glib::path_get_dirname(for_abs_filename);
734         }
735 
736         /* effic: Once we're confident that we never need (or never want) to resort
737          * to using sodipodi:absref instead of the xlink:href value,
738          * then we should do `if streq() { free them and set both to NULL; }'. */
739     }
740     sp_repr_save_stream(doc, file, default_ns, compress, old_href_abs_base.c_str(), new_href_abs_base.c_str());
741 
742     if (fclose (file) != 0) {
743         return false;
744     }
745 
746     return true;
747 }
748 
749 /**
750  * Returns true iff file successfully saved.
751  */
sp_repr_save_file(Document * doc,gchar const * const filename,gchar const * default_ns)752 bool sp_repr_save_file(Document *doc, gchar const *const filename, gchar const *default_ns)
753 {
754     return sp_repr_save_rebased_file(doc, filename, default_ns, nullptr, nullptr);
755 }
756 
757 
758 /* (No doubt this function already exists elsewhere.) */
repr_quote_write(Writer & out,const gchar * val)759 static void repr_quote_write (Writer &out, const gchar * val)
760 {
761     if (val) {
762         for (; *val != '\0'; val++) {
763             switch (*val) {
764                 case '"': out.writeString( "&quot;" ); break;
765                 case '&': out.writeString( "&amp;" ); break;
766                 case '<': out.writeString( "&lt;" ); break;
767                 case '>': out.writeString( "&gt;" ); break;
768                 default: out.writeChar( *val ); break;
769             }
770         }
771     }
772 }
773 
repr_write_comment(Writer & out,const gchar * val,bool addWhitespace,gint indentLevel,int indent)774 static void repr_write_comment( Writer &out, const gchar * val, bool addWhitespace, gint indentLevel, int indent )
775 {
776     if ( indentLevel > 16 ) {
777         indentLevel = 16;
778     }
779     if (addWhitespace && indent) {
780         for (gint i = 0; i < indentLevel; i++) {
781             for (gint j = 0; j < indent; j++) {
782                 out.writeChar(' ');
783             }
784         }
785     }
786 
787     out.printf("<!--%s-->", val);
788 
789     if (addWhitespace) {
790         out.writeChar('\n');
791     }
792 }
793 
794 namespace {
795 
796 typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap;
797 typedef std::map<Glib::QueryQuark, Inkscape::Util::ptr_shared, Inkscape::compare_quark_ids> NSMap;
798 
qname_local_name(Glib::QueryQuark qname)799 gchar const *qname_local_name(Glib::QueryQuark qname) {
800     static LocalNameMap local_name_map;
801     LocalNameMap::iterator iter = local_name_map.find(qname);
802     if ( iter != local_name_map.end() ) {
803         return (*iter).second;
804     } else {
805         gchar const *name_string=g_quark_to_string(qname);
806         gchar const *prefix_end=strchr(name_string, ':');
807         if (prefix_end) {
808             return prefix_end + 1;
809         } else {
810             return name_string;
811         }
812     }
813 }
814 
add_ns_map_entry(NSMap & ns_map,Glib::QueryQuark prefix)815 void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) {
816     using Inkscape::Util::ptr_shared;
817     using Inkscape::Util::share_unsafe;
818 
819     static const Glib::QueryQuark xml_prefix("xml");
820 
821     NSMap::iterator iter=ns_map.find(prefix);
822     if ( iter == ns_map.end() ) {
823         if (prefix.id()) {
824             gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix));
825             if (uri) {
826                 ns_map.insert(NSMap::value_type(prefix, share_unsafe(uri)));
827             } else if ( prefix != xml_prefix ) {
828                 g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix));
829             }
830         } else {
831             ns_map.insert(NSMap::value_type(prefix, ptr_shared()));
832         }
833     }
834 }
835 
populate_ns_map(NSMap & ns_map,Node & repr)836 void populate_ns_map(NSMap &ns_map, Node &repr) {
837     if ( repr.type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
838         add_ns_map_entry(ns_map, qname_prefix(repr.code()));
839         for ( const auto & iter : repr.attributeList() )
840         {
841             Glib::QueryQuark prefix=qname_prefix(iter.key);
842             if (prefix.id()) {
843                 add_ns_map_entry(ns_map, prefix);
844             }
845         }
846         for ( Node *child=repr.firstChild() ;
847               child ; child = child->next() )
848         {
849             populate_ns_map(ns_map, *child);
850         }
851     }
852 }
853 
854 }
855 
sp_repr_write_stream_root_element(Node * repr,Writer & out,bool add_whitespace,gchar const * default_ns,int inlineattrs,int indent,gchar const * const old_href_base,gchar const * const new_href_base)856 static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
857                                   bool add_whitespace, gchar const *default_ns,
858                                   int inlineattrs, int indent,
859                                   gchar const *const old_href_base,
860                                   gchar const *const new_href_base)
861 {
862     using Inkscape::Util::ptr_shared;
863 
864     g_assert(repr != nullptr);
865 
866     // Clean unnecessary attributes and stype properties. (Controlled by preferences.)
867     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
868     bool clean = prefs->getBool("/options/svgoutput/check_on_writing");
869     if (clean) sp_attribute_clean_tree( repr );
870 
871     // Sort attributes in a canonical order (helps with "diffing" SVG files).only if not set disable optimizations
872     bool sort = !prefs->getBool("/options/svgoutput/disable_optimizations") && prefs->getBool("/options/svgoutput/sort_attributes");
873     if (sort) sp_attribute_sort_tree( *repr );
874 
875     Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml");
876 
877     NSMap ns_map;
878     populate_ns_map(ns_map, *repr);
879 
880     Glib::QueryQuark elide_prefix=GQuark(0);
881     if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) {
882         elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, nullptr));
883     }
884 
885     auto attributes = repr->attributeList(); // copy
886 
887     using Inkscape::Util::share_string;
888     for (auto iter : ns_map)
889     {
890         Glib::QueryQuark prefix=iter.first;
891         ptr_shared ns_uri=iter.second;
892 
893         if (prefix.id()) {
894             if ( prefix != xml_prefix ) {
895                 if ( elide_prefix == prefix ) {
896                     //repr->setAttribute(share_string("xmlns"), share_string(ns_uri));
897                     attributes.emplace_back(g_quark_from_static_string("xmlns"), ns_uri);
898                 }
899 
900                 Glib::ustring attr_name="xmlns:";
901                 attr_name.append(g_quark_to_string(prefix));
902                 GQuark key = g_quark_from_string(attr_name.c_str());
903                 //repr->setAttribute(share_string(attr_name.c_str()), share_string(ns_uri));
904                 attributes.emplace_back(key, ns_uri);
905             }
906         } else {
907             // if there are non-namespaced elements, we can't globally
908             // use a default namespace
909             elide_prefix = GQuark(0);
910         }
911     }
912 
913     return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes,
914                                         inlineattrs, indent, old_href_base, new_href_base);
915 }
916 
sp_repr_write_stream(Node * repr,Writer & out,gint indent_level,bool add_whitespace,Glib::QueryQuark elide_prefix,int inlineattrs,int indent,gchar const * const old_href_base,gchar const * const new_href_base)917 void sp_repr_write_stream( Node *repr, Writer &out, gint indent_level,
918                            bool add_whitespace, Glib::QueryQuark elide_prefix,
919                            int inlineattrs, int indent,
920                            gchar const *const old_href_base,
921                            gchar const *const new_href_base)
922 {
923     switch (repr->type()) {
924         case Inkscape::XML::NodeType::TEXT_NODE: {
925             auto textnode = dynamic_cast<const Inkscape::XML::TextNode *>(repr);
926             assert(textnode);
927             if (textnode->is_CData()) {
928                 // Preserve CDATA sections, not converting '&' to &amp;, etc.
929                 out.printf( "<![CDATA[%s]]>", repr->content() );
930             } else {
931                 repr_quote_write( out, repr->content() );
932             }
933             break;
934         }
935         case Inkscape::XML::NodeType::COMMENT_NODE: {
936             repr_write_comment( out, repr->content(), add_whitespace, indent_level, indent );
937             break;
938         }
939         case Inkscape::XML::NodeType::PI_NODE: {
940             out.printf( "<?%s %s?>", repr->name(), repr->content() );
941             break;
942         }
943         case Inkscape::XML::NodeType::ELEMENT_NODE: {
944             sp_repr_write_stream_element( repr, out, indent_level,
945                                           add_whitespace, elide_prefix,
946                                           repr->attributeList(),
947                                           inlineattrs, indent,
948                                           old_href_base, new_href_base);
949             break;
950         }
951         case Inkscape::XML::NodeType::DOCUMENT_NODE: {
952             g_assert_not_reached();
953             break;
954         }
955         default: {
956             g_assert_not_reached();
957         }
958     }
959 }
960 
961 
sp_repr_write_stream_element(Node * repr,Writer & out,gint indent_level,bool add_whitespace,Glib::QueryQuark elide_prefix,const AttributeVector & attributes,int inlineattrs,int indent,gchar const * old_href_base,gchar const * new_href_base)962 void sp_repr_write_stream_element( Node * repr, Writer & out,
963                                    gint indent_level, bool add_whitespace,
964                                    Glib::QueryQuark elide_prefix,
965                                    const AttributeVector & attributes,
966                                    int inlineattrs, int indent,
967                                    gchar const *old_href_base,
968                                    gchar const *new_href_base )
969 {
970     Node *child = nullptr;
971     bool loose = false;
972     bool const add_whitespace_parent = add_whitespace;
973 
974     g_return_if_fail (repr != nullptr);
975 
976     if ( indent_level > 16 ) {
977         indent_level = 16;
978     }
979 
980     if (add_whitespace && indent) {
981         for (gint i = 0; i < indent_level; i++) {
982             for (gint j = 0; j < indent; j++) {
983                 out.writeChar(' ');
984             }
985         }
986     }
987 
988     GQuark code = repr->code();
989     gchar const *element_name;
990     if ( elide_prefix == qname_prefix(code) ) {
991         element_name = qname_local_name(code);
992     } else {
993         element_name = g_quark_to_string(code);
994     }
995     out.printf( "<%s", element_name );
996 
997     // If this is a <text> element, suppress formatting whitespace
998     // for its content and children:
999     if (strcmp(repr->name(), "svg:text")     == 0 ||
1000         strcmp(repr->name(), "svg:flowRoot") == 0) {
1001         add_whitespace = false;
1002     } else {
1003         // Suppress formatting whitespace for xml:space="preserve"
1004         gchar const *xml_space_attr = repr->attribute("xml:space");
1005         if (g_strcmp0(xml_space_attr, "preserve") == 0) {
1006             add_whitespace = false;
1007         } else if (g_strcmp0(xml_space_attr, "default") == 0) {
1008             add_whitespace = true;
1009         }
1010     }
1011 
1012     const auto rbd = rebase_href_attrs(old_href_base, new_href_base, attributes);
1013     for (const auto &iter : rbd) {
1014         if (!inlineattrs) {
1015             out.writeChar('\n');
1016             if (indent) {
1017                 for ( gint i = 0 ; i < indent_level + 1 ; i++ ) {
1018                     for ( gint j = 0 ; j < indent ; j++ ) {
1019                         out.writeChar(' ');
1020                     }
1021                 }
1022             }
1023         }
1024         out.printf(" %s=\"", g_quark_to_string(iter.key));
1025         repr_quote_write(out, iter.value);
1026         out.writeChar('"');
1027     }
1028 
1029     loose = TRUE;
1030     for (child = repr->firstChild() ; child != nullptr; child = child->next()) {
1031         if (child->type() == Inkscape::XML::NodeType::TEXT_NODE) {
1032             loose = FALSE;
1033             break;
1034         }
1035     }
1036 
1037     if (repr->firstChild()) {
1038         out.writeChar('>');
1039         if (loose && add_whitespace) {
1040             out.writeChar('\n');
1041         }
1042         for (child = repr->firstChild(); child != nullptr; child = child->next()) {
1043             sp_repr_write_stream(child, out, ( loose ? indent_level + 1 : 0 ),
1044                                  add_whitespace, elide_prefix, inlineattrs, indent,
1045                                  old_href_base, new_href_base);
1046         }
1047 
1048         if (loose && add_whitespace && indent) {
1049             for (gint i = 0; i < indent_level; i++) {
1050                 for ( gint j = 0 ; j < indent ; j++ ) {
1051                     out.writeChar(' ');
1052                 }
1053             }
1054         }
1055         out.printf( "</%s>", element_name );
1056     } else {
1057         out.writeString( " />" );
1058     }
1059 
1060     if (add_whitespace_parent) {
1061         out.writeChar('\n');
1062     }
1063 }
1064 
1065 
1066 /*
1067   Local Variables:
1068   mode:c++
1069   c-file-style:"stroustrup"
1070   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1071   indent-tabs-mode:nil
1072   fill-column:99
1073   End:
1074 */
1075 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1076