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 }