ObjFW  Check-in [c75059a52f]

Overview
Comment:OFURI: Rewrite parser to support URIs
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: c75059a52f35f7e20ccb75694204c6b17985e674bc9799008394b00fc02d99aa
User & Date: js on 2022-10-03 00:39:58
Other Links: manifest | tags
References
2022-10-03
00:47 Ticket [44ec7f4c75] Add support for URIs status still Open with 3 other changes artifact: 8e35b34057 user: js
Context
2022-10-03
19:10
OFURI: Always parse query and fragment check-in: 88765bceeb user: js tags: trunk
00:39
OFURI: Rewrite parser to support URIs check-in: c75059a52f user: js tags: trunk
2022-10-02
21:04
RuntimeARCTests: Limit Windows hack to x86_64 check-in: 8d5e62c432 user: js tags: trunk
Changes

Modified src/OFEmbeddedURIHandler.m from [4e495a4837] to [d66851cc9f].

72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
72
73
74
75
76
77
78

79
80
81
82
83
84
85
86







-
+







}

@implementation OFEmbeddedURIHandler
- (OFStream *)openItemAtURI: (OFURI *)URI mode: (OFString *)mode
{
	const char *path;

	if (![URI.scheme isEqual: @"objfw-embedded"] || URI.host != nil ||
	if (![URI.scheme isEqual: @"objfw-embedded"] || URI.host.length > 0 ||
	    URI.port != nil || URI.user != nil || URI.password != nil ||
	    URI.query != nil || URI.fragment != nil)
		@throw [OFInvalidArgumentException exception];

	if (![mode isEqual: @"r"])
		@throw [OFOpenItemFailedException exceptionWithURI: URI
							      mode: mode

Modified src/OFURI.m from [bd23469364] to [91ee9c8850].

428
429
430
431
432
433
434
435

436
437
438


439
440
441
442
443

444
445
446
447
448

449
450

451
452
453

454
455
456
457
458
459




460
461
462
463
464






465
466
467


468
469
470

471

472
473

474
475

476
477
478


479
480
481
482
483

484

485
486
487
488




489
490
491
492

493
494
495
496
497
498
499
500



501
502
503

504
505
506
507
508



509
510
511
512
513
514
515




516
517
518
519
520
521






522
523
524
525
526
527


528
529
530
531


532
533
534



535
536
537

538
539
540
541

542
543
544
545
546


547
548
549
550

551
552


553
554

555
556
557
558
559
560
561




562
563
564
565
566
567
568
569
570
571
572
573






















574
575
576


577
578
579
580







581
582
583
584




585
586
587
588










589
590
591



592
593
594
595
596





















597


598

599
600
601
602



603
604
605
606
607
608
609
610
611
612
613
614













615
616
617







618
619


620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
428
429
430
431
432
433
434

435



436
437





438

439



440


441



442






443
444
445
446
447




448
449
450
451
452
453
454


455
456

457
458
459

460


461


462
463


464
465

466



467

468
469



470
471
472
473




474


475





476
477
478
479


480





481
482
483

484





485
486
487
488
489





490
491
492
493
494
495




496

497
498
499
500


501
502



503
504
505



506
507



508
509
510



511
512
513



514


515
516
517

518
519
520
521
522



523
524
525
526












527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549


550
551




552
553
554
555
556
557
558
559



560
561
562
563
564
565


566
567
568
569
570
571
572
573
574
575
576


577
578
579
580




581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604

605




606
607
608












609
610
611
612
613
614
615
616
617
618
619
620
621



622
623
624
625
626
627
628
629

630
631
632
633
634
635
636
637
638
639
640


641
642
643
644
645
646
647







-
+
-
-
-
+
+
-
-
-
-
-
+
-

-
-
-
+
-
-
+
-
-
-
+
-
-
-
-
-
-
+
+
+
+

-
-
-
-
+
+
+
+
+
+

-
-
+
+
-


+
-
+
-
-
+
-
-
+

-
-
+
+
-

-
-
-
+
-
+

-
-
-
+
+
+
+
-
-
-
-
+
-
-

-
-
-
-
-
+
+
+

-
-
+
-
-
-
-
-
+
+
+
-

-
-
-
-
-
+
+
+
+

-
-
-
-
-
+
+
+
+
+
+
-
-
-
-

-
+
+


-
-
+
+
-
-
-
+
+
+
-
-
-
+

-
-
-
+


-
-
-
+
+

-
-
-
+
-
-
+
+

-
+




-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
+
+
-
-
-
-
+
+
+
+
+
+
+

-
-
-
+
+
+
+


-
-
+
+
+
+
+
+
+
+
+
+

-
-
+
+
+

-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
-
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+

-
+
+









-
-







		    isDirectory: (bool)isDirectory
{
	return [[[self alloc] initFileURIWithPath: path
				      isDirectory: isDirectory] autorelease];
}
#endif

- (instancetype)initWithString: (OFString *)string
static void
{
	char *UTF8String, *UTF8String2 = NULL;

parseUserInfo(OFURI *self, const char *UTF8String, size_t length)
{
	self = [super init];

	@try {
		void *pool = objc_autoreleasePoolPush();
		char *tmp, *tmp2;
	const char *colon;
		bool isIPv6Host = false;

		UTF8String = UTF8String2 = OFStrDup(string.UTF8String);

		if ((tmp = strchr(UTF8String, ':')) == NULL)
	if ((colon = memchr(UTF8String, ':', length)) != NULL) {
			@throw [OFInvalidFormatException exception];

		self->_percentEncodedUser = [[OFString alloc]
		if (strncmp(tmp, "://", 3) != 0)
			@throw [OFInvalidFormatException exception];

		    initWithUTF8String: UTF8String
		for (tmp2 = UTF8String; tmp2 < tmp; tmp2++)
			*tmp2 = OFASCIIToLower(*tmp2);

		_percentEncodedScheme = [[OFString alloc]
		    initWithUTF8String: UTF8String
				length: tmp - UTF8String];
				length: colon - UTF8String];
		self->_percentEncodedPassword = [[OFString alloc]
		    initWithUTF8String: colon + 1
				length: length - (colon - UTF8String) - 1];

		OFURIVerifyIsEscaped(_percentEncodedScheme,
		    [OFCharacterSet URISchemeAllowedCharacterSet]);

		UTF8String = tmp + 3;
		OFURIVerifyIsEscaped(self->_percentEncodedPassword,
		    [OFCharacterSet URIPasswordAllowedCharacterSet]);
	} else
		self->_percentEncodedUser = [[OFString alloc]
		    initWithUTF8String: UTF8String
				length: length];

		if ((tmp = strchr(UTF8String, '/')) != NULL) {
			*tmp = '\0';
	OFURIVerifyIsEscaped(self->_percentEncodedUser,
	    [OFCharacterSet URIUserAllowedCharacterSet]);
			tmp++;
		}

static void
		if ((tmp2 = strchr(UTF8String, '@')) != NULL) {
parseHostPort(OFURI *self, const char *UTF8String, size_t length)
			char *tmp3;

{
			*tmp2 = '\0';
			tmp2++;
	OFString *portString;

			if ((tmp3 = strchr(UTF8String, ':')) != NULL) {
				*tmp3 = '\0';
	if (*UTF8String == '[') {
		const char *end = memchr(UTF8String, ']', length);
				tmp3++;

				_percentEncodedUser = [[OFString alloc]
				    initWithUTF8String: UTF8String];
				_percentEncodedPassword = [[OFString alloc]
		if (end == NULL)
				    initWithUTF8String: tmp3];
			@throw [OFInvalidFormatException exception];

				OFURIVerifyIsEscaped(_percentEncodedPassword,
				    [OFCharacterSet
				    URIPasswordAllowedCharacterSet]);
		for (const char *iter = UTF8String + 1; iter < end; iter++)
			if (!OFASCIIIsDigit(*iter) && *iter != ':' &&
			    (*iter < 'a' || *iter > 'f') &&
			    (*iter < 'A' || *iter > 'F'))
			} else
				_percentEncodedUser = [[OFString alloc]
				    initWithUTF8String: UTF8String];

				@throw [OFInvalidFormatException exception];
			OFURIVerifyIsEscaped(_percentEncodedUser,
			    [OFCharacterSet URIUserAllowedCharacterSet]);

			UTF8String = tmp2;
		}

		if (UTF8String[0] == '[') {
			tmp2 = UTF8String++;
		self->_percentEncodedHost = [[OFString alloc]
		    initWithUTF8String: UTF8String
				length: end - UTF8String + 1];

			while (OFASCIIIsDigit(*UTF8String) ||
			    *UTF8String == ':' ||
		length -= (end - UTF8String) + 1;
			    (*UTF8String >= 'a' && *UTF8String <= 'f') ||
			    (*UTF8String >= 'A' && *UTF8String <= 'F'))
				UTF8String++;

			if (*UTF8String != ']')
		UTF8String = end + 1;
	} else {
		const char *colon = memchr(UTF8String, ':', length);
				@throw [OFInvalidFormatException exception];

			UTF8String++;

			_percentEncodedHost = [[OFString alloc]
			    initWithUTF8String: tmp2
					length: UTF8String - tmp2];
		if (colon != NULL) {
			self->_percentEncodedHost = [[OFString alloc]
			    initWithUTF8String: UTF8String
					length: colon - UTF8String];

			if (*UTF8String == ':') {
				OFString *portString;

				tmp2 = ++UTF8String;

			length -= colon - UTF8String;
			UTF8String = colon;
		} else {
			self->_percentEncodedHost = [[OFString alloc]
			    initWithUTF8String: UTF8String
					length: length];
				while (*UTF8String != '\0') {
					if (!OFASCIIIsDigit(*UTF8String))
						@throw [OFInvalidFormatException
						    exception];

					UTF8String++;
			UTF8String += length;
			length = 0;
				}

				portString = [OFString
				    stringWithUTF8String: tmp2
		OFURIVerifyIsEscaped(self->_percentEncodedHost,
		    [OFCharacterSet URIHostAllowedCharacterSet]);
						  length: UTF8String - tmp2];

				if (portString.length == 0 ||
	}

	if (length == 0)
				    portString.unsignedLongLongValue > 65535)
					@throw [OFInvalidFormatException
					    exception];
		return;

				_port = [[OFNumber alloc] initWithUnsignedShort:
				    portString.unsignedLongLongValue];
			} else if (*UTF8String != '\0')
	if (length <= 1 || *UTF8String != ':')
				@throw [OFInvalidFormatException exception];

			isIPv6Host = true;
		} else if ((tmp2 = strchr(UTF8String, ':')) != NULL) {
			OFString *portString;
	UTF8String++;
	length--;

			*tmp2 = '\0';
			tmp2++;

	for (size_t i = 0; i < length; i++)
			_percentEncodedHost = [[OFString alloc]
			    initWithUTF8String: UTF8String];
		if (!OFASCIIIsDigit(UTF8String[i]))
			@throw [OFInvalidFormatException exception];

			portString = [OFString stringWithUTF8String: tmp2];
	portString = [OFString stringWithUTF8String: UTF8String length: length];

			if (portString.unsignedLongLongValue > 65535)
				@throw [OFInvalidFormatException exception];

			_port = [[OFNumber alloc] initWithUnsignedShort:
			    portString.unsignedLongLongValue];
		} else {
	self->_port = [[OFNumber alloc] initWithUnsignedShort:
	    (unsigned short)portString.unsignedLongLongValue];
}

			_percentEncodedHost = [[OFString alloc]
			    initWithUTF8String: UTF8String];

			if (_percentEncodedHost.length == 0) {
				[_percentEncodedHost release];
				_percentEncodedHost = nil;
			}
		}

		if (_percentEncodedHost != nil && !isIPv6Host)
			OFURIVerifyIsEscaped(_percentEncodedHost,
			    [OFCharacterSet URIHostAllowedCharacterSet]);
static size_t
parseAuthority(OFURI *self, const char *UTF8String, size_t length)
{
	size_t ret;
	const char *slash, *at;

	if ((slash = memchr(UTF8String, '/', length)) != NULL)
		length = slash - UTF8String;

	ret = length;

	if ((at = memchr(UTF8String, '@', length)) != NULL) {
		parseUserInfo(self, UTF8String, at - UTF8String);

		length -= at - UTF8String + 1;
		UTF8String = at + 1;
	}

	parseHostPort(self, UTF8String, length);

	return ret;
}

		if ((UTF8String = tmp) != NULL) {
			if ((tmp = strchr(UTF8String, '#')) != NULL) {
static void
parsePathQueryFragment(OFURI *self, const char *UTF8String, size_t length)
				*tmp = '\0';

				_percentEncodedFragment = [[OFString alloc]
				    initWithUTF8String: tmp + 1];
{
	const char *fragment, *query;

	if ((fragment = memchr(UTF8String, '#', length)) != NULL) {
		self->_percentEncodedFragment = [[OFString alloc]
		    initWithUTF8String: fragment + 1
				length: length - (fragment - UTF8String) - 1];

				OFURIVerifyIsEscaped(_percentEncodedFragment,
				    [OFCharacterSet
				    URIFragmentAllowedCharacterSet]);
		OFURIVerifyIsEscaped(self->_percentEncodedFragment,
		    [OFCharacterSet URIQueryAllowedCharacterSet]);

		length = fragment - UTF8String;
			}

			if ((tmp = strchr(UTF8String, '?')) != NULL) {
				*tmp = '\0';
	if ((query = memchr(UTF8String, '?', length)) != NULL) {
		self->_percentEncodedQuery = [[OFString alloc]
		    initWithUTF8String: query + 1
				length: length - (query - UTF8String) - 1];

		OFURIVerifyIsEscaped(self->_percentEncodedQuery,
		    [OFCharacterSet URIFragmentAllowedCharacterSet]);

		length = query - UTF8String;
	}

				_percentEncodedQuery = [[OFString alloc]
				    initWithUTF8String: tmp + 1];
	self->_percentEncodedPath = [[OFString alloc]
	    initWithUTF8String: UTF8String
			length: length];

				OFURIVerifyIsEscaped(_percentEncodedQuery,
				    [OFCharacterSet
				    URIQueryAllowedCharacterSet]);
			}
	OFURIVerifyIsEscaped(self->_percentEncodedPath,
	    [OFCharacterSet URIQueryAllowedCharacterSet]);
}

- (instancetype)initWithString: (OFString *)string
{
	self = [super init];

	@try {
		void *pool = objc_autoreleasePoolPush();
		const char *UTF8String = string.UTF8String;
		size_t length = string.UTF8StringLength;
		const char *colon;

		if ((colon = strchr(UTF8String, ':')) == NULL)
			@throw [OFInvalidFormatException exception];

		_percentEncodedScheme = [[[OFString
		    stringWithUTF8String: UTF8String
				  length: colon - UTF8String] lowercaseString]
		    copy];

		OFURIVerifyIsEscaped(_percentEncodedScheme,
		    [OFCharacterSet URISchemeAllowedCharacterSet]);
			/*

			 * Some versions of GCC issue a false-positive warning
			 * (turned error) about a string overflow. This is a
			 * false positive because UTF8String is set to tmp
			 * above and tmp is either NULL or points *after* the
		length -= colon - UTF8String + 1;
		UTF8String = colon + 1;

			 * slash for the path. So all we do here is go back to
			 * that slash and restore it.
			 */
#if OF_GCC_VERSION >= 402
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpragmas"
# pragma GCC diagnostic ignored "-Wunknown-warning-option"
# pragma GCC diagnostic ignored "-Wstringop-overflow"
#endif
			UTF8String--;
			*UTF8String = '/';
#if OF_GCC_VERSION >= 402
		if (length >= 2 && UTF8String[0] == '/' &&
		    UTF8String[1] == '/') {
			size_t authorityLength;

			UTF8String += 2;
			length -= 2;

			authorityLength = parseAuthority(self,
			    UTF8String, length);

			UTF8String += authorityLength;
			length -= authorityLength;

# pragma GCC diagnostic pop
#endif

			if (length > 0)
				OFEnsure(UTF8String[0] == '/');
		}

		if (length > 0 && UTF8String[0] == '/')
			parsePathQueryFragment(self, UTF8String, length);
		else {
			_percentEncodedPath = [[OFString alloc]
			    initWithUTF8String: UTF8String];
			    initWithUTF8String: UTF8String
					length: length];

			OFURIVerifyIsEscaped(_percentEncodedPath,
			    [OFCharacterSet URIPathAllowedCharacterSet]);
		}

		objc_autoreleasePoolPop(pool);
	} @catch (id e) {
		[self release];
		@throw e;
	} @finally {
		OFFreeMemory(UTF8String2);
	}

	return self;
}

- (instancetype)initWithString: (OFString *)string relativeToURI: (OFURI *)URI
{
682
683
684
685
686
687
688
689

690
691
692
693
694
695
696
692
693
694
695
696
697
698

699
700
701
702
703
704
705
706







-
+







			if ([URI->_percentEncodedPath hasSuffix: @"/"])
				_percentEncodedPath = [[URI->_percentEncodedPath
				    stringByAppendingString: relativePath]
				    copy];
			else {
				OFMutableString *path = [OFMutableString
				    stringWithString:
				    (URI->_percentEncodedPath != nil
				    (URI->_percentEncodedPath.length > 0
				    ? URI->_percentEncodedPath
				    : @"/")];
				OFRange range = [path
				    rangeOfString: @"/"
					  options: OFStringSearchBackwards];

				if (range.location == OFNotFound)
1116
1117
1118
1119
1120
1121
1122
1123





1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137

1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1126
1127
1128
1129
1130
1131
1132

1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150

1151



1152

1153
1154
1155
1156
1157
1158
1159







-
+
+
+
+
+













-
+
-
-
-

-







	return copy;
}

- (OFString *)string
{
	OFMutableString *ret = [OFMutableString string];

	[ret appendFormat: @"%@://", _percentEncodedScheme];
	[ret appendFormat: @"%@:", _percentEncodedScheme];

	if (_percentEncodedHost != nil || _port != nil ||
	    _percentEncodedUser != nil || _percentEncodedPassword != nil)
		[ret appendString: @"//"];

	if (_percentEncodedUser != nil && _percentEncodedPassword != nil)
		[ret appendFormat: @"%@:%@@",
				   _percentEncodedUser,
				   _percentEncodedPassword];
	else if (_percentEncodedUser != nil)
		[ret appendFormat: @"%@@", _percentEncodedUser];

	if (_percentEncodedHost != nil)
		[ret appendString: _percentEncodedHost];
	if (_port != nil)
		[ret appendFormat: @":%@", _port];

	if (_percentEncodedPath != nil) {
	if (_percentEncodedPath != nil)
		if (![_percentEncodedPath hasPrefix: @"/"])
			@throw [OFInvalidFormatException exception];

		[ret appendString: _percentEncodedPath];
	}

	if (_percentEncodedQuery != nil)
		[ret appendFormat: @"?%@", _percentEncodedQuery];

	if (_percentEncodedFragment != nil)
		[ret appendFormat: @"#%@", _percentEncodedFragment];

Modified tests/OFURITests.m from [5a6871bef0] to [e76e355dc0].

21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36
37
38




39
40
41
42
43
44
45
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46
47
48
49







-
+
+









-
+
+
+
+







static OFString *URIString = @"ht%3atp://us%3Aer:p%40w@ho%3Ast:1234/"
    @"pa%3Fth?que%23ry=1&f%26oo=b%3dar#frag%23ment";

@implementation TestsAppDelegate (OFURITests)
- (void)URITests
{
	void *pool = objc_autoreleasePoolPush();
	OFURI *URI1, *URI2, *URI3, *URI4, *URI5, *URI6, *URI7;
	OFURI *URI1, *URI2, *URI3, *URI4, *URI5, *URI6, *URI7, *URI8, *URI9;
	OFURI *URI10;
	OFMutableURI *mutableURI;

	TEST(@"+[URIWithString:]",
	    R(URI1 = [OFURI URIWithString: URIString]) &&
	    R(URI2 = [OFURI URIWithString: @"http://foo:80"]) &&
	    R(URI3 = [OFURI URIWithString: @"http://bar/"]) &&
	    R(URI4 = [OFURI URIWithString: @"file:///etc/passwd"]) &&
	    R(URI5 = [OFURI URIWithString: @"http://foo/bar/qux/foo%2fbar"]) &&
	    R(URI6 = [OFURI URIWithString: @"https://[12:34::56:abcd]/"]) &&
	    R(URI7 = [OFURI URIWithString: @"https://[12:34::56:abcd]:234/"]))
	    R(URI7 = [OFURI URIWithString: @"https://[12:34::56:abcd]:234/"]) &&
	    R(URI8 = [OFURI URIWithString: @"urn:qux:foo"]) &&
	    R(URI9 = [OFURI URIWithString: @"file:/foo?query#frag"]) &&
	    R(URI10 = [OFURI URIWithString: @"file:foo@bar/qux"]))

	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #1",
	    OFInvalidFormatException,
	    [OFURI URIWithString: @"ht,tp://foo"])

	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #2",
	    OFInvalidFormatException,
64
65
66
67
68
69
70








71
72
73
74
75
76
77
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89







+
+
+
+
+
+
+
+







	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #7",
	    OFInvalidFormatException,
	    [OFURI URIWithString: @"https://[f]:/"])

	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #8",
	    OFInvalidFormatException,
	    [OFURI URIWithString: @"https://[f]:f/"])

	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #9",
	    OFInvalidFormatException,
	    [OFURI URIWithString: @"foo:bar?qux"])

	EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #10",
	    OFInvalidFormatException,
	    [OFURI URIWithString: @"foo:bar#qux"])

	TEST(@"+[URIWithString:relativeToURI:]",
	    [[[OFURI URIWithString: @"/foo" relativeToURI: URI1] string]
	    isEqual: @"ht%3atp://us%3Aer:p%40w@ho%3Ast:1234/foo"] &&
	    [[[OFURI URIWithString: @"foo/bar?q"
		     relativeToURI: [OFURI URIWithString: @"http://h/qux/quux"]]
	    string] isEqual: @"http://h/qux/foo/bar?q"] &&
134
135
136
137
138
139
140
141







142
143
144


145
146


147
148


149
150
151


152
153


154
155
156




157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175


176
177
178
179
180
181


182
183
184
185
186
187
188
146
147
148
149
150
151
152

153
154
155
156
157
158
159
160
161

162
163
164

165
166
167

168
169
170
171

172
173
174

175
176
177
178

179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

201
202
203
204
205
206
207

208
209
210
211
212
213
214
215
216







-
+
+
+
+
+
+
+


-
+
+

-
+
+

-
+
+


-
+
+

-
+
+


-
+
+
+
+


















-
+
+





-
+
+







# endif
#endif

	TEST(@"-[string]",
	    [URI1.string isEqual: URIString] &&
	    [URI2.string isEqual: @"http://foo:80"] &&
	    [URI3.string isEqual: @"http://bar/"] &&
	    [URI4.string isEqual: @"file:///etc/passwd"])
	    [URI4.string isEqual: @"file:///etc/passwd"] &&
	    [URI5.string isEqual: @"http://foo/bar/qux/foo%2fbar"] &&
	    [URI6.string isEqual: @"https://[12:34::56:abcd]/"] &&
	    [URI7.string isEqual: @"https://[12:34::56:abcd]:234/"] &&
	    [URI8.string isEqual: @"urn:qux:foo"] &&
	    [URI9.string isEqual: @"file:/foo?query#frag"] &&
	    [URI10.string isEqual: @"file:foo@bar/qux"])

	TEST(@"-[scheme]",
	    [URI1.scheme isEqual: @"ht:tp"] && [URI4.scheme isEqual: @"file"])
	    [URI1.scheme isEqual: @"ht:tp"] && [URI4.scheme isEqual: @"file"] &&
	    [URI9.scheme isEqual: @"file"] && [URI10.scheme isEqual: @"file"])

	TEST(@"-[user]", [URI1.user isEqual: @"us:er"] && URI4.user == nil)
	TEST(@"-[user]", [URI1.user isEqual: @"us:er"] && URI4.user == nil &&
	    URI10.user == nil)
	TEST(@"-[password]",
	    [URI1.password isEqual: @"p@w"] && URI4.password == nil)
	    [URI1.password isEqual: @"p@w"] && URI4.password == nil &&
	    URI10.password == nil)
	TEST(@"-[host]", [URI1.host isEqual: @"ho:st"] &&
	    [URI6.host isEqual: @"12:34::56:abcd"] &&
	    [URI7.host isEqual: @"12:34::56:abcd"])
	    [URI7.host isEqual: @"12:34::56:abcd"] &&
	    URI8.host == nil && URI9.host == nil && URI10.host == nil)
	TEST(@"-[port]", URI1.port.unsignedShortValue == 1234 &&
	    [URI4 port] == nil && URI7.port.unsignedShortValue == 234)
	    [URI4 port] == nil && URI7.port.unsignedShortValue == 234 &&
	    URI8.port == nil && URI9.port == nil && URI10.port == nil)
	TEST(@"-[path]",
	    [URI1.path isEqual: @"/pa?th"] &&
	    [URI4.path isEqual: @"/etc/passwd"])
	    [URI4.path isEqual: @"/etc/passwd"] &&
	    [URI8.path isEqual: @"qux:foo"] &&
	    [URI9.path isEqual: @"/foo"] &&
	    [URI10.path isEqual: @"foo@bar/qux"])
	TEST(@"-[pathComponents]",
	    [URI1.pathComponents isEqual:
	    [OFArray arrayWithObjects: @"/", @"pa?th", nil]] &&
	    [URI4.pathComponents isEqual:
	    [OFArray arrayWithObjects: @"/", @"etc", @"passwd", nil]] &&
	    [URI5.pathComponents isEqual:
	    [OFArray arrayWithObjects: @"/", @"bar", @"qux", @"foo/bar", nil]])
	TEST(@"-[lastPathComponent]",
	    [[[OFURI URIWithString: @"http://host/foo//bar/baz"]
	    lastPathComponent] isEqual: @"baz"] &&
	    [[[OFURI URIWithString: @"http://host/foo//bar/baz/"]
	    lastPathComponent] isEqual: @"baz"] &&
	    [[[OFURI URIWithString: @"http://host/foo/"]
	    lastPathComponent] isEqual: @"foo"] &&
	    [[[OFURI URIWithString: @"http://host/"]
	    lastPathComponent] isEqual: @"/"] &&
	    [URI5.lastPathComponent isEqual: @"foo/bar"])
	TEST(@"-[query]",
	    [URI1.query isEqual: @"que#ry=1&f&oo=b=ar"] && URI4.query == nil)
	    [URI1.query isEqual: @"que#ry=1&f&oo=b=ar"] && URI4.query == nil &&
	    [URI9.query isEqual: @"query"])
	TEST(@"-[queryItems]",
	    [URI1.queryItems isEqual: [OFArray arrayWithObjects:
	    [OFPair pairWithFirstObject: @"que#ry" secondObject: @"1"],
	    [OFPair pairWithFirstObject: @"f&oo" secondObject: @"b=ar"], nil]]);
	TEST(@"-[fragment]",
	    [URI1.fragment isEqual: @"frag#ment"] && URI4.fragment == nil)
	    [URI1.fragment isEqual: @"frag#ment"] && URI4.fragment == nil &&
	    [URI9.fragment isEqual: @"frag"] && URI10.fragment == nil)

	TEST(@"-[copy]", R(URI4 = [[URI1 copy] autorelease]))

	TEST(@"-[isEqual:]", [URI1 isEqual: URI4] && ![URI2 isEqual: URI3] &&
	    [[OFURI URIWithString: @"HTTP://bar/"] isEqual: URI3])

	TEST(@"-[hash:]", URI1.hash == URI4.hash && URI2.hash != URI3.hash)