/** NSURL.m - Class NSURL Copyright (C) 1999 Free Software Foundation, Inc. Written by: Manuel Guesdon Date: Jan 1999 Rewrite by: Richard Frith-Macdonald Date: Jun 2002 Add'l by: Gregory John Casamento Date: Jan 2020 This file is part of the GNUstep Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA. NSURL class reference */ /* Note from Manuel Guesdon: * I've made some test to compare apple NSURL results and GNUstep NSURL results but as there this class is not very documented, some function may be incorrect * I've put 2 functions to make tests. You can add your own tests * Some functions are not implemented */ #define GS_NSURLQueryItem_IVARS \ NSString *_name; \ NSString *_value; #define GS_NSURLComponents_IVARS \ NSString *_string; \ NSString *_fragment; \ NSString *_host; \ NSString *_password; \ NSString *_path; \ NSNumber *_port; \ NSArray *_queryItems; \ NSString *_scheme; \ NSString *_user; \ NSRange _rangeOfFragment; \ NSRange _rangeOfHost; \ NSRange _rangeOfPassword; \ NSRange _rangeOfPath; \ NSRange _rangeOfPort; \ NSRange _rangeOfQuery; \ NSRange _rangeOfQueryItems; \ NSRange _rangeOfScheme; \ NSRange _rangeOfUser; \ BOOL _dirty; #import "common.h" #define EXPOSE_NSURL_IVARS 1 #import "Foundation/NSArray.h" #import "Foundation/NSAutoreleasePool.h" #import "Foundation/NSCoder.h" #import "Foundation/NSData.h" #import "Foundation/NSDictionary.h" #import "Foundation/NSError.h" #import "Foundation/NSException.h" #import "Foundation/NSFileManager.h" #import "Foundation/NSLock.h" #import "Foundation/NSMapTable.h" #import "Foundation/NSPortCoder.h" #import "Foundation/NSRunLoop.h" #import "Foundation/NSURL.h" #import "Foundation/NSURLHandle.h" #import "Foundation/NSValue.h" #import "Foundation/NSCharacterSet.h" #import "Foundation/NSString.h" #import "GNUstepBase/NSURL+GNUstepBase.h" @interface NSURL (GSPrivate) - (NSURL*) _URLBySettingPath: (NSString*)newPath; @end @implementation NSURL (GSPrivate) - (NSURL*) _URLBySettingPath: (NSString*)newPath { if ([self isFileURL]) { return [NSURL fileURLWithPath: newPath]; } else { NSURL *u; u = [[NSURL alloc] initWithScheme: [self scheme] user: [self user] password: [self password] host: [self host] port: [self port] fullPath: newPath parameterString: [self parameterString] query: [self query] fragment: [self fragment]]; return [u autorelease]; } } @end /* * Structure describing a URL. * All the char* fields may be NULL pointers, except path, which * is *always* non-null (though it may be an empty string). */ typedef struct { id absolute; // Cache absolute string or nil char *scheme; char *user; char *password; char *host; char *port; char *path; // May never be NULL char *parameters; char *query; char *fragment; BOOL pathIsAbsolute; BOOL emptyPath; BOOL hasNoPath; BOOL isGeneric; BOOL isFile; } parsedURL; #define myData ((parsedURL*)(self->_data)) #define baseData ((self->_baseURL == 0)?0:((parsedURL*)(self->_baseURL->_data))) static NSLock *clientsLock = nil; /* * Local utility functions. */ static char *buildURL(parsedURL *base, parsedURL *rel, BOOL standardize); static id clientForHandle(void *data, NSURLHandle *hdl); static char *findUp(char *str); static char *unescape(const char *from, char * to); /** * Build an absolute URL as a C string */ static char *buildURL(parsedURL *base, parsedURL *rel, BOOL standardize) { const char *rpath; char *buf; char *ptr; char *tmp; int l; unsigned int len = 1; if (NO == rel->hasNoPath) { len += 1; // trailing '/' to be added } if (rel->scheme != 0) { len += strlen(rel->scheme) + 3; // scheme:// } else if (YES == rel->isGeneric) { len += 2; // need '//' even if no scheme } if (rel->user != 0) { len += strlen(rel->user) + 1; // user...@ } if (rel->password != 0) { len += strlen(rel->password) + 1; // :password } if (rel->host != 0) { len += strlen(rel->host) + 1; // host.../ } if (rel->port != 0) { len += strlen(rel->port) + 1; // :port } if (rel->path != 0) { rpath = rel->path; } else { rpath = ""; } len += strlen(rpath) + 1; // path if (base != 0 && base->path != 0) { len += strlen(base->path) + 1; // path } if (rel->parameters != 0) { len += strlen(rel->parameters) + 1; // ;parameters } if (rel->query != 0) { len += strlen(rel->query) + 1; // ?query } if (rel->fragment != 0) { len += strlen(rel->fragment) + 1; // #fragment } ptr = buf = (char*)NSZoneMalloc(NSDefaultMallocZone(), len); if (rel->scheme != 0) { l = strlen(rel->scheme); memcpy(ptr, rel->scheme, l); ptr += l; *ptr++ = ':'; } if (rel->isGeneric == YES || rel->user != 0 || rel->password != 0 || rel->host != 0 || rel->port != 0) { *ptr++ = '/'; *ptr++ = '/'; if (rel->user != 0 || rel->password != 0) { if (rel->user != 0) { l = strlen(rel->user); memcpy(ptr, rel->user, l); ptr += l; } if (rel->password != 0) { *ptr++ = ':'; l = strlen(rel->password); memcpy(ptr, rel->password, l); ptr += l; } if (rel->host != 0 || rel->port != 0) { *ptr++ = '@'; } } if (rel->host != 0) { l = strlen(rel->host); memcpy(ptr, rel->host, l); ptr += l; } if (rel->port != 0) { *ptr++ = ':'; l = strlen(rel->port); memcpy(ptr, rel->port, l); ptr += l; } } /* * Now build path. */ tmp = ptr; if (rel->pathIsAbsolute == YES) { if (rel->hasNoPath == NO) { *tmp++ = '/'; } l = strlen(rpath); memcpy(tmp, rpath, l); tmp += l; } else if (base == 0) { l = strlen(rpath); memcpy(tmp, rpath, l); tmp += l; } else if (rpath[0] == 0) { if (base->hasNoPath == NO) { *tmp++ = '/'; } if (base->path) { l = strlen(base->path); memcpy(tmp, base->path, l); tmp += l; } } else { char *start = base->path; if (start != 0) { char *end = strrchr(start, '/'); if (end != 0) { *tmp++ = '/'; memcpy(tmp, start, end - start); tmp += (end - start); } } *tmp++ = '/'; l = strlen(rpath); memcpy(tmp, rpath, l); tmp += l; } *tmp = '\0'; if (standardize == YES) { /* * Compact '/./' to '/' and strip any trailing '/.' */ tmp = ptr; while (*tmp != '\0') { if (tmp[0] == '/' && tmp[1] == '.' && (tmp[2] == '/' || tmp[2] == '\0')) { /* * Ensure we don't remove the leading '/' */ if (tmp == ptr && tmp[2] == '\0') { tmp[1] = '\0'; } else { l = strlen(&tmp[2]) + 1; memmove(tmp, &tmp[2], l); } } else { tmp++; } } /* * Reduce any sequence of '/' characters to a single '/' */ tmp = ptr; while (*tmp != '\0') { if (tmp[0] == '/' && tmp[1] == '/') { l = strlen(&tmp[1]) + 1; memmove(tmp, &tmp[1], l); } else { tmp++; } } /* * Reduce any '/something/../' sequence to '/' and a trailing * "/something/.." to "" */ tmp = ptr; while ((tmp = findUp(tmp)) != 0) { char *next = &tmp[3]; while (tmp > ptr) { if (*--tmp == '/') { break; } } /* * Ensure we don't remove the leading '/' */ if (tmp == ptr && *next == '\0') { tmp[1] = '\0'; } else { l = strlen(next) + 1; memmove(tmp, next, l); } } /* * if we have an empty path, we standardize to a single slash. */ tmp = ptr; if (*tmp == '\0') { memcpy(tmp, "/", 2); } } ptr = &ptr[strlen(ptr)]; if (rel->parameters != 0) { *ptr++ = ';'; l = strlen(rel->parameters); memcpy(ptr, rel->parameters, l); ptr += l; } if (rel->query != 0) { *ptr++ = '?'; l = strlen(rel->query); memcpy(ptr, rel->query, l); ptr += l; } if (rel->fragment != 0) { *ptr++ = '#'; l = strlen(rel->fragment); memcpy(ptr, rel->fragment, l); ptr += l; } *ptr = '\0'; return buf; } static id clientForHandle(void *data, NSURLHandle *hdl) { id client = nil; if (data != 0) { [clientsLock lock]; client = RETAIN((id)NSMapGet((NSMapTable*)data, hdl)); [clientsLock unlock]; } return AUTORELEASE(client); } /** * Locate a '/../ or trailing '/..' */ static char *findUp(char *str) { while (*str != '\0') { if (str[0] == '/' && str[1] == '.' && str[2] == '.' && (str[3] == '/' || str[3] == '\0')) { return str; } str++; } return 0; } /* * Check a string to see if it contains only legal data characters * or percent escape sequences. */ static BOOL legal(const char *str, const char *extras) { const char *mark = "-_.!~*'()"; if (str != 0) { while (*str != 0) { if (*str == '%' && isxdigit(str[1]) && isxdigit(str[2])) { str += 3; } else if (isalnum(*str)) { str++; } else if (strchr(mark, *str) != 0) { str++; } else if (strchr(extras, *str) != 0) { str++; } else { return NO; } } } return YES; } /* * Convert percent escape sequences to individual characters. */ static char *unescape(const char *from, char * to) { while (*from != '\0') { if (*from == '%') { unsigned char c; from++; if (isxdigit(*from)) { if (*from <= '9') { c = *from - '0'; } else if (*from <= 'F') { c = *from - 'A' + 10; } else { c = *from - 'a' + 10; } from++; } else { c = 0; // Avoid compiler warning [NSException raise: NSGenericException format: @"Bad percent escape sequence in URL string"]; } c <<= 4; if (isxdigit(*from)) { if (*from <= '9') { c |= *from - '0'; } else if (*from <= 'F') { c |= *from - 'A' + 10; } else { c |= *from - 'a' + 10; } from++; *to++ = c; } else { [NSException raise: NSGenericException format: @"Bad percent escape sequence in URL string"]; } } else { *to++ = *from++; } } *to = '\0'; return to; } @implementation NSURL static NSCharacterSet *fileCharSet = nil; static NSUInteger urlAlign; + (id) fileURLWithPath: (NSString*)aPath { return AUTORELEASE([[NSURL alloc] initFileURLWithPath: aPath]); } + (id) fileURLWithPath: (NSString*)aPath isDirectory: (BOOL)isDir { return AUTORELEASE([[NSURL alloc] initFileURLWithPath: aPath isDirectory: isDir]); } + (id) fileURLWithPath: (NSString *)aPath isDirectory: (BOOL)isDir relativeToURL: (NSURL *)baseURL { return AUTORELEASE([[NSURL alloc] initFileURLWithPath: aPath isDirectory: isDir relativeToURL: baseURL]); } + (id)fileURLWithPath: (NSString *)aPath relativeToURL: (NSURL *)baseURL { return AUTORELEASE([[NSURL alloc] initFileURLWithPath: aPath relativeToURL: baseURL]); } + (id) fileURLWithPathComponents: (NSArray*)components { return [self fileURLWithPath: [NSString pathWithComponents: components]]; } + (void) initialize { if (clientsLock == nil) { NSGetSizeAndAlignment(@encode(parsedURL), NULL, &urlAlign); clientsLock = [NSLock new]; [[NSObject leakAt: &clientsLock] release]; ASSIGN(fileCharSet, [NSCharacterSet characterSetWithCharactersInString: @"!$&'()*+,-./0123456789:=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"]); } } + (id) URLWithString: (NSString*)aUrlString { return AUTORELEASE([[NSURL alloc] initWithString: aUrlString]); } + (id) URLWithString: (NSString*)aUrlString relativeToURL: (NSURL*)aBaseUrl { return AUTORELEASE([[NSURL alloc] initWithString: aUrlString relativeToURL: aBaseUrl]); } + (id) URLByResolvingAliasFileAtURL: (NSURL*)url options: (NSURLBookmarkResolutionOptions)options error: (NSError**)error { // TODO: unimplemented return nil; } - (id) initFileURLWithPath: (NSString *)aPath { /* isDirectory flag will be overwritten if a directory exists. */ return [self initFileURLWithPath: aPath isDirectory: NO relativeToURL: nil]; } - (id) initFileURLWithPath: (NSString *)aPath isDirectory: (BOOL)isDir { return [self initFileURLWithPath: aPath isDirectory: isDir relativeToURL: nil]; } - (id) initFileURLWithPath: (NSString *)aPath relativeToURL: (NSURL *)baseURL { /* isDirectory flag will be overwritten if a directory exists. */ return [self initFileURLWithPath: aPath isDirectory: NO relativeToURL: baseURL]; } - (id) initFileURLWithPath: (NSString *)aPath isDirectory: (BOOL)isDir relativeToURL: (NSURL *)baseURL { NSFileManager *mgr = [NSFileManager defaultManager]; BOOL flag = NO; if (nil == aPath) { NSString *name = NSStringFromClass([self class]); RELEASE(self); [NSException raise: NSInvalidArgumentException format: @"[%@ %@] nil string parameter", name, NSStringFromSelector(_cmd)]; } if ([aPath isAbsolutePath] == NO) { if (baseURL) { /* Append aPath to baseURL */ aPath = [[baseURL relativePath] stringByAppendingPathComponent: aPath]; } else { aPath = [[mgr currentDirectoryPath] stringByAppendingPathComponent: aPath]; } } if ([mgr fileExistsAtPath: aPath isDirectory: &flag] == YES) { if ([aPath isAbsolutePath] == NO) { aPath = [aPath stringByStandardizingPath]; } isDir = flag; } if (isDir == YES && [aPath hasSuffix:@"/"] == NO) { aPath = [aPath stringByAppendingString: @"/"]; } return [self initWithScheme: NSURLFileScheme host: @"" path: aPath]; } - (id) initWithScheme: (NSString*)aScheme host: (NSString*)aHost path: (NSString*)aPath { NSRange r; NSString *auth = nil; NSString *aUrlString = [NSString alloc]; if ([aScheme isEqualToString: @"file"]) { aPath = [aPath stringByAddingPercentEncodingWithAllowedCharacters: fileCharSet]; } else { aPath = [aPath stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; } r = [aHost rangeOfString: @"@"]; /* Allow for authentication (username:password) before actual host. */ if (r.length > 0) { auth = [aHost substringToIndex: r.location]; aHost = [aHost substringFromIndex: NSMaxRange(r)]; } /* Add square brackets around ipv6 address if necessary */ if ([[aHost componentsSeparatedByString: @":"] count] > 2 && [aHost hasPrefix: @"["] == NO) { aHost = [NSString stringWithFormat: @"[%@]", aHost]; } if (auth != nil) { aHost = [NSString stringWithFormat: @"%@@%@", auth, aHost]; } if ([aPath length] > 0) { /* * For MacOS-X compatibility, assume a path component with * a leading slash is intended to have that slash separating * the host from the path as specified in the RFC1738 */ if ([aPath hasPrefix: @"/"] == YES) { aUrlString = [aUrlString initWithFormat: @"%@://%@%@", aScheme, aHost, aPath]; } #if defined(_WIN32) /* On Windows file systems, an absolute file path can begin with * a drive letter. The first component in an absolute path * (e.g. C:) has to be enclosed by a leading slash. * * "file:///c:/path/to/file" */ else if ([aScheme isEqualToString: @"file"] && [aPath characterAtIndex:1] == ':') { aUrlString = [aUrlString initWithFormat: @"%@:///%@%@", aScheme, aHost, aPath]; } #endif else { aUrlString = [aUrlString initWithFormat: @"%@://%@/%@", aScheme, aHost, aPath]; } } else { aUrlString = [aUrlString initWithFormat: @"%@://%@/", aScheme, aHost]; } self = [self initWithString: aUrlString relativeToURL: nil]; RELEASE(aUrlString); return self; } - (id) initWithString: (NSString*)aUrlString { self = [self initWithString: aUrlString relativeToURL: nil]; return self; } - (id) initWithString: (NSString*)aUrlString relativeToURL: (NSURL*)aBaseUrl { /* RFC 2396 'reserved' characters ... * as modified by RFC2732 * static const char *reserved = ";/?:@&=+$,[]"; */ /* Same as reserved set but allow the hash character in a path too. */ static const char *filepath = ";/?:@&=+$,[]#"; if (nil == aUrlString) { RELEASE(self); return nil; // OSX behavior is to give up. } if ([aUrlString isKindOfClass: [NSString class]] == NO) { NSString *name = NSStringFromClass([self class]); RELEASE(self); [NSException raise: NSInvalidArgumentException format: @"[%@ %@] bad string parameter", name, NSStringFromSelector(_cmd)]; } if (aBaseUrl != nil && [aBaseUrl isKindOfClass: [NSURL class]] == NO) { NSString *name = NSStringFromClass([self class]); RELEASE(self); [NSException raise: NSInvalidArgumentException format: @"[%@ %@] bad base URL parameter", name, NSStringFromSelector(_cmd)]; } ASSIGNCOPY(_urlString, aUrlString); ASSIGN(_baseURL, [aBaseUrl absoluteURL]); NS_DURING { parsedURL *buf; parsedURL *base = baseData; unsigned size = [_urlString length]; char *end; char *start; char *ptr; BOOL usesFragments = YES; BOOL usesParameters = YES; BOOL usesQueries = YES; BOOL canBeGeneric = YES; size += sizeof(parsedURL) + urlAlign + 1; buf = _data = (parsedURL*)NSZoneMalloc(NSDefaultMallocZone(), size); memset(buf, '\0', size); start = end = (char*)&buf[1]; NS_DURING { [_urlString getCString: start maxLength: size encoding: NSASCIIStringEncoding]; } NS_HANDLER { /* OSX behavior when given non-ascii text is to return nil. */ RELEASE(self); return nil; } NS_ENDHANDLER /* * Parse the scheme if possible. */ ptr = start; if (isalpha(*ptr)) { ptr++; while (isalnum(*ptr) || *ptr == '+' || *ptr == '-' || *ptr == '.') { ptr++; } if (*ptr == ':') { buf->scheme = start; // Got scheme. *ptr = '\0'; // Terminate it. end = &ptr[1]; /* * Standardise uppercase to lower. */ while (--ptr > start) { if (isupper(*ptr)) { *ptr = tolower(*ptr); } } } } start = end; if (buf->scheme != 0 && base != 0 && 0 != strcmp(buf->scheme, base->scheme)) { /* The relative URL is of a different scheme to the base ... * so it's actually an absolute URL without a base. */ DESTROY(_baseURL); base = 0; } if (buf->scheme == 0 && base != 0) { buf->scheme = base->scheme; } /* * Set up scheme specific parsing options. */ if (buf->scheme != 0) { if (strcmp(buf->scheme, "file") == 0) { buf->isFile = YES; } else if (strcmp(buf->scheme, "data") == 0) { canBeGeneric = NO; DESTROY(_baseURL); base = 0; } else if (strcmp(buf->scheme, "mailto") == 0) { usesFragments = NO; usesParameters = NO; usesQueries = NO; } else if (strcmp(buf->scheme, "http") == 0 || strcmp(buf->scheme, "https") == 0) { buf->emptyPath = YES; } } if (canBeGeneric == YES) { /* * Parse the 'authority' * //user:password@host:port */ if (start[0] == '/' && start[1] == '/') { buf->isGeneric = YES; start = &end[2]; /* * Set 'end' to point to the start of the path, or just past * the 'authority' if there is no path. */ end = strchr(start, '/'); if (end == 0) { buf->hasNoPath = YES; end = &start[strlen(start)]; } else { *end++ = '\0'; } /* * Parser username:password part */ ptr = strchr(start, '@'); if (ptr != 0) { buf->user = start; *ptr++ = '\0'; start = ptr; if (legal(buf->user, ";:&=+$,") == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in user/password part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } ptr = strchr(buf->user, ':'); if (ptr != 0) { *ptr++ = '\0'; buf->password = ptr; } } /* * Parse host:port part */ buf->host = start; if (*start == '[') { ptr = strchr(buf->host, ']'); if (ptr == 0) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal ipv6 host address", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } else { ptr = start + 1; while (*ptr != ']') { if (*ptr != ':' && *ptr != '.' && !isxdigit(*ptr)) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal ipv6 host address", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } ptr++; } } ptr = strchr(ptr, ':'); } else { ptr = strchr(buf->host, ':'); } if (ptr != 0) { const char *str; *ptr++ = '\0'; buf->port = ptr; str = buf->port; while (*str != 0) { if (*str == '%' && isxdigit(str[1]) && isxdigit(str[2])) { unsigned char c; str++; if (*str <= '9') { c = *str - '0'; } else if (*str <= 'F') { c = *str - 'A' + 10; } else { c = *str - 'a' + 10; } c <<= 4; str++; if (*str <= '9') { c |= *str - '0'; } else if (*str <= 'F') { c |= *str - 'A' + 10; } else { c |= *str - 'a' + 10; } if (isdigit(c)) { str++; } else { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal port part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } else if (isdigit(*str)) { str++; } else { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in port part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } } start = end; /* Check for a legal host, unless it's an ipv6 address * (which would have been checked earlier). */ if (*buf->host != '[' && legal(buf->host, "-") == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in host part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } /* * If we have an authority component, * this must be an absolute URL */ buf->pathIsAbsolute = YES; base = 0; } else { if (base != 0) { buf->isGeneric = base->isGeneric; } if (*start == '/') { buf->pathIsAbsolute = YES; start++; } } if (usesFragments == YES) { /* * Strip fragment string from end of url. */ ptr = strchr(start, '#'); if (ptr != 0) { *ptr++ = '\0'; if (*ptr != 0) { buf->fragment = ptr; } } if (buf->fragment == 0 && base != 0) { buf->fragment = base->fragment; } if (legal(buf->fragment, filepath) == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in fragment part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } if (usesQueries == YES) { /* * Strip query string from end of url. */ ptr = strchr(start, '?'); if (ptr != 0) { *ptr++ = '\0'; if (*ptr != 0) { buf->query = ptr; } } if (buf->query == 0 && base != 0) { buf->query = base->query; } if (legal(buf->query, filepath) == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in query part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } if (usesParameters == YES) { /* * Strip parameters string from end of url. */ ptr = strchr(start, ';'); if (ptr != 0) { *ptr++ = '\0'; if (*ptr != 0) { buf->parameters = ptr; } } if (buf->parameters == 0 && base != 0) { buf->parameters = base->parameters; } if (legal(buf->parameters, filepath) == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in parameters part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } if (buf->isFile == YES) { buf->user = 0; buf->password = 0; if (base != 0 && base->host != 0) { buf->host = base->host; } else if (buf->host != 0 && *buf->host == 0) { buf->host = 0; } buf->port = 0; buf->isGeneric = YES; } else if (base != 0 && buf->user == 0 && buf->password == 0 && buf->host == 0 && buf->port == 0) { buf->user = base->user; buf->password = base->password; buf->host = base->host; buf->port = base->port; } } /* * Store the path. */ buf->path = start; if (0 == base && '\0' == *buf->path && NO == buf->pathIsAbsolute) { buf->hasNoPath = YES; } if (legal(buf->path, filepath) == NO) { [NSException raise: NSInvalidArgumentException format: @"[%@ %@](%@, %@) " @"illegal character in path part", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aUrlString, aBaseUrl]; } } NS_HANDLER { NSDebugLog(@"%@", localException); DESTROY(self); } NS_ENDHANDLER return self; } - (void) dealloc { if (_clients != 0) { NSFreeMapTable(_clients); _clients = 0; } if (_data != 0) { DESTROY(myData->absolute); NSZoneFree([self zone], _data); _data = 0; } DESTROY(_urlString); DESTROY(_baseURL); DEALLOC } - (id) copyWithZone: (NSZone*)zone { if (NSShouldRetainWithZone(self, zone) == NO) { return [[[self class] allocWithZone: zone] initWithString: _urlString relativeToURL: _baseURL]; } else { return RETAIN(self); } } - (NSString*) description { NSString *dscr = _urlString; if (_baseURL != nil) { dscr = [dscr stringByAppendingFormat: @" -- %@", _baseURL]; } return dscr; } - (void) encodeWithCoder: (NSCoder*)aCoder { if ([aCoder allowsKeyedCoding]) { [aCoder encodeObject: _baseURL forKey: @"NS.base"]; [aCoder encodeObject: _urlString forKey: @"NS.relative"]; } else { [aCoder encodeObject: _urlString]; [aCoder encodeObject: _baseURL]; } } - (NSUInteger) hash { return [[self absoluteString] hash]; } - (id) initWithCoder: (NSCoder*)aCoder { NSURL *base; NSString *rel; if ([aCoder allowsKeyedCoding]) { base = [aCoder decodeObjectForKey: @"NS.base"]; rel = [aCoder decodeObjectForKey: @"NS.relative"]; } else { rel = [aCoder decodeObject]; base = [aCoder decodeObject]; } if (nil == rel) { rel = @""; } self = [self initWithString: rel relativeToURL: base]; return self; } - (BOOL) isEqual: (id)other { if (other == nil || [other isKindOfClass: [NSURL class]] == NO) { return NO; } return [[self absoluteString] isEqualToString: [other absoluteString]]; } - (NSString*) absoluteString { NSString *absString = myData->absolute; if (absString == nil) { char *url = buildURL(baseData, myData, NO); unsigned len = strlen(url); absString = [[NSString alloc] initWithCStringNoCopy: url length: len freeWhenDone: YES]; myData->absolute = absString; } return absString; } - (NSURL*) absoluteURL { if (_baseURL == nil) { return self; } else { return [NSURL URLWithString: [self absoluteString]]; } } - (NSURL*) baseURL { return _baseURL; } - (BOOL) checkResourceIsReachableAndReturnError: (NSError **)error { NSString *errorStr = nil; if ([self isFileURL]) { NSFileManager *mgr = [NSFileManager defaultManager]; NSString *path = [self path]; if ([mgr fileExistsAtPath: path]) { if (![mgr isReadableFileAtPath: path]) { errorStr = @"File not readable"; } } else { errorStr = @"File does not exist"; } } else { errorStr = @"No file URL"; } if ((errorStr != nil) && (error != NULL)) { NSDictionary *info; info = [NSDictionary dictionaryWithObjectsAndKeys: errorStr, NSLocalizedDescriptionKey, nil]; *error = [NSError errorWithDomain: @"NSURLError" code: 0 userInfo: info]; } return nil == errorStr ? YES : NO; } - (NSString*) fragment { NSString *fragment = nil; if (myData->fragment != 0) { fragment = [NSString stringWithUTF8String: myData->fragment]; } return fragment; } - (char*) _path: (char*)buf withEscapes: (BOOL)withEscapes { char *ptr = buf; char *tmp = buf; int l; *buf = '\0'; if (myData->pathIsAbsolute == YES) { if (myData->hasNoPath == NO) { *tmp++ = '/'; *tmp = '\0'; } if (myData->path != 0) { l = strlen(myData->path); memcpy(tmp, myData->path, l + 1); } } else if (nil == _baseURL) { if (myData->path != 0) { l = strlen(myData->path); memcpy(tmp, myData->path, l + 1); } } else if (0 == myData->path || 0 == *myData->path) { if (baseData->hasNoPath == NO) { *tmp++ = '/'; *tmp = '\0'; } if (baseData->path != 0) { l = strlen(baseData->path); memcpy(tmp, baseData->path, l + 1); } } else { char *start = baseData->path; char *end = (start == 0) ? 0 : strrchr(start, '/'); if (end != 0) { *tmp++ = '/'; strncpy(tmp, start, end - start); tmp += end - start; } *tmp++ = '/'; *tmp = '\0'; if (myData->path != 0) { l = strlen(myData->path); memcpy(tmp, myData->path, l + 1); } } if (!withEscapes) { unescape(buf, buf); } #if defined(_WIN32) /* On Windows a file URL path may be of the form C:\xxx or \\xxx, * and in both cases we should not insert the leading slash. * Also the vertical bar symbol may have been used instead of the * colon, so we need to convert that. */ if (myData->isFile == YES) { if ((ptr[1] && isalpha(ptr[1])) && (ptr[2] == ':' || ptr[2] == '|') && (ptr[3] == '\0' || ptr[3] == '/' || ptr[3] == '\\')) { ptr[2] = ':'; ptr++; // remove leading slash } else if (ptr[1] == '\\' && ptr[2] == '\\') { ptr++; // remove leading slash } } #endif return ptr; } - (NSString*) host { NSString *host = nil; if (myData->host != 0) { char buf[strlen(myData->host)+1]; if (*myData->host == '[') { char *end = unescape(myData->host + 1, buf); if (end > buf && end[-1] == ']') { end[-1] = '\0'; } } else { unescape(myData->host, buf); } host = [NSString stringWithUTF8String: buf]; } return host; } - (BOOL) isFileURL { return myData->isFile; } - (NSString*) lastPathComponent { return [[self path] lastPathComponent]; } - (BOOL) isFileReferenceURL { return NO; } - (NSURL *) fileReferenceURL { if ([self isFileURL]) { return self; } return nil; } - (NSURL *) filePathURL { if ([self isFileURL]) { return self; } return nil; } - (BOOL) getResourceValue: (id*)value forKey: (NSString *)key error: (NSError**)error { // TODO: unimplemented return NO; } - (void) loadResourceDataNotifyingClient: (id)client usingCache: (BOOL)shouldUseCache { NSURLHandle *handle = [self URLHandleUsingCache: shouldUseCache]; NSData *d; if (shouldUseCache == YES && (d = [handle availableResourceData]) != nil) { /* * We already have cached data we should use. */ if ([client respondsToSelector: @selector(URL:resourceDataDidBecomeAvailable:)]) { [client URL: self resourceDataDidBecomeAvailable: d]; } if ([client respondsToSelector: @selector(URLResourceDidFinishLoading:)]) { [client URLResourceDidFinishLoading: self]; } } else { if (client != nil) { [clientsLock lock]; if (_clients == 0) { _clients = NSCreateMapTable (NSObjectMapKeyCallBacks, NSNonRetainedObjectMapValueCallBacks, 0); } NSMapInsert((NSMapTable*)_clients, (void*)handle, (void*)client); [clientsLock unlock]; [handle addClient: self]; } /* * Kick off the load process. */ [handle loadInBackground]; } } - (NSString*) parameterString { NSString *parameters = nil; if (myData->parameters != 0) { parameters = [NSString stringWithUTF8String: myData->parameters]; } return parameters; } - (NSString*) password { NSString *password = nil; if (myData->password != 0) { char buf[strlen(myData->password)+1]; unescape(myData->password, buf); password = [NSString stringWithUTF8String: buf]; } return password; } - (NSString*) _pathWithEscapes: (BOOL)withEscapes { NSString *path = nil; if (YES == myData->isGeneric || 0 == myData->scheme) { unsigned int len = 3; if (_baseURL != nil) { if (baseData->path && *baseData->path) { len += strlen(baseData->path); } else if (baseData->hasNoPath == NO) { len++; } } if (myData->path && *myData->path) { len += strlen(myData->path); } else if (myData->hasNoPath == NO) { len++; } if (len > 3) { char buf[len]; char *ptr; char *tmp; ptr = [self _path: buf withEscapes: withEscapes]; /* Remove any trailing '/' from the path for MacOS-X compatibility. */ tmp = ptr + strlen(ptr) - 1; if (tmp > ptr && *tmp == '/') { *tmp = '\0'; } path = [NSString stringWithUTF8String: ptr]; } else if (YES == myData->emptyPath) { /* OSX seems to use an empty string for some schemes, * though it normally uses nil. */ path = @""; } } return path; } - (NSString*) path { return [self _pathWithEscapes: NO]; } - (NSArray*) pathComponents { return [[self path] pathComponents]; } - (NSString*) pathExtension { return [[self path] pathExtension]; } - (NSNumber*) port { NSNumber *port = nil; if (myData->port != 0) { char buf[strlen(myData->port)+1]; unescape(myData->port, buf); port = [NSNumber numberWithUnsignedShort: atol(buf)]; } return port; } - (id) propertyForKey: (NSString*)propertyKey { NSURLHandle *handle = [self URLHandleUsingCache: YES]; return [handle propertyForKey: propertyKey]; } - (NSString*) query { NSString *query = nil; if (myData->query != 0) { query = [NSString stringWithUTF8String: myData->query]; } return query; } - (NSString*) relativePath { if (nil == _baseURL) { return [self path]; } else { NSString *path = nil; if (myData->path != 0) { char buf[strlen(myData->path) + 1]; strcpy(buf, myData->path); unescape(buf, buf); path = [NSString stringWithUTF8String: buf]; } return path; } } - (NSString*) relativeString { return _urlString; } /* Encode bycopy unless explicitly requested otherwise. */ - (id) replacementObjectForPortCoder: (NSPortCoder*)aCoder { if ([aCoder isByref] == NO) return self; return [super replacementObjectForPortCoder: aCoder]; } - (NSData*) resourceDataUsingCache: (BOOL)shouldUseCache { NSURLHandle *handle = [self URLHandleUsingCache: YES]; NSData *data = nil; if ([handle status] == NSURLHandleLoadSucceeded) { data = [handle availableResourceData]; } if (shouldUseCache == NO || [handle status] != NSURLHandleLoadSucceeded) { data = [handle loadInForeground]; } if (nil == data) { data = [handle availableResourceData]; } return data; } - (NSString*) resourceSpecifier { if (YES == myData->isGeneric) { NSRange range = [_urlString rangeOfString: @"://"]; if (range.length > 0) { NSString *specifier; /* MacOSX compatibility - in the case where there is no * host in the URL, just return the path (without the "//"). * For all other cases we return the whole specifier. */ if (nil == [self host]) { specifier = [_urlString substringFromIndex: NSMaxRange(range)]; } else { specifier = [_urlString substringFromIndex: range.location+1]; } return specifier; } else { /* * Cope with URLs missing net_path info - :/... */ range = [_urlString rangeOfString: @":"]; if (range.length > 0) { return [_urlString substringFromIndex: range.location + 1]; } else { return _urlString; } } } else { return [NSString stringWithUTF8String: myData->path]; } } - (NSString*) scheme { NSString *scheme = nil; if (myData->scheme != 0) { scheme = [NSString stringWithUTF8String: myData->scheme]; } return scheme; } - (BOOL) setProperty: (id)property forKey: (NSString*)propertyKey { NSURLHandle *handle = [self URLHandleUsingCache: YES]; return [handle writeProperty: property forKey: propertyKey]; } - (BOOL) setResourceData: (NSData*)data { NSURLHandle *handle = [self URLHandleUsingCache: YES]; if (handle == nil) { return NO; } if ([handle writeData: data] == NO) { return NO; } if ([handle loadInForeground] == nil) { return NO; } return YES; } - (NSURL*) standardizedURL { char *url = buildURL(baseData, myData, YES); unsigned len = strlen(url); NSString *str; NSURL *tmp; str = [[NSString alloc] initWithCStringNoCopy: url length: len freeWhenDone: YES]; tmp = [NSURL URLWithString: str]; RELEASE(str); return tmp; } - (NSURLHandle*) URLHandleUsingCache: (BOOL)shouldUseCache { NSURLHandle *handle = nil; if (shouldUseCache) { handle = [NSURLHandle cachedHandleForURL: self]; } if (handle == nil) { Class c = [NSURLHandle URLHandleClassForURL: self]; if (c != 0) { handle = [[c alloc] initWithURL: self cached: shouldUseCache]; IF_NO_ARC([handle autorelease];) } } return handle; } - (NSString*) user { NSString *user = nil; if (myData->user != 0) { char buf[strlen(myData->user)+1]; unescape(myData->user, buf); user = [NSString stringWithUTF8String: buf]; } return user; } - (NSURL*) URLByAppendingPathComponent: (NSString*)pathComponent { return [self _URLBySettingPath: [[self path] stringByAppendingPathComponent: pathComponent]]; } - (NSURL*) URLByAppendingPathExtension: (NSString*)pathExtension { return [self _URLBySettingPath: [[self path] stringByAppendingPathExtension: pathExtension]]; } - (NSURL*) URLByDeletingLastPathComponent { return [self _URLBySettingPath: [[self path] stringByDeletingLastPathComponent]]; } - (NSURL*) URLByDeletingPathExtension { return [self _URLBySettingPath: [[self path] stringByDeletingPathExtension]]; } - (NSURL*) URLByResolvingSymlinksInPath { if ([self isFileURL]) { return [NSURL fileURLWithPath: [[self path] stringByResolvingSymlinksInPath]]; } return self; } - (NSURL*) URLByStandardizingPath { if ([self isFileURL]) { return [NSURL fileURLWithPath: [[self path] stringByStandardizingPath]]; } return self; } - (NSURL *) URLByAppendingPathComponent: (NSString *)pathComponent isDirectory: (BOOL)isDirectory { NSString *path = [[self path] stringByAppendingPathComponent: pathComponent]; if (isDirectory) { path = [path stringByAppendingString: @"/"]; } return [self _URLBySettingPath: path]; } - (void) URLHandle: (NSURLHandle*)sender resourceDataDidBecomeAvailable: (NSData*)newData { id c = clientForHandle(_clients, sender); if ([c respondsToSelector: @selector(URL:resourceDataDidBecomeAvailable:)]) { [c URL: self resourceDataDidBecomeAvailable: newData]; } } - (void) URLHandle: (NSURLHandle*)sender resourceDidFailLoadingWithReason: (NSString*)reason { id c = clientForHandle(_clients, sender); RETAIN(self); [sender removeClient: self]; if (c != nil) { [clientsLock lock]; NSMapRemove((NSMapTable*)_clients, (void*)sender); [clientsLock unlock]; if ([c respondsToSelector: @selector(URL:resourceDidFailLoadingWithReason:)]) { [c URL: self resourceDidFailLoadingWithReason: reason]; } } RELEASE(self); } - (void) URLHandleResourceDidBeginLoading: (NSURLHandle*)sender { } - (void) URLHandleResourceDidCancelLoading: (NSURLHandle*)sender { id c = clientForHandle(_clients, sender); RETAIN(self); [sender removeClient: self]; if (c != nil) { [clientsLock lock]; NSMapRemove((NSMapTable*)_clients, (void*)sender); [clientsLock unlock]; if ([c respondsToSelector: @selector(URLResourceDidCancelLoading:)]) { [c URLResourceDidCancelLoading: self]; } } RELEASE(self); } - (void) URLHandleResourceDidFinishLoading: (NSURLHandle*)sender { id c = clientForHandle(_clients, sender); RETAIN(self); [sender removeClient: self]; if (c != nil) { [clientsLock lock]; NSMapRemove((NSMapTable*)_clients, (void*)sender); [clientsLock unlock]; if ([c respondsToSelector: @selector(URLResourceDidFinishLoading:)]) { [c URLResourceDidFinishLoading: self]; } } RELEASE(self); } @end /** * An informal protocol to which clients may conform if they wish to be * notified of the progress in loading a URL for them. NSURL conforms to * this protocol but all methods are implemented as no-ops. See also * the [(NSURLHandleClient)] protocol. */ @implementation NSObject (NSURLClient) - (void) URL: (NSURL*)sender resourceDataDidBecomeAvailable: (NSData*)newBytes { } - (void) URL: (NSURL*)sender resourceDidFailLoadingWithReason: (NSString*)reason { } - (void) URLResourceDidCancelLoading: (NSURL*)sender { } - (void) URLResourceDidFinishLoading: (NSURL*)sender { } @end @implementation NSURL (GNUstepBase) - (NSString*) fullPath { NSString *path = nil; if (YES == myData->isGeneric || 0 == myData->scheme) { unsigned int len = 3; if (_baseURL != nil) { if (baseData->path && *baseData->path) { len += strlen(baseData->path); } else if (baseData->hasNoPath == NO) { len++; } } if (myData->path && *myData->path) { len += strlen(myData->path); } else if (myData->hasNoPath == NO) { len++; } if (len > 3) { char buf[len]; char *ptr; ptr = [self _path: buf withEscapes: NO]; path = [NSString stringWithUTF8String: ptr]; } } return path; } - (NSString*) pathWithEscapes { NSString *path = nil; if (YES == myData->isGeneric || 0 == myData->scheme) { unsigned int len = 3; if (_baseURL != nil) { if (baseData->path && *baseData->path) { len += strlen(baseData->path); } else if (baseData->hasNoPath == NO) { len++; } } if (myData->path && *myData->path) { len += strlen(myData->path); } else if (myData->hasNoPath == NO) { len++; } if (len > 3) { char buf[len]; char *ptr; ptr = [self _path: buf withEscapes: YES]; path = [NSString stringWithUTF8String: ptr]; } } return path; } @end #define GSInternal NSURLQueryItemInternal #include "GSInternal.h" GS_PRIVATE_INTERNAL(NSURLQueryItem) @implementation NSURLQueryItem // Creating query items. + (instancetype)queryItemWithName: (NSString *)name value: (NSString *)value { NSURLQueryItem *newQueryItem = [[NSURLQueryItem alloc] initWithName: name value: value]; return AUTORELEASE(newQueryItem); } - (instancetype) init { self = [self initWithName: nil value: nil]; if (self != nil) { } return self; } - (instancetype) initWithName: (NSString *)name value: (NSString *)value { self = [super init]; if (self != nil) { GS_CREATE_INTERNAL(NSURLQueryItem); if (name) { ASSIGNCOPY(internal->_name, name); } else { /* OSX behaviour is to set an empty string for nil name property */ ASSIGN(internal->_name, @""); } ASSIGNCOPY(internal->_value, value); } return self; } - (void) dealloc { if (GS_EXISTS_INTERNAL) { RELEASE(internal->_name); RELEASE(internal->_value); GS_DESTROY_INTERNAL(NSURLQueryItem); } DEALLOC } // Reading a name and value from a query - (NSString *) name { return internal->_name; } - (NSString *) value { return internal->_value; } - (id) initWithCoder: (NSCoder *)acoder { if ((self = [super init]) != nil) { if ([acoder allowsKeyedCoding]) { internal->_name = [acoder decodeObjectForKey: @"NS.name"]; internal->_value = [acoder decodeObjectForKey: @"NS.value"]; } else { internal->_name = [acoder decodeObject]; internal->_value = [acoder decodeObject]; } } return self; } - (void) encodeWithCoder: (NSCoder *)acoder { if ([acoder allowsKeyedCoding]) { [acoder encodeObject: internal->_name forKey: @"NS.name"]; [acoder encodeObject: internal->_value forKey: @"NS.value"]; } else { [acoder encodeObject: internal->_name]; [acoder encodeObject: internal->_value]; } } - (id) copyWithZone: (NSZone *)zone { return [[[self class] allocWithZone: zone] initWithName: internal->_name value: internal->_value]; } @end #undef GSInternal #define GSInternal NSURLComponentsInternal #include "GSInternal.h" GS_PRIVATE_INTERNAL(NSURLComponents) @implementation NSURLComponents static NSCharacterSet *queryItemCharSet = nil; + (void) initialize { if (nil == queryItemCharSet) { ENTER_POOL NSMutableCharacterSet *m; m = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; /* Rationale: if a query item contained an ampersand we would not be * able to tell where one name/value pair ends and the next starts, * so we cannot permit that character in an item. Similarly, if a * query item contained an equals sign we would not be able to tell * where the name ends and the value starts, so we cannot permit that * character either. */ [m removeCharactersInString: @"&="]; queryItemCharSet = [m copy]; RELEASE(m); LEAVE_POOL } } // Creating URL components... + (instancetype) componentsWithString: (NSString *)urlString { return AUTORELEASE([[NSURLComponents alloc] initWithString: urlString]); } + (instancetype) componentsWithURL: (NSURL *)url resolvingAgainstBaseURL: (BOOL)resolve { return AUTORELEASE([[NSURLComponents alloc] initWithURL: url resolvingAgainstBaseURL: resolve]); } - (instancetype) init { self = [super init]; if (self != nil) { GS_CREATE_INTERNAL(NSURLComponents); internal->_rangeOfFragment = NSMakeRange(NSNotFound, 0); internal->_rangeOfHost = NSMakeRange(NSNotFound, 0); internal->_rangeOfPassword = NSMakeRange(NSNotFound, 0); internal->_rangeOfPath = NSMakeRange(NSNotFound, 0); internal->_rangeOfPort = NSMakeRange(NSNotFound, 0); internal->_rangeOfQuery = NSMakeRange(NSNotFound, 0); internal->_rangeOfScheme = NSMakeRange(NSNotFound, 0); internal->_rangeOfUser = NSMakeRange(NSNotFound, 0); } return self; } - (instancetype) initWithString: (NSString *)URLString { /* OSX behavior is to return nil for a string which cannot be * used to initialize valid NSURL object */ NSURL *url = [NSURL URLWithString: URLString]; if (url) { return [self initWithURL: url resolvingAgainstBaseURL: NO]; } else { RELEASE(self); return nil; } } - (instancetype) initWithURL: (NSURL *)url resolvingAgainstBaseURL: (BOOL)resolve { self = [self init]; if (self != nil) { NSURL *tempURL = url; if (resolve) { tempURL = [url absoluteURL]; } [self setURL: tempURL]; } return self; } - (void) dealloc { if (GS_EXISTS_INTERNAL) { RELEASE(internal->_string); RELEASE(internal->_fragment); RELEASE(internal->_host); RELEASE(internal->_password); RELEASE(internal->_path); RELEASE(internal->_port); RELEASE(internal->_queryItems); RELEASE(internal->_scheme); RELEASE(internal->_user); GS_DESTROY_INTERNAL(NSURLComponents); } DEALLOC } - (id) copyWithZone: (NSZone *)zone { return [[NSURLComponents allocWithZone: zone] initWithURL: [self URL] resolvingAgainstBaseURL: NO]; } // Regenerate URL when components are changed... - (void) _regenerateURL { NSMutableString *urlString; NSString *component; NSUInteger location; NSUInteger len; if (internal->_dirty == NO) { return; } urlString = [[NSMutableString alloc] initWithCapacity: 1000]; location = 0; // Build up the URL from components... if (internal->_scheme != nil) { component = [self scheme]; [urlString appendString: component]; len = [component length]; internal->_rangeOfScheme = NSMakeRange(location, len); [urlString appendString: @"://"]; location += len + 3; } else { internal->_rangeOfScheme = NSMakeRange(NSNotFound, 0); } if (internal->_user != nil) { if (internal->_password != nil) { component = [self percentEncodedUser]; len = [component length]; [urlString appendString: component]; internal->_rangeOfUser = NSMakeRange(location, len); [urlString appendString: @":"]; location += len + 1; component = [self percentEncodedPassword]; len = [component length]; [urlString appendString: component]; internal->_rangeOfUser = NSMakeRange(location, len); [urlString appendString: @"@"]; location += len + 1; } else { component = [self percentEncodedUser]; len = [component length]; [urlString appendString: component]; internal->_rangeOfUser = NSMakeRange(location, len); [urlString appendString: @"@"]; location += len + 1; } } if (internal->_host != nil) { component = [self percentEncodedHost]; len = [component length]; [urlString appendString: component]; internal->_rangeOfHost = NSMakeRange(location, len); location += len; } if (internal->_port != nil) { component = [[self port] stringValue]; len = [component length]; [urlString appendString: @":"]; location += 1; [urlString appendString: component]; internal->_rangeOfPort = NSMakeRange(location, len); location += len; } /* FIXME ... if the path is empty we still need a '/' do we not? */ if (internal->_path != nil) { component = [self percentEncodedPath]; len = [component length]; [urlString appendString: component]; internal->_rangeOfPath = NSMakeRange(location, len); location += len; } if ([internal->_queryItems count] > 0) { component = [self percentEncodedQuery]; len = [component length]; [urlString appendString: @"?"]; location += 1; [urlString appendString: component]; internal->_rangeOfQuery = NSMakeRange(location, len); location += len; } if (internal->_fragment != nil) { component = [self percentEncodedFragment]; len = [component length]; [urlString appendString: @"#"]; location += 1; [urlString appendString: component]; internal->_rangeOfFragment = NSMakeRange(location, len); } ASSIGNCOPY(internal->_string, urlString); RELEASE(urlString); internal->_dirty = NO; } // Getting the URL - (NSString *) string { [self _regenerateURL]; return internal->_string; } - (void) setString: (NSString *)urlString { NSURL *url = [NSURL URLWithString: urlString]; [self setURL: url]; } - (NSURL *) URL { return AUTORELEASE([[NSURL alloc] initWithScheme: [self scheme] user: [self user] password: [self password] host: [self host] port: [self port] fullPath: [self path] parameterString: nil query: [self percentEncodedQuery] fragment: [self fragment]]); } - (void) setURL: (NSURL *)url { // Set all the components... [self setScheme: [url scheme]]; [self setHost: [url host]]; [self setPort: [url port]]; [self setUser: [url user]]; [self setPassword: [url password]]; [self setPath: [url path]]; [self setPercentEncodedQuery:[url query]]; [self setFragment: [url fragment]]; } - (NSURL *) URLRelativeToURL: (NSURL *)baseURL { return nil; } // Accessing Components in Native Format - (NSString *) fragment { return internal->_fragment; } - (void) setFragment: (NSString *)fragment { ASSIGNCOPY(internal->_fragment, fragment); internal->_dirty = YES; } - (NSString *) host { return internal->_host; } - (void) setHost: (NSString *)host { ASSIGNCOPY(internal->_host, host); internal->_dirty = YES; } - (NSString *) password { return internal->_password; } - (void) setPassword: (NSString *)password { ASSIGNCOPY(internal->_password, password); internal->_dirty = YES; } - (NSString *) path { return internal->_path; } - (void) setPath: (NSString *)path { ASSIGNCOPY(internal->_path, path); internal->_dirty = YES; } - (NSNumber *) port { return internal->_port; } - (void) setPort: (NSNumber *)port { ASSIGNCOPY(internal->_port, port); internal->_dirty = YES; } - (NSString *) query { NSString *result = nil; if (internal->_queryItems != nil) { NSMutableString *query = nil; NSURLQueryItem *item = nil; NSEnumerator *en; en = [internal->_queryItems objectEnumerator]; while ((item = (NSURLQueryItem *)[en nextObject]) != nil) { NSString *name = [item name]; NSString *value = [item value]; if (nil == query) { query = [[NSMutableString alloc] initWithCapacity: 1000]; } else { [query appendString: @"&"]; } [query appendString: name]; if (value != nil) { [query appendString: @"="]; [query appendString: value]; } } if (nil == query) { result = @""; } else { result = AUTORELEASE([query copy]); RELEASE(query); } } return result; } - (void) _setQuery: (NSString *)query fromPercentEncodedString: (BOOL)encoded { /* Parse according to https://developer.apple.com/documentation/foundation/nsurlcomponents/1407752-queryitems?language=objc */ if (nil == query) { [self setQueryItems: nil]; } else if ([query length] == 0) { [self setQueryItems: [NSArray array]]; } else { NSMutableArray *result = [NSMutableArray arrayWithCapacity: 5]; NSArray *items = [query componentsSeparatedByString: @"&"]; NSEnumerator *en = [items objectEnumerator]; id item = nil; while ((item = [en nextObject]) != nil) { NSURLQueryItem *qitem; NSString *name; NSString *value; if ([item length] == 0) { name = @""; value = nil; } else { NSRange r = [item rangeOfString: @"="]; if (0 == r.length) { /* No '=' found in query item. */ name = item; value = nil; } else { name = [item substringToIndex: r.location]; value = [item substringFromIndex: NSMaxRange(r)]; } } if (encoded) { name = [name stringByRemovingPercentEncoding]; value = [value stringByRemovingPercentEncoding]; } qitem = [NSURLQueryItem queryItemWithName: name value: value]; [result addObject: qitem]; } [self setQueryItems: result]; } } - (void) setQuery: (NSString *)query { [self _setQuery: query fromPercentEncodedString: NO]; } - (NSArray *) queryItems { return AUTORELEASE(RETAIN(internal->_queryItems)); } - (void) setQueryItems: (NSArray *)queryItems { ASSIGNCOPY(internal->_queryItems, queryItems); internal->_dirty = YES; } - (NSString *) scheme { return internal->_scheme; } - (void) setScheme: (NSString *)scheme { ASSIGNCOPY(internal->_scheme, scheme); internal->_dirty = YES; } - (NSString *) user { return internal->_user; } - (void) setUser: (NSString *)user { ASSIGNCOPY(internal->_user, user); internal->_dirty = YES; } // Accessing Components in PercentEncoded Format - (NSString *) percentEncodedFragment { return [internal->_fragment stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLFragmentAllowedCharacterSet]]; } - (void) setPercentEncodedFragment: (NSString *)fragment { [self setFragment: [fragment stringByRemovingPercentEncoding]]; } - (NSString *) percentEncodedHost { return [internal->_host stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLHostAllowedCharacterSet]]; } - (void) setPercentEncodedHost: (NSString *)host { [self setHost: [host stringByRemovingPercentEncoding]]; } - (NSString *) percentEncodedPassword { return [internal->_password stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLPasswordAllowedCharacterSet]]; } - (void) setPercentEncodedPassword: (NSString *)password { [self setPassword: [password stringByRemovingPercentEncoding]]; } - (NSString *) percentEncodedPath { return [internal->_path stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLPathAllowedCharacterSet]]; } - (void) setPercentEncodedPath: (NSString *)path { [self setPath: [path stringByRemovingPercentEncoding]]; } - (NSString *) percentEncodedQuery { NSString *result = nil; if (internal->_queryItems != nil) { NSMutableString *query = nil; NSURLQueryItem *item = nil; NSEnumerator *en; en = [[self percentEncodedQueryItems] objectEnumerator]; while ((item = (NSURLQueryItem *)[en nextObject]) != nil) { NSString *name = [item name]; NSString *value = [item value]; if (nil == query) { query = [[NSMutableString alloc] initWithCapacity: 1000]; } else { [query appendString: @"&"]; } [query appendString: name]; if (value != nil) { [query appendString: @"="]; [query appendString: value]; } } if (nil == query) { result = @""; } else { result = AUTORELEASE([query copy]); RELEASE(query); } } return result; } - (void) setPercentEncodedQuery: (NSString *)query { [self _setQuery: query fromPercentEncodedString: YES]; } - (NSArray *) percentEncodedQueryItems { NSArray *result = nil; if (internal->_queryItems != nil) { NSMutableArray *items; NSEnumerator *en = [internal->_queryItems objectEnumerator]; NSURLQueryItem *i = nil; items = [[NSMutableArray alloc] initWithCapacity: [internal->_queryItems count]]; while ((i = [en nextObject]) != nil) { NSURLQueryItem *ni; NSString *name = [i name]; NSString *value = [i value]; name = [name stringByAddingPercentEncodingWithAllowedCharacters: queryItemCharSet]; value = [value stringByAddingPercentEncodingWithAllowedCharacters: queryItemCharSet]; ni = [NSURLQueryItem queryItemWithName: name value: value]; [items addObject: ni]; } result = AUTORELEASE([items copy]); RELEASE(items); } return result; } - (void) setPercentEncodedQueryItems: (NSArray *)queryItems { NSMutableArray *items = nil; if (queryItems != nil) { NSEnumerator *en = [queryItems objectEnumerator]; NSURLQueryItem *i = nil; items = [NSMutableArray arrayWithCapacity: [queryItems count]]; while ((i = [en nextObject]) != nil) { NSString *name; NSString *value; NSURLQueryItem *ni; name = [[i name] stringByRemovingPercentEncoding]; value = [[i value] stringByRemovingPercentEncoding]; ni = [NSURLQueryItem queryItemWithName: name value: value]; [items addObject: ni]; } } [self setQueryItems: items]; } - (NSString *) percentEncodedUser { return [internal->_user stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLUserAllowedCharacterSet]]; } - (void) setPercentEncodedUser: (NSString *)user { [self setUser: [user stringByRemovingPercentEncoding]]; } // Locating components of the URL string representation - (NSRange) rangeOfFragment { [self _regenerateURL]; return internal->_rangeOfFragment; } - (NSRange) rangeOfHost { [self _regenerateURL]; return internal->_rangeOfHost; } - (NSRange) rangeOfPassword { [self _regenerateURL]; return internal->_rangeOfPassword; } - (NSRange) rangeOfPath { [self _regenerateURL]; return internal->_rangeOfPath; } - (NSRange) rangeOfPort { [self _regenerateURL]; return internal->_rangeOfPort; } - (NSRange) rangeOfQuery { [self _regenerateURL]; return internal->_rangeOfQuery; } - (NSRange) rangeOfScheme { [self _regenerateURL]; return internal->_rangeOfScheme; } - (NSRange) rangeOfUser { [self _regenerateURL]; return internal->_rangeOfUser; } @end