1 /** 2 * Authors: Szabo Bogdan <szabobogdan@yahoo.com> 3 * Date: 2 25, 2015 4 * License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 5 * Copyright: Public Domain 6 */ 7 module vibedav.prop; 8 9 public import vibedav.base; 10 11 import vibe.core.log; 12 import vibe.core.file; 13 import vibe.inet.mimetypes; 14 import vibe.inet.message; 15 import vibe.http.server; 16 import vibe.http.fileserver; 17 import vibe.http.router : URLRouter; 18 import vibe.stream.operations; 19 import vibe.utils.dictionarylist; 20 21 import std.conv : to; 22 import std.algorithm; 23 import std.file; 24 import std.path; 25 import std.digest.md; 26 import std.datetime; 27 import std..string; 28 import std.stdio : writeln; //todo: remove this 29 import std.typecons; 30 import std.uri; 31 import std.uuid; 32 33 import core.thread; 34 35 class DavPropException : DavException { 36 /// 37 this(HTTPStatus status, string msg, string mime = "plain/text", string file = __FILE__, size_t line = __LINE__, Throwable next = null) 38 { 39 super(status, msg, mime, file, line, next); 40 } 41 42 /// 43 this(HTTPStatus status, string msg, Throwable next, string mime = "plain/text", string file = __FILE__, size_t line = __LINE__) 44 { 45 super(status, msg, next, mime, file, line); 46 } 47 } 48 49 class DavProp { 50 private DavProp[] properties; 51 private string[string] namespaces; 52 private enum _NULL_NS = "_NULL_NS"; 53 54 DavProp parent; 55 56 string name; 57 string value; 58 string namespaceAttr = _NULL_NS; 59 string[string] attribute; 60 61 this(string namespaceAttr, string name, string value) { 62 this(value); 63 this.namespaceAttr = namespaceAttr; 64 this.name = name; 65 } 66 67 this(string namespaceAttr, string name) { 68 this(namespaceAttr, name, ""); 69 } 70 71 this(string value) { 72 this.value = value; 73 } 74 75 this() {} 76 77 /// A key is a tag name glued with a `:` and the namespace 78 static { 79 DavProp FromKeyAndList(string key, string[][string] values) { 80 DavProp p = FromKey(key, ""); 81 82 foreach(nsName, nsList; values) 83 foreach(value; nsList) { 84 DavProp child = DavProp.FromKey(nsName, value); 85 86 p.properties ~= child; 87 } 88 89 return p; 90 } 91 92 DavProp FromKey(string key, string value) { 93 auto pos = key.indexOf(":"); 94 auto name = key[0..pos]; 95 auto ns = key[pos+1..$]; 96 97 return new DavProp(ns, name, value); 98 } 99 } 100 101 private { 102 ulong getKeyPos(string key) inout { 103 foreach(i; 0..properties.length) { 104 if(properties[i].name == key || 105 properties[i].tagName == key || 106 properties[i].tagName ~ ":" ~ properties[i].namespace == key) 107 return i; 108 } 109 110 throw new DavPropException(HTTPStatus.notFound, "Key `" ~ key ~ "` not found"); 111 } 112 113 string getNamespaceAttributes() inout { 114 string ns; 115 if(namespaceAttr != _NULL_NS) 116 ns = ` xmlns="` ~ namespaceAttr ~ `"`; 117 118 if(namespaces.length > 0) 119 foreach(string key, string value; namespaces) 120 ns ~= ` xmlns:` ~ key ~ `="` ~ value ~ `"`; 121 122 return ns; 123 } 124 125 string getAttributes() inout { 126 string ns; 127 128 if(attribute.length > 0) 129 foreach(string key, string value; attribute) 130 ns ~= ` ` ~ key ~ `="` ~ value ~ `"`; 131 132 return ns; 133 } 134 135 string getTagText() { 136 if(namespaceAttr != _NULL_NS && prefix == "") { 137 auto p = getPrefixForNamespace(namespaceAttr); 138 if(p != "") { 139 name = p ~ ":" ~ name; 140 namespaceAttr = _NULL_NS; 141 } 142 } 143 144 return name ~ getAttributes ~ getNamespaceAttributes; 145 } 146 } 147 148 @property { 149 string prefix() inout { 150 auto pos = name.indexOf(":"); 151 152 if (pos == -1) return ""; 153 154 return name[0..pos]; 155 } 156 157 string tagName() inout { 158 auto pos = name.indexOf(":"); 159 160 if (pos == -1) return name; 161 162 return name[pos+1..$]; 163 } 164 165 bool isGroup() { 166 return properties.length > 0; 167 } 168 169 bool isText() { 170 return properties.length == 0 && name == ""; 171 } 172 173 ulong length() { 174 return properties.length; 175 } 176 177 string namespace() inout { 178 if(namespaceAttr != _NULL_NS) 179 return namespaceAttr; 180 181 if(prefix != "") 182 return getNamespaceForPrefix(prefix); 183 184 if(parent !is null) 185 return parent.namespace; 186 187 return ""; 188 } 189 } 190 191 string getNamespaceForPrefix(string prefix) inout { 192 if(prefix in namespaces) 193 return namespaces[prefix]; 194 195 if(parent !is null) 196 return parent.getNamespaceForPrefix(prefix); 197 198 throw new DavPropException(HTTPStatus.internalServerError, `Undefined '` ~ prefix ~ `' namespace.`); 199 } 200 201 string getPrefixForNamespace(string namespace) inout { 202 203 foreach(string prefix, string ns; namespaces) 204 if(namespace == ns) 205 return prefix; 206 207 if(parent !is null) 208 return parent.getPrefixForNamespace(namespace); 209 210 return ""; 211 } 212 213 bool isNameSpaceDefined(string prefix) { 214 if(prefix in namespaces) 215 return true; 216 217 if(parent !is null) 218 return parent.isNameSpaceDefined(prefix); 219 220 return false; 221 } 222 223 bool isPrefixNameSpaceDefined() { 224 if(prefix in namespaces) 225 return true; 226 227 if(parent !is null) 228 return parent.isNameSpaceDefined(prefix); 229 230 return false; 231 } 232 233 ref DavProp[] opIndex() { 234 return properties; 235 } 236 237 ref DavProp opIndex(const string key) { 238 auto pos = getKeyPos(key); 239 return properties[pos]; 240 } 241 242 ref DavProp opIndex(const ulong index) { 243 return properties[index]; 244 } 245 246 void addChild(DavProp value) { 247 auto oldParent = value.parent; 248 value.parent = this; 249 250 try value.checkNamespacePrefixes; 251 catch (DavPropException e) { 252 value.parent = oldParent; 253 throw e; 254 } 255 256 properties ~= value; 257 } 258 259 DavProp opIndexAssign(DavProp value, string key) { 260 auto oldParent = value.parent; 261 value.parent = this; 262 value.name = key; 263 264 try { 265 value.checkNamespacePrefixes; 266 } catch (DavPropException e) { 267 remove(key); 268 value.parent = oldParent; 269 throw e; 270 } 271 272 if(key !in this) 273 properties ~= value; 274 else { 275 auto pos = getKeyPos(key); 276 277 if(properties[pos].namespace != value.namespace) { 278 properties ~= value; 279 } else { 280 properties[pos] = value; 281 } 282 } 283 284 return properties[properties.length - 1]; 285 } 286 287 DavProp opIndexAssign(string data, string key) { 288 DavProp prop = new DavProp(data); 289 return opIndexAssign(prop, key); 290 } 291 292 void remove(string key) { 293 if(key in this) { 294 auto pos = getKeyPos(key); 295 properties.remove(pos); 296 properties.length--; 297 } 298 } 299 300 int opApply(int delegate(ref string, ref DavProp) dg) { 301 int result = 0; 302 303 foreach(ulong index, DavProp p; properties) 304 { 305 string name = p.tagName ~ ":" ~ p.namespace; 306 result = dg(name, p); 307 if (result) 308 break; 309 } 310 311 return result; 312 } 313 314 inout(DavProp)* opBinaryRight(string op)(string key) inout if(op == "in") { 315 try { 316 auto pos = getKeyPos(key); 317 return &properties[pos]; 318 } catch (Exception e) { 319 return null; 320 } 321 } 322 323 override string toString() { 324 if(isText) { 325 return value; 326 } 327 328 if(properties.length == 0 && value == "" && name != "") { 329 if(name == "?xml") 330 return "<" ~ getTagText ~ ">"; 331 else 332 return "<" ~ getTagText ~ "/>"; 333 } 334 335 string a; 336 337 if(name != "") 338 a = "<" ~ getTagText ~ ">"; 339 340 if(properties.length == 0) 341 a ~= value; 342 else 343 foreach(ulong index, DavProp p; properties) 344 a ~= p.toString; 345 346 if(name != "") 347 a ~= `</`~name~`>`; 348 349 return a; 350 } 351 352 bool has(string key) { 353 if(key in this) 354 return true; 355 356 return false; 357 } 358 359 void addNamespace(string prefix, string namespace) { 360 if(isNameSpaceDefined(prefix)) 361 throw new DavPropException(HTTPStatus.internalServerError, "Prefix `"~prefix~"` is already defined."); 362 363 if(namespace == "") 364 throw new DavPropException(HTTPStatus.internalServerError, "Empty namespace for prefix `"~prefix~"`."); 365 366 namespaces[prefix] = namespace; 367 } 368 369 void checkNamespacePrefixes() { 370 if(prefix != "" && !isPrefixNameSpaceDefined) 371 throw new DavPropException(HTTPStatus.internalServerError, `Undefined '` ~ prefix ~ `' namespace.`); 372 373 foreach(DavProp p; properties) { 374 p.checkNamespacePrefixes; 375 } 376 } 377 } 378 379 DavProp[] getTagChilds(DavProp[] list, string key) { 380 DavProp[] result; 381 382 foreach(parent; list) 383 foreach(p; parent.properties) 384 if(p.name == key || p.tagName == key) 385 result ~= p; 386 387 return result; 388 } 389 390 @("string prop") 391 unittest { 392 auto prop = new DavProp("value"); 393 assert(prop.to!string == "value"); 394 } 395 396 @("tag prop") 397 unittest { 398 auto prop = new DavProp; 399 prop["name"] = "value"; 400 assert(prop.toString == `<name>value</name>`); 401 } 402 403 @("check if tags can be accessed from other references") 404 unittest { 405 DavProp properties = new DavProp; 406 auto prop = new DavProp("value"); 407 auto subProp = new DavProp("other value"); 408 409 properties["test"] = prop; 410 properties["test"]["sub"] = subProp; 411 412 assert(prop["sub"] == subProp); 413 } 414 415 @("remove child tags") 416 unittest { 417 DavProp properties = new DavProp; 418 419 properties["prop1"] = "value1"; 420 properties["prop2"] = "value2"; 421 properties["prop3"] = "value2"; 422 423 properties.remove("prop1"); 424 425 assert(properties.length == 2); 426 } 427 428 @("filter tags by tag name") 429 unittest { 430 DavProp properties = new DavProp; 431 auto prop1 = new DavProp; 432 auto prop2 = new DavProp; 433 auto other = new DavProp; 434 435 prop1.name = "prop"; 436 prop2.name = "prop"; 437 other.name = "other"; 438 439 properties.addChild(prop1); 440 properties.addChild(other); 441 properties.addChild(prop2); 442 443 assert([ properties ].getTagChilds("prop") == [prop1, prop2]); 444 } 445 446 @("set tag value property") 447 unittest { 448 auto prop = new DavProp; 449 prop["name"] = "value"; 450 prop["name"].value = "value2"; 451 452 assert(prop.toString == `<name>value2</name>`); 453 } 454 455 @("set namespace attr") 456 unittest { 457 auto prop = new DavProp; 458 prop["name"] = "value"; 459 prop["name"].namespaceAttr = "ns"; 460 461 assert(prop.toString == `<name xmlns="ns">value</name>`); 462 } 463 464 @("set a tag namesapace prefix") 465 unittest { 466 auto prop = new DavProp; 467 prop["name"] = ""; 468 prop["name"].namespaces["D"] = "DAV:"; 469 470 assert(prop.toString == `<name xmlns:D="DAV:"/>`); 471 } 472 473 @("get tag prefix") 474 unittest { 475 auto prop = new DavProp; 476 prop["name"] = ""; 477 prop["name"].namespaces["D"] = "DAV:"; 478 prop["name"]["D:propname"] = "value"; 479 480 assert(prop["name"]["D:propname"].prefix == "D"); 481 } 482 483 @("add a tag with an unknown ns prefix") 484 unittest { 485 auto prop = new DavProp; 486 prop["name"] = ""; 487 488 bool raised = false; 489 490 try { 491 prop["name"]["R:propname"] = "value"; 492 } catch(DavPropException e) { 493 raised = true; 494 assert(!prop["name"].has("R:propname")); 495 } 496 497 assert(raised); 498 } 499 500 @("add same name tag with different ns") 501 unittest { 502 auto prop = new DavProp; 503 auto prop1 = new DavProp; 504 auto prop2 = new DavProp; 505 506 prop1.namespaceAttr = "NS1"; 507 prop2.namespaceAttr = "NS2"; 508 509 prop1.name = "somename"; 510 prop2.name = "somename"; 511 512 prop1.value = "somevalue"; 513 prop2.value = "somevalue"; 514 515 prop["somename"] = prop1; 516 prop["somename"] = prop2; 517 518 assert(prop.length == 2); 519 } 520 521 @("get a namespace name from a prefixed tag") 522 unittest { 523 auto prop = new DavProp; 524 prop.namespaces["D"] = "DAV:"; 525 prop.name = "root"; 526 prop["D:name"] = ""; 527 prop["D:name"]["D:val"] = ""; 528 529 assert(prop["D:name"]["D:val"].namespace == "DAV:"); 530 } 531 532 @("create FromKey") 533 unittest { 534 auto p = DavProp.FromKey("A:DAV:", "value"); 535 536 assert(p.name == "A"); 537 assert(p.namespace == "DAV:"); 538 assert(p.value == "value"); 539 } 540 541 @("create FromKeyAndList") 542 unittest { 543 string[][string] value; 544 value["href:DAV:"] = []; 545 value["href:DAV:"] ~= [ "value" ]; 546 547 auto p = DavProp.FromKeyAndList("A:DAV:", value); 548 549 assert(p.toString == `<A xmlns="DAV:"><href xmlns="DAV:">value</href></A>`); 550 } 551 552 @("attributes to string") 553 unittest { 554 auto p = new DavProp("DAV:", "A"); 555 p.attribute["name"] = "value"; 556 557 assert(p.toString == `<A name="value" xmlns="DAV:"/>`); 558 } 559 560 561 string normalize(const string text) { 562 bool isWs(const char ch) { 563 if(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') 564 return true; 565 566 return false; 567 } 568 569 immutable char getNext(ulong pos) { 570 foreach(i; pos+1..text.length) 571 if(!isWs(text[i])) return text[i]; 572 573 return ' '; 574 } 575 576 string normalisedText; 577 ulong spaces = 0; 578 bool isInsdeTag; 579 string quote; 580 581 foreach(i; 0..text.length) { 582 auto ch = text[i]; 583 auto nextCh = getNext(i); 584 585 if(spaces > 0 && isWs(ch)) 586 normalisedText ~= ch; 587 else if (!isWs(ch) || quote.length == 1) { 588 normalisedText ~= ch; 589 590 if(ch == '<') { 591 spaces = 0; 592 isInsdeTag = true; 593 } else { 594 spaces = 1; 595 } 596 597 //do not remove space inside tag quotes 598 if(isInsdeTag && quote.length == 1 && ch == quote[0]) 599 quote = ""; 600 else if(isInsdeTag && quote.length == 0 && (ch == '"' || ch == '\'')) 601 quote ~= ch; 602 603 if(isInsdeTag && quote.length == 0) { 604 if(ch == '/' || ch == '=') 605 spaces = 0; 606 607 if(nextCh == '/' || nextCh == '=' || nextCh == '>') 608 spaces = 0; 609 610 if(ch == '>') 611 isInsdeTag = false; 612 } 613 614 if(!isInsdeTag && ch == '>' && nextCh == '<') 615 spaces = 0; 616 } 617 618 if(ch == ' ' && spaces > 0) 619 spaces--; 620 } 621 622 return normalisedText; 623 } 624 625 DavProp parseXMLProp(string xmlText, DavProp parent = null) { 626 xmlText = normalize(xmlText); 627 628 DavProp root; 629 DavProp[] nodes; 630 DavProp currentNode = new DavProp; 631 632 ulong i = 0; 633 ulong prevTagEnd = 0; 634 635 while(i < xmlText.length) { 636 auto ch = xmlText[i]; 637 ulong len; 638 DavProp node; 639 640 if(ch == '<') 641 //parse the node 642 node = parseXMLPropNode(xmlText[i..$], parent, len); 643 else 644 //parse the text 645 node = parseXMLPropText(xmlText[i..$], len); 646 647 nodes ~= node; 648 i += len+1; 649 } 650 651 if(nodes.length == 0) return new DavProp; 652 else { 653 root = new DavProp; 654 if(nodes.length == 1 && nodes[0].isText) 655 root.value = nodes[0].value; 656 else 657 root.properties = nodes; 658 } 659 660 return root; 661 } 662 663 664 DavProp parseXMLPropText(string xmlNodeText, ref ulong end) { 665 if(xmlNodeText[0] == '<') 666 throw new DavPropException(HTTPStatus.internalServerError, "The text node can not start with `<`."); 667 668 DavProp node = new DavProp; 669 670 while(end < xmlNodeText.length) { 671 if(xmlNodeText[end] == '<') 672 break; 673 674 node.value ~= xmlNodeText[end]; 675 676 end++; 677 } 678 679 return node; 680 } 681 682 DavProp parseXMLPropNode(string xmlNodeText, DavProp parent, ref ulong end) { 683 DavProp node = new DavProp; 684 node.parent = parent; 685 string startTag; 686 string[] tagPieces; 687 bool isSelfClosed; 688 689 string getTagTextContent() { 690 ulong start = end; 691 ulong tags = 1; 692 693 ulong i = end; 694 ulong searchLength = xmlNodeText.length - node.name.length - 2; 695 696 while(i < searchLength) { 697 auto ch = xmlNodeText[i]; 698 auto nextCh = xmlNodeText[i + 1]; 699 700 auto beginTag = i + 1; 701 auto endTag = i + node.name.length + 1; 702 703 //check for a tag with the same name 704 if(ch == '<' && xmlNodeText[beginTag..endTag] == node.name && 705 (xmlNodeText[endTag] == ' ' || xmlNodeText[endTag] == '/' || xmlNodeText[endTag] == '>') ) 706 tags++; 707 708 beginTag++; 709 endTag++; 710 711 //check for an end tag with the same name 712 if(ch == '<' && nextCh == '/' && xmlNodeText[beginTag..endTag] == node.name) 713 tags--; 714 715 if(tags < 0) 716 throw new DavPropException(HTTPStatus.internalServerError, "Invalid end tag for `" ~ node.name ~ "`."); 717 else if(tags == 0) 718 return xmlNodeText[end..i]; 719 720 i++; 721 } 722 723 throw new DavPropException(HTTPStatus.internalServerError, "Can not find the `" ~ node.name ~ "` end tag."); 724 } 725 726 void setNameSpaces() { 727 enum len = "xmlns:".length; 728 729 foreach(attr; tagPieces) { 730 if(attr.length > len && attr[0..len] == "xmlns:") { 731 string ns; 732 string val; 733 auto eqPos = attr.indexOf('='); 734 if(eqPos != -1) { 735 ns = attr[len..eqPos]; 736 val = attr[eqPos+2..$-1]; 737 node.addNamespace(ns, val); 738 } 739 } 740 } 741 } 742 743 string getAttrValue(string name) { 744 auto len = name.length + 1; 745 746 foreach(attr; tagPieces) { 747 auto pos = attr.indexOf(name ~ "="); 748 if(pos == 0) 749 return attr[len+1..$-1]; 750 } 751 752 return ""; 753 } 754 755 if(xmlNodeText[0] != '<') 756 throw new DavPropException(HTTPStatus.internalServerError, "The node must start with `<`."); 757 758 ///get tag attributes 759 foreach(i; 0..xmlNodeText.length) { 760 auto ch = xmlNodeText[i]; 761 if(ch == '>') { 762 startTag = xmlNodeText[1..i]; 763 end = i + 1; 764 break; 765 } 766 } 767 768 if(startTag[$-1..$] == "/") { 769 startTag = startTag[0..$-1]; 770 isSelfClosed = true; 771 } 772 773 tagPieces = startTag.split(" "); 774 775 if(tagPieces.length == 0) 776 throw new DavPropException(HTTPStatus.internalServerError, "Invalid node content."); 777 else { 778 if(startTag.indexOf("xmlns:") != -1) 779 setNameSpaces; 780 781 if(startTag.indexOf("xmlns=") != -1) 782 node.namespaceAttr = getAttrValue("xmlns"); 783 } 784 785 if(tagPieces[0] == "?xml") 786 isSelfClosed = true; 787 node.name = tagPieces[0]; 788 789 //get the tag content 790 if(!isSelfClosed) { 791 string tagTextContent = getTagTextContent; 792 793 auto insideNodes = parseXMLProp(tagTextContent, node); 794 795 if(insideNodes.length == 0 && insideNodes.isText) { 796 node.value = insideNodes.value; 797 } else if(insideNodes.length == 0) { 798 node[insideNodes.name] = insideNodes; 799 } else { 800 foreach(string key, val; insideNodes) { 801 node.addChild(val); 802 } 803 } 804 805 //look for end tag 806 end += tagTextContent.length + node.name.length + 2; 807 } else 808 end--; 809 810 return node; 811 } 812 813 @("parse a string without any tags") 814 unittest { 815 auto prop = parseXMLProp("value"); 816 assert(prop.value == "value"); 817 } 818 819 @("parse a string with a tag") 820 unittest { 821 auto prop = parseXMLProp(`<name>value</name>`)[0]; 822 assert(prop.value == "value"); 823 assert(prop.name == "name"); 824 assert(prop.toString == "<name>value</name>"); 825 } 826 827 @("parse a string without imbricated tags") 828 unittest { 829 auto prop = parseXMLProp(`<prop1>val1</prop1><prop2>val2</prop2>`); 830 assert(prop["prop1"].value == "val1"); 831 assert(prop["prop2"].value == "val2"); 832 assert(prop.toString == "<prop1>val1</prop1><prop2>val2</prop2>"); 833 } 834 835 @("parse a string with imbricated tags") 836 unittest { 837 auto prop = parseXMLProp(`<name><name>value</name></name>`)[0]; 838 839 assert(prop.name == "name"); 840 assert(prop["name"].value == "value"); 841 assert(prop.toString == "<name><name>value</name></name>"); 842 } 843 844 @("check invalid namespaces") 845 unittest { 846 auto prop = parseXMLProp(`<d:prop1>val1</d:prop1>`)[0]; 847 848 bool raised; 849 850 try prop.checkNamespacePrefixes; 851 catch(DavPropException e) raised = true; 852 853 assert(raised); 854 } 855 856 @("check valid namespaces") 857 unittest { 858 auto prop = parseXMLProp(`<cat xmlns:d="DAV:"><d:prop>val</d:prop></cat>`); 859 prop.checkNamespacePrefixes; 860 assert(prop.toString == `<cat xmlns:d="DAV:"><d:prop>val</d:prop></cat>`); 861 } 862 863 @("parse xml attribute") 864 unittest { 865 auto prop = parseXMLProp(`<cat xmlns="DAV:"></cat>`)[0]; 866 assert(prop.namespaceAttr == `DAV:`); 867 } 868 869 @("normalize xml string") 870 unittest { 871 auto text = normalize(`< cat xmlns = "DAV:" > < / cat >`); 872 assert(text == `<cat xmlns="DAV:"></cat>`); 873 } 874 875 @("normalize `/` and `<` in attr values") 876 unittest { 877 auto text = normalize(`< cat xmlns = " / < " > < / cat >`); 878 assert(text == `<cat xmlns=" / < "></cat>`); 879 } 880 881 @("access nodes without passing the ns") 882 unittest { 883 auto prop = parseXMLProp(`<d:tag1 xmlns:d="DAV:"><d:tag2>value</d:tag2></d:tag1>`)[0]; 884 assert(prop.tagName == "tag1"); 885 assert(prop["tag2"].value == "value"); 886 } 887 888 @("parse self closing tags") 889 unittest { 890 auto prop = parseXMLProp(`<a/>`)[0]; 891 assert(prop.tagName == "a"); 892 } 893 894 @("check ?xml tag parsing") 895 unittest { 896 auto prop = parseXMLProp(`<?xml><propfind xmlns="DAV:"><prop><getcontentlength xmlns="DAV:"/></prop></propfind>`); 897 assert(prop.toString == `<?xml><propfind xmlns="DAV:"><prop><getcontentlength xmlns="DAV:"/></prop></propfind>`); 898 } 899 900 @("check if the parsing fails to check the parents") 901 unittest { 902 auto prop = parseXMLProp(`<d:a xmlns:d="DAV:"><d:b><d:c/></d:b></d:a>`); 903 assert(prop.toString == `<d:a xmlns:d="DAV:"><d:b><d:c/></d:b></d:a>`); 904 } 905 906 @("check child names with similar name") 907 unittest { 908 auto prop = parseXMLProp(`<prop><prop0/></prop>`); 909 assert(prop.toString == `<prop><prop0/></prop>`); 910 } 911 912 @("allow to have childrens with the same tag name") 913 unittest { 914 auto prop = parseXMLProp(`<a><b>c1</b><b>c2</b></a>`); 915 assert(prop.toString == `<a><b>c1</b><b>c2</b></a>`); 916 } 917 918 @("value given for tag with null ns") 919 unittest { 920 auto prop = parseXMLProp(`<test xmlns="DAV:"><nonamespace xmlns="">randomvalue</nonamespace></test>`); 921 assert(prop.toString == `<test xmlns="DAV:"><nonamespace xmlns="">randomvalue</nonamespace></test>`); 922 } 923 924 @("replace namespaces with prefixes") 925 unittest { 926 auto prop = parseXMLProp(`<a xmlns:D="DAV:"><b xmlns="DAV:">randomvalue</b></a>`); 927 assert(prop.toString == `<a xmlns:D="DAV:"><D:b>randomvalue</D:b></a>`); 928 }