1 /**
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 /*
21 * XSEC
22 *
23 * XSECDOMUtils:= Utilities to manipulate DOM within XML-SECURITY
24 *
25 * $Id: XSECDOMUtils.cpp 1833341 2018-06-11 16:25:41Z scantor $
26 *
27 */
28
29 // XSEC
30
31 #include <xsec/framework/XSECError.hpp>
32 #include <xsec/xkms/XKMSConstants.hpp>
33
34 #include "XSECDOMUtils.hpp"
35
36 // Xerces
37
38 #include <xercesc/util/XMLUniDefs.hpp>
39 #include <xercesc/util/Janitor.hpp>
40 #include <xercesc/util/PlatformUtils.hpp>
41 #include <xercesc/util/TransService.hpp>
42
43 #include <string.h>
44
45 XERCES_CPP_NAMESPACE_USE
46
47 // --------------------------------------------------------------------------------
48 // Utilities to manipulate DSIG namespaces
49 // --------------------------------------------------------------------------------
50
getDSIGLocalName(const DOMNode * node)51 const XMLCh * getDSIGLocalName(const DOMNode *node) {
52
53 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIDSIG))
54 return NULL; //DOMString("");
55 else
56 return node->getLocalName();
57
58 }
59
getDSIG11LocalName(const DOMNode * node)60 const XMLCh * getDSIG11LocalName(const DOMNode *node) {
61
62 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIDSIG11))
63 return NULL; //DOMString("");
64 else
65 return node->getLocalName();
66
67 }
68
getECLocalName(const DOMNode * node)69 const XMLCh * getECLocalName(const DOMNode * node) {
70
71 // Exclusive Canonicalisation namespace
72 // Probably should have a generic function
73
74 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIEC))
75 return NULL;
76 else
77 return node->getLocalName();
78
79 }
80
getXPFLocalName(const DOMNode * node)81 const XMLCh * getXPFLocalName(const DOMNode * node) {
82
83 // XPath Filter namespace
84
85 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIXPF))
86 return NULL;
87 else
88 return node->getLocalName();
89
90 }
91
getXENCLocalName(const DOMNode * node)92 const XMLCh * getXENCLocalName(const DOMNode *node) {
93
94 // XML Encryption namespace node
95
96 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIXENC))
97 return NULL;
98 else
99 return node->getLocalName();
100
101 }
102
getXENC11LocalName(const DOMNode * node)103 const XMLCh * getXENC11LocalName(const DOMNode *node) {
104
105 // XML Encryption 1.1 namespace node
106
107 if (!strEquals(node->getNamespaceURI(), DSIGConstants::s_unicodeStrURIXENC11))
108 return NULL;
109 else
110 return node->getLocalName();
111
112 }
113
114 #ifdef XSEC_XKMS_ENABLED
getXKMSLocalName(const DOMNode * node)115 const XMLCh * getXKMSLocalName(const DOMNode *node) {
116
117 // XKMS namespace node
118
119 if (!strEquals(node->getNamespaceURI(), XKMSConstants::s_unicodeStrURIXKMS))
120 return NULL;
121 else
122 return node->getLocalName();
123
124 }
125 #endif
126
127 // --------------------------------------------------------------------------------
128 // Find a nominated DSIG node in a document
129 // --------------------------------------------------------------------------------
130
findDSIGNode(DOMNode * n,const char * nodeName)131 DOMNode *findDSIGNode(DOMNode *n, const char * nodeName) {
132
133 const XMLCh * name = getDSIGLocalName(n);
134
135 if (strEquals(name, nodeName)) {
136
137 return n;
138
139 }
140
141 DOMNode *child = n->getFirstChild();
142
143 while (child != NULL) {
144
145 DOMNode *ret = findDSIGNode(child, nodeName);
146 if (ret != NULL)
147 return ret;
148 child = child->getNextSibling();
149
150 }
151
152 return child;
153
154 }
155
156 // --------------------------------------------------------------------------------
157 // Find a nominated XENC node in a document
158 // --------------------------------------------------------------------------------
159
findXENCNode(DOMNode * n,const char * nodeName)160 DOMNode *findXENCNode(DOMNode *n, const char * nodeName) {
161
162 const XMLCh * name = getXENCLocalName(n);
163
164 if (strEquals(name, nodeName)) {
165
166 return n;
167
168 }
169
170 DOMNode *child = n->getFirstChild();
171
172 while (child != NULL) {
173
174 DOMNode *ret = findXENCNode(child, nodeName);
175 if (ret != NULL)
176 return ret;
177 child = child->getNextSibling();
178
179 }
180
181 return child;
182
183 }
184
185 // --------------------------------------------------------------------------------
186 // Find particular type of node child
187 // --------------------------------------------------------------------------------
188
findFirstChildOfType(DOMNode * n,DOMNode::NodeType t)189 DOMNode *findFirstChildOfType(DOMNode *n, DOMNode::NodeType t) {
190
191 DOMNode *c;
192
193 if (n == NULL)
194 return n;
195
196 c = n->getFirstChild();
197
198 while (c != NULL && c->getNodeType() != t)
199 c = c->getNextSibling();
200
201 return c;
202
203 }
204
findNextChildOfType(DOMNode * n,DOMNode::NodeType t)205 DOMNode * findNextChildOfType(DOMNode *n, DOMNode::NodeType t) {
206
207 DOMNode * s = n;
208
209 if (s == NULL)
210 return s;
211
212 do {
213 s = s->getNextSibling();
214 } while (s != NULL && s->getNodeType() != t);
215
216 return s;
217
218 }
219
findFirstElementChild(DOMNode * n)220 DOMElement *findFirstElementChild(DOMNode *n) {
221
222 DOMNode *c;
223
224 if (n == NULL)
225 return NULL;
226
227 c = n->getFirstChild();
228
229 while (c != NULL && c->getNodeType() != DOMNode::ELEMENT_NODE)
230 c = c->getNextSibling();
231
232 return (DOMElement *) c;
233
234 }
235
findNextElementChild(DOMNode * n)236 DOMElement * findNextElementChild(DOMNode *n) {
237
238 DOMNode * s = n;
239
240 if (s == NULL)
241 return NULL;
242
243 do {
244 s = s->getNextSibling();
245 } while (s != NULL && s->getNodeType() != DOMNode::ELEMENT_NODE);
246
247 return (DOMElement *) s;
248
249 }
250
251 // --------------------------------------------------------------------------------
252 // Make a QName
253 // --------------------------------------------------------------------------------
254
makeQName(safeBuffer & qname,safeBuffer & prefix,const char * localName)255 safeBuffer &makeQName(safeBuffer & qname, safeBuffer &prefix, const char * localName) {
256
257 if (prefix[0] == '\0') {
258 qname = localName;
259 }
260 else {
261 qname = prefix;
262 qname.sbStrcatIn(":");
263 qname.sbStrcatIn(localName);
264 }
265
266 return qname;
267
268 }
makeQName(safeBuffer & qname,const XMLCh * prefix,const char * localName)269 safeBuffer &makeQName(safeBuffer & qname, const XMLCh *prefix, const char * localName) {
270
271 if (prefix == NULL || prefix[0] == 0) {
272 qname.sbTranscodeIn(localName);
273 }
274 else {
275 qname.sbXMLChIn(prefix);
276 qname.sbXMLChAppendCh(XERCES_CPP_NAMESPACE_QUALIFIER chColon);
277 qname.sbXMLChCat(localName); // Will transcode
278 }
279
280 return qname;
281 }
282
makeQName(safeBuffer & qname,const XMLCh * prefix,const XMLCh * localName)283 safeBuffer &makeQName(safeBuffer & qname, const XMLCh *prefix, const XMLCh * localName) {
284
285 if (prefix == NULL || prefix[0] == 0) {
286 qname.sbXMLChIn(localName);
287 }
288 else {
289 qname.sbXMLChIn(prefix);
290 qname.sbXMLChAppendCh(XERCES_CPP_NAMESPACE_QUALIFIER chColon);
291 qname.sbXMLChCat(localName);
292 }
293
294 return qname;
295 }
296
297 // --------------------------------------------------------------------------------
298 // "Quick" Transcode (low performance)
299 // --------------------------------------------------------------------------------
300
301
302
XMLT(const char * str)303 XMLT::XMLT(const char * str) {
304
305 mp_unicodeStr = XMLString::transcode(str);
306
307 }
308
~XMLT(void)309 XMLT::~XMLT (void) {
310
311 XSEC_RELEASE_XMLCH(mp_unicodeStr);
312
313 }
314
getUnicodeStr(void)315 XMLCh * XMLT::getUnicodeStr(void) {
316
317 return mp_unicodeStr;
318
319 }
320
321 // --------------------------------------------------------------------------------
322 // Gather text from children
323 // --------------------------------------------------------------------------------
324
gatherChildrenText(DOMNode * parent,safeBuffer & output)325 void gatherChildrenText(DOMNode * parent, safeBuffer &output) {
326
327 DOMNode * c = parent->getFirstChild();
328
329 output.sbXMLChIn(DSIGConstants::s_unicodeStrEmpty);
330
331 while (c != NULL) {
332
333 if (c->getNodeType() == DOMNode::TEXT_NODE)
334 output.sbXMLChCat(c->getNodeValue());
335
336 c = c->getNextSibling();
337
338 }
339
340 }
341
342 // --------------------------------------------------------------------------------
343 // Some UTF8 utilities
344 // --------------------------------------------------------------------------------
345
transcodeFromUTF8(const unsigned char * src)346 XMLCh * transcodeFromUTF8(const unsigned char * src) {
347
348 // Take a UTF-8 buffer and transcode to UTF-16
349
350 safeBuffer fullDest;
351 fullDest.sbXMLChIn(DSIGConstants::s_unicodeStrEmpty);
352 XMLCh outputBuf[2050];
353
354 // Used to record byte sizes
355 unsigned char charSizes[2050];
356
357 // Grab a transcoder
358 XMLTransService::Codes failReason;
359
360 XMLTranscoder* t =
361 XMLPlatformUtils::fgTransService->makeNewTranscoderFor("UTF-8",
362 failReason,
363 2*1024,
364 XMLPlatformUtils::fgMemoryManager);
365 Janitor<XMLTranscoder> j_t(t);
366
367 // Need to loop through, 2K at a time
368 XMLSize_t bytesEaten, bytesEatenCounter;
369 XMLSize_t charactersEaten;
370 XMLSize_t totalBytesEaten = 0;
371 XMLSize_t bytesToEat = XMLString::stringLen((char *) src);
372
373 while (totalBytesEaten < bytesToEat) {
374
375 XMLSize_t toEat = bytesToEat - totalBytesEaten;
376
377
378 if (toEat > 2048)
379 toEat = 2048;
380
381 t->transcodeFrom(&src[totalBytesEaten],
382 toEat,
383 outputBuf,
384 2048,
385 bytesEaten,
386 charSizes);
387
388 // Determine how many characters were used
389 bytesEatenCounter = 0;
390 charactersEaten = 0;
391 while (bytesEatenCounter < bytesEaten) {
392 bytesEatenCounter += charSizes[charactersEaten++];
393 }
394
395 outputBuf[charactersEaten] = chNull;
396 fullDest.sbXMLChCat(outputBuf);
397 totalBytesEaten += bytesEaten;
398 }
399
400 // Dup and output
401 return XMLString::replicate(fullDest.rawXMLChBuffer());
402
403 }
404
transcodeToUTF8(const XMLCh * src)405 char * transcodeToUTF8(const XMLCh * src) {
406
407 // Take a UTF-16 buffer and transcode to UTF-8
408
409 safeBuffer fullDest("");
410 unsigned char outputBuf[2050];
411
412 // Grab a transcoder
413 XMLTransService::Codes failReason;
414
415 XMLTranscoder* t =
416 XMLPlatformUtils::fgTransService->makeNewTranscoderFor("UTF-8",
417 failReason,
418 2*1024,
419 XMLPlatformUtils::fgMemoryManager);
420 Janitor<XMLTranscoder> j_t(t);
421
422 // Need to loop through, 2K at a time
423 XMLSize_t charactersEaten, charactersOutput;
424 XMLSize_t totalCharsEaten = 0;
425 XMLSize_t charsToEat = XMLString::stringLen(src);
426
427 while (totalCharsEaten < charsToEat) {
428
429 XMLSize_t toEat = charsToEat - totalCharsEaten;
430
431 if (toEat > 2048)
432 toEat = 2048;
433
434 charactersOutput = t->transcodeTo(&src[totalCharsEaten],
435 toEat,
436 (unsigned char * const) outputBuf,
437 2048,
438 charactersEaten,
439 XMLTranscoder::UnRep_RepChar);
440
441 outputBuf[charactersOutput] = '\0';
442 fullDest.sbStrcatIn((char *) outputBuf);
443 totalCharsEaten += charactersEaten;
444 }
445
446 // Dup and output
447 return XMLString::replicate(fullDest.rawCharBuffer());
448
449 }
450
451 // --------------------------------------------------------------------------------
452 // String decode/encode
453 // --------------------------------------------------------------------------------
454
455 /*
456 * Distinguished names have a particular encoding that needs to be performed prior
457 * to enclusion in the DOM
458 */
459
encodeDName(const XMLCh * toEncode)460 XMLCh * encodeDName(const XMLCh * toEncode) {
461
462 XERCES_CPP_NAMESPACE_USE;
463
464 safeBuffer result;
465
466 static XMLCh s_strEncodedSpace[] = {
467 chBackSlash,
468 chDigit_2,
469 chDigit_0,
470 chNull
471 };
472
473 result.sbXMLChIn(DSIGConstants::s_unicodeStrEmpty);
474
475 if (toEncode == NULL) {
476 return NULL;
477 }
478
479
480 // Find where the trailing whitespace starts
481 const XMLCh * ws = &toEncode[XMLString::stringLen(toEncode)];
482
483 ws--;
484 while (ws != toEncode &&
485 (*ws == '\t' || *ws == '\r' || *ws ==' ' || *ws == '\n'))
486 ws--;
487
488 // Set to first white space character, if we didn't get back to the start
489 if (toEncode != ws)
490 ws++;
491
492 // Now run through each character and encode if necessary
493
494 const XMLCh * i = toEncode;
495
496 if (*i == chPound) {
497 // "#" Characters escaped at the start of a string
498 result.sbXMLChAppendCh(chBackSlash);
499 }
500
501 while (*i != chNull && i != ws) {
502
503 if (*i <= 0x09) {
504 result.sbXMLChAppendCh(chBackSlash);
505 result.sbXMLChAppendCh(chDigit_0);
506 result.sbXMLChAppendCh(chDigit_0 + *i);
507 }
508 else if (*i <= 0x0f) {
509 result.sbXMLChAppendCh(chBackSlash);
510 result.sbXMLChAppendCh(chDigit_0);
511 result.sbXMLChAppendCh(chLatin_A + *i);
512 }
513 else if (*i <= 0x19) {
514 result.sbXMLChAppendCh(chBackSlash);
515 result.sbXMLChAppendCh(chDigit_1);
516 result.sbXMLChAppendCh(chDigit_0 + *i);
517 }
518 else if (*i <= 0x1f) {
519 result.sbXMLChAppendCh(chBackSlash);
520 result.sbXMLChAppendCh(chDigit_1);
521 result.sbXMLChAppendCh(chLatin_A + *i);
522 }
523
524 else if (*i == chComma) {
525
526 // Determine if this is an RDN separator
527 const XMLCh *j = i;
528 j++;
529 while (*j != chComma && *j != chEqual && *j != chNull)
530 j++;
531
532 if (*j != chEqual)
533 result.sbXMLChAppendCh(chBackSlash);
534
535 result.sbXMLChAppendCh(*i);
536
537 }
538
539 else {
540
541 if (*i == chPlus ||
542 *i == chDoubleQuote ||
543 *i == chBackSlash ||
544 *i == chOpenAngle ||
545 *i == chCloseAngle ||
546 *i == chSemiColon) {
547
548 result.sbXMLChAppendCh(chBackSlash);
549 }
550
551 result.sbXMLChAppendCh(*i);
552
553 }
554
555 i++;
556
557 }
558
559 // Now encode trailing white space
560 while (*i != chNull) {
561
562 if (*i == ' ')
563 result.sbXMLChCat(s_strEncodedSpace);
564 else
565 result.sbXMLChAppendCh(*i);
566
567 i++;
568
569 }
570
571 return XMLString::replicate(result.rawXMLChBuffer());
572
573 }
574
decodeDName(const XMLCh * toDecode)575 XMLCh * decodeDName(const XMLCh * toDecode) {
576
577 // Take an encoded name and decode to a normal XMLCh string
578
579 XERCES_CPP_NAMESPACE_USE;
580
581 safeBuffer result;
582
583 result.sbXMLChIn(DSIGConstants::s_unicodeStrEmpty);
584
585 if (toDecode == NULL) {
586 return NULL;
587 }
588
589 const XMLCh * i = toDecode;
590
591 if (*i == chBackSlash && i[1] == chPound) {
592
593 result.sbXMLChAppendCh(chPound);
594 i++;
595 i++;
596
597 }
598
599 while (*i != chNull) {
600
601 if (*i == chBackSlash) {
602
603 i++;
604
605 if (*i == chDigit_0) {
606
607 i++;
608
609 if (*i >= chDigit_0 && *i <= chDigit_9) {
610 result.sbXMLChAppendCh(*i - chDigit_0);
611 }
612 else if (*i >= chLatin_A && *i <= chLatin_F) {
613 result.sbXMLChAppendCh(10 + *i - chLatin_A);
614 }
615 else if (*i >= chLatin_a && *i <= chLatin_f) {
616 result.sbXMLChAppendCh(10 + *i - chLatin_a);
617 }
618 else {
619 throw XSECException(XSECException::DNameDecodeError,
620 "Unexpected escaped character in Distinguished name");
621 }
622 }
623
624 else if (*i == chDigit_1) {
625
626 i++;
627
628 if (*i >= chDigit_0 && *i <= chDigit_9) {
629 result.sbXMLChAppendCh(16 + *i - chDigit_0);
630 }
631 else if (*i >= chLatin_A && *i <= chLatin_F) {
632 result.sbXMLChAppendCh(26 + *i - chLatin_A);
633 }
634 else if (*i >= chLatin_a && *i <= chLatin_f) {
635 result.sbXMLChAppendCh(26 + *i - chLatin_a);
636 }
637 else {
638 throw XSECException(XSECException::DNameDecodeError,
639 "Unexpected escaped character in Distinguished name");
640 }
641 }
642
643 else if (*i == chDigit_2) {
644
645 i++;
646
647 if (*i == '0') {
648 result.sbXMLChAppendCh(' ');
649 }
650
651 else {
652 throw XSECException(XSECException::DNameDecodeError,
653 "Unexpected escaped character in Distinguished name");
654 }
655
656 }
657
658 else if (*i == chComma ||
659 *i == chPlus ||
660 *i == chDoubleQuote ||
661 *i == chBackSlash ||
662 *i == chOpenAngle ||
663 *i == chCloseAngle ||
664 *i == chSemiColon) {
665
666 result.sbXMLChAppendCh(*i);
667 }
668
669 else {
670
671 throw XSECException(XSECException::DNameDecodeError,
672 "Unexpected escaped character in Distinguished name");
673
674 }
675
676 i++;
677
678 }
679
680 else {
681
682 result.sbXMLChAppendCh(*i++);
683
684 }
685
686 }
687
688 return XMLString::replicate(result.rawXMLChBuffer());
689
690 }
691
692 // --------------------------------------------------------------------------------
693 // Misc string functions
694 // --------------------------------------------------------------------------------
695
696 // These three functions are pretty much lifted from XMLURL.cpp in Xerces
697
isHexDigit(const XMLCh toCheck)698 static bool isHexDigit(const XMLCh toCheck)
699 {
700 if ((toCheck >= chDigit_0 && toCheck <= chDigit_9)
701 || (toCheck >= chLatin_A && toCheck <= chLatin_F)
702 || (toCheck >= chLatin_a && toCheck <= chLatin_f))
703 {
704 return true;
705 }
706 return false;
707 }
708
xlatHexDigit(const XMLCh toXlat)709 static unsigned int xlatHexDigit(const XMLCh toXlat)
710 {
711 if (!isHexDigit(toXlat)) {
712 throw XSECException(XSECException::ErrorOpeningURI,
713 "Unknown hex char");
714 }
715 if ((toXlat >= chDigit_0) && (toXlat <= chDigit_9))
716 return (unsigned int)(toXlat - chDigit_0);
717
718 if ((toXlat >= chLatin_A) && (toXlat <= chLatin_F))
719 return (unsigned int)(toXlat - chLatin_A) + 10;
720
721 return (unsigned int)(toXlat - chLatin_a) + 10;
722 }
723
cleanURIEscapes(const XMLCh * uriPath)724 XMLCh * cleanURIEscapes(const XMLCh * uriPath) {
725
726 XMLByte *ptr, *utf8Path;
727 XMLSize_t len = XMLString::stringLen(uriPath);
728
729 ptr = utf8Path = new XMLByte[len + 1];
730
731 for (XMLSize_t i = 0; i < len; i++) {
732 unsigned int value = uriPath[i];
733
734 if (value > 255) {
735 delete[] utf8Path;
736 throw XSECException(XSECException::ErrorOpeningURI, "Wide character in URI");
737 }
738
739 if (value == chPercent) {
740 if (!(i + 2 < len && isHexDigit(uriPath[i + 1]) && isHexDigit(uriPath[i + 2]))) {
741 delete[] utf8Path;
742 throw XSECException(XSECException::ErrorOpeningURI, "Bad escape sequence in URI");
743 }
744
745 value = (xlatHexDigit(uriPath[i + 1]) * 16) + (xlatHexDigit(uriPath[i + 2]));
746 i += 2;
747 }
748
749 *(ptr++) = (XMLByte) value;
750 }
751 *ptr = 0;
752
753 try {
754 XMLCh* unicodePath = transcodeFromUTF8(utf8Path);
755 delete[] utf8Path;
756 return unicodePath;
757 }
758 catch (const XMLException&) {
759 }
760
761 delete[] utf8Path;
762 throw XSECException(XSECException::ErrorOpeningURI, "Failed to transcode URI from UTF-8");
763 }
764
765 // --------------------------------------------------------------------------------
766 // Generate Ids
767 // --------------------------------------------------------------------------------
768
makeHexByte(XMLCh * h,unsigned char b)769 void makeHexByte(XMLCh * h, unsigned char b) {
770
771 unsigned char toConvert = (b & 0xf0);
772 toConvert = (toConvert >> 4);
773
774 if (toConvert < 10)
775 h[0] = chDigit_0 + toConvert;
776 else
777 h[0] = chLatin_a + toConvert - 10;
778
779 toConvert = (b & 0xf);
780
781 if (toConvert < 10)
782 h[1] = chDigit_0 + toConvert;
783 else
784 h[1] = chLatin_a + toConvert - 10;
785
786 }
787
788
generateId(unsigned int bytes)789 XMLCh * generateId(unsigned int bytes) {
790
791 unsigned char b[128];
792 XMLCh id[258];
793 unsigned int toGen = (bytes > 128 ? 16 : bytes);
794
795 // Get the appropriate amount of random data
796 // Need to zeroise to ensure valgrind is happy
797 memset(b, 0, 128);
798 memset(id, 0, sizeof(id));
799 if (XSECPlatformUtils::g_cryptoProvider->getRandom(b, toGen) != toGen) {
800
801 throw XSECException(XSECException::CryptoProviderError,
802 "generateId - could not obtain enough random");
803
804 }
805
806 id[0] = chLatin_I;
807
808 unsigned int i;
809 for (i = 0; i < toGen; ++i) {
810
811 makeHexByte(&id[1+(i*2)], b[i]);
812
813 }
814
815 id[1+(i*2)] = chNull;
816
817 return XMLString::replicate(id);
818
819 }
820
821