688 lines
18 KiB
Objective-C
688 lines
18 KiB
Objective-C
//
|
|
// XMPPSRVResolver.m
|
|
//
|
|
// Originally created by Eric Chamberlain on 6/15/10.
|
|
// Based on SRVResolver by Apple, Inc.
|
|
//
|
|
|
|
#import "XMPPSRVResolver.h"
|
|
#import "XMPPLogging.h"
|
|
|
|
//#warning Fix "dns.h" issue without resorting to this ugly hack.
|
|
// This is a hack to prevent OnionKit's clobbering of the actual system's <dns.h>
|
|
//#include "/usr/include/dns.h"
|
|
|
|
#include <dns_util.h>
|
|
#include <stdlib.h>
|
|
|
|
#if ! __has_feature(objc_arc)
|
|
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
|
#endif
|
|
|
|
NSString *const XMPPSRVResolverErrorDomain = @"XMPPSRVResolverErrorDomain";
|
|
|
|
// Log levels: off, error, warn, info, verbose
|
|
#if DEBUG
|
|
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE;
|
|
#else
|
|
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@interface XMPPSRVRecord ()
|
|
|
|
@property(nonatomic, assign) NSUInteger srvResultsIndex;
|
|
@property(nonatomic, assign) NSUInteger sum;
|
|
|
|
- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord;
|
|
|
|
@end
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@implementation XMPPSRVResolver
|
|
|
|
- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq
|
|
{
|
|
NSParameterAssert(aDelegate != nil);
|
|
NSParameterAssert(dq != NULL);
|
|
|
|
if ((self = [super init]))
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
delegate = aDelegate;
|
|
delegateQueue = dq;
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_retain(delegateQueue);
|
|
#endif
|
|
|
|
if (rq)
|
|
{
|
|
resolverQueue = rq;
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_retain(resolverQueue);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
resolverQueue = dispatch_queue_create("XMPPSRVResolver", NULL);
|
|
}
|
|
|
|
resolverQueueTag = &resolverQueueTag;
|
|
dispatch_queue_set_specific(resolverQueue, resolverQueueTag, resolverQueueTag, NULL);
|
|
|
|
results = [[NSMutableArray alloc] initWithCapacity:2];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
[self stop];
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
if (resolverQueue)
|
|
dispatch_release(resolverQueue);
|
|
#endif
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Properties
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@dynamic srvName;
|
|
@dynamic timeout;
|
|
|
|
- (NSString *)srvName
|
|
{
|
|
__block NSString *result = nil;
|
|
|
|
dispatch_block_t block = ^{
|
|
result = [srvName copy];
|
|
};
|
|
|
|
if (dispatch_get_specific(resolverQueueTag))
|
|
block();
|
|
else
|
|
dispatch_sync(resolverQueue, block);
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSTimeInterval)timeout
|
|
{
|
|
__block NSTimeInterval result = 0.0;
|
|
|
|
dispatch_block_t block = ^{
|
|
result = timeout;
|
|
};
|
|
|
|
if (dispatch_get_specific(resolverQueueTag))
|
|
block();
|
|
else
|
|
dispatch_sync(resolverQueue, block);
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Private Methods
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (void)sortResults
|
|
{
|
|
NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
|
|
|
|
XMPPLogTrace();
|
|
|
|
// Sort results
|
|
NSMutableArray *sortedResults = [NSMutableArray arrayWithCapacity:[results count]];
|
|
|
|
// Sort the list by priority (lowest number first)
|
|
[results sortUsingSelector:@selector(compareByPriority:)];
|
|
|
|
/* From RFC 2782
|
|
*
|
|
* For each distinct priority level
|
|
* While there are still elements left at this priority level
|
|
*
|
|
* Select an element as specified above, in the
|
|
* description of Weight in "The format of the SRV
|
|
* RR" Section, and move it to the tail of the new
|
|
* list.
|
|
*
|
|
* The following algorithm SHOULD be used to order
|
|
* the SRV RRs of the same priority:
|
|
*/
|
|
|
|
NSUInteger srvResultsCount;
|
|
|
|
while ([results count] > 0)
|
|
{
|
|
srvResultsCount = [results count];
|
|
|
|
if (srvResultsCount == 1)
|
|
{
|
|
XMPPSRVRecord *srvRecord = results[0];
|
|
|
|
[sortedResults addObject:srvRecord];
|
|
[results removeObjectAtIndex:0];
|
|
}
|
|
else // (srvResultsCount > 1)
|
|
{
|
|
// more than two records so we need to sort
|
|
|
|
/* To select a target to be contacted next, arrange all SRV RRs
|
|
* (that have not been ordered yet) in any order, except that all
|
|
* those with weight 0 are placed at the beginning of the list.
|
|
*
|
|
* Compute the sum of the weights of those RRs, and with each RR
|
|
* associate the running sum in the selected order.
|
|
*/
|
|
|
|
NSUInteger runningSum = 0;
|
|
NSMutableArray *samePriorityRecords = [NSMutableArray arrayWithCapacity:srvResultsCount];
|
|
|
|
XMPPSRVRecord *srvRecord = results[0];
|
|
|
|
NSUInteger initialPriority = srvRecord.priority;
|
|
NSUInteger index = 0;
|
|
|
|
do
|
|
{
|
|
if (srvRecord.weight == 0)
|
|
{
|
|
// add to front of array
|
|
[samePriorityRecords insertObject:srvRecord atIndex:0];
|
|
|
|
srvRecord.srvResultsIndex = index;
|
|
srvRecord.sum = 0;
|
|
}
|
|
else
|
|
{
|
|
// add to end of array and update the running sum
|
|
[samePriorityRecords addObject:srvRecord];
|
|
|
|
runningSum += srvRecord.weight;
|
|
|
|
srvRecord.srvResultsIndex = index;
|
|
srvRecord.sum = runningSum;
|
|
}
|
|
|
|
if (++index < srvResultsCount)
|
|
{
|
|
srvRecord = results[index];
|
|
}
|
|
else
|
|
{
|
|
srvRecord = nil;
|
|
}
|
|
|
|
} while(srvRecord && (srvRecord.priority == initialPriority));
|
|
|
|
/* Then choose a uniform random number between 0 and the sum computed
|
|
* (inclusive), and select the RR whose running sum value is the
|
|
* first in the selected order which is greater than or equal to
|
|
* the random number selected.
|
|
*/
|
|
|
|
NSUInteger randomIndex = arc4random() % (runningSum + 1);
|
|
|
|
for (srvRecord in samePriorityRecords)
|
|
{
|
|
if (srvRecord.sum >= randomIndex)
|
|
{
|
|
/* The target host specified in the
|
|
* selected SRV RR is the next one to be contacted by the client.
|
|
* Remove this SRV RR from the set of the unordered SRV RRs and
|
|
* apply the described algorithm to the unordered SRV RRs to select
|
|
* the next target host. Continue the ordering process until there
|
|
* are no unordered SRV RRs. This process is repeated for each
|
|
* Priority.
|
|
*/
|
|
|
|
[sortedResults addObject:srvRecord];
|
|
[results removeObjectAtIndex:srvRecord.srvResultsIndex];
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
results = sortedResults;
|
|
|
|
XMPPLogVerbose(@"%@: Sorted results:\n%@", THIS_FILE, results);
|
|
}
|
|
|
|
- (void)succeed
|
|
{
|
|
NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
|
|
|
|
XMPPLogTrace();
|
|
|
|
[self sortResults];
|
|
|
|
id theDelegate = delegate;
|
|
NSArray *records = [results copy];
|
|
|
|
dispatch_async(delegateQueue, ^{ @autoreleasepool {
|
|
|
|
SEL selector = @selector(xmppSRVResolver:didResolveRecords:);
|
|
|
|
if ([theDelegate respondsToSelector:selector])
|
|
{
|
|
[theDelegate xmppSRVResolver:self didResolveRecords:records];
|
|
}
|
|
else
|
|
{
|
|
XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
|
|
}
|
|
|
|
}});
|
|
|
|
[self stop];
|
|
}
|
|
|
|
- (void)failWithError:(NSError *)error
|
|
{
|
|
NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
|
|
|
|
XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, error);
|
|
|
|
id theDelegate = delegate;
|
|
|
|
if (delegateQueue != NULL)
|
|
{
|
|
dispatch_async(delegateQueue, ^{ @autoreleasepool {
|
|
|
|
SEL selector = @selector(xmppSRVResolver:didNotResolveDueToError:);
|
|
|
|
if ([theDelegate respondsToSelector:selector])
|
|
{
|
|
[theDelegate xmppSRVResolver:self didNotResolveDueToError:error];
|
|
}
|
|
else
|
|
{
|
|
XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
|
|
}
|
|
|
|
}});
|
|
}
|
|
|
|
[self stop];
|
|
}
|
|
|
|
- (void)failWithDNSError:(DNSServiceErrorType)sdErr
|
|
{
|
|
XMPPLogTrace2(@"%@: %@ %i", THIS_FILE, THIS_METHOD, (int)sdErr);
|
|
|
|
[self failWithError:[NSError errorWithDomain:XMPPSRVResolverErrorDomain code:sdErr userInfo:nil]];
|
|
}
|
|
|
|
- (XMPPSRVRecord *)processRecord:(const void *)rdata length:(uint16_t)rdlen
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
// Note: This method is almost entirely from Apple's sample code.
|
|
//
|
|
// Otherwise there would be a lot more comments and explanation...
|
|
|
|
if (rdata == NULL)
|
|
{
|
|
XMPPLogWarn(@"%@: %@ - rdata == NULL", THIS_FILE, THIS_METHOD);
|
|
return nil;
|
|
}
|
|
|
|
// Rather than write a whole bunch of icky parsing code, I just synthesise
|
|
// a resource record and use <dns_util.h>.
|
|
|
|
XMPPSRVRecord *result = nil;
|
|
|
|
NSMutableData * rrData;
|
|
dns_resource_record_t * rr;
|
|
uint8_t u8; // 1 byte
|
|
uint16_t u16; // 2 bytes
|
|
uint32_t u32; // 4 bytes
|
|
|
|
rrData = [NSMutableData dataWithCapacity:(1 + 2 + 2 + 4 + 2 + rdlen)];
|
|
|
|
u8 = 0;
|
|
[rrData appendBytes:&u8 length:sizeof(u8)];
|
|
u16 = htons(kDNSServiceType_SRV);
|
|
[rrData appendBytes:&u16 length:sizeof(u16)];
|
|
u16 = htons(kDNSServiceClass_IN);
|
|
[rrData appendBytes:&u16 length:sizeof(u16)];
|
|
u32 = htonl(666);
|
|
[rrData appendBytes:&u32 length:sizeof(u32)];
|
|
u16 = htons(rdlen);
|
|
[rrData appendBytes:&u16 length:sizeof(u16)];
|
|
[rrData appendBytes:rdata length:rdlen];
|
|
|
|
// Parse the record.
|
|
|
|
rr = dns_parse_resource_record([rrData bytes], (uint32_t) [rrData length]);
|
|
if (rr != NULL)
|
|
{
|
|
NSString *target;
|
|
|
|
target = [NSString stringWithCString:rr->data.SRV->target encoding:NSASCIIStringEncoding];
|
|
if (target != nil)
|
|
{
|
|
UInt16 priority = rr->data.SRV->priority;
|
|
UInt16 weight = rr->data.SRV->weight;
|
|
UInt16 port = rr->data.SRV->port;
|
|
|
|
result = [XMPPSRVRecord recordWithPriority:priority weight:weight port:port target:target];
|
|
}
|
|
|
|
dns_free_resource_record(rr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void QueryRecordCallback(DNSServiceRef sdRef,
|
|
DNSServiceFlags flags,
|
|
uint32_t interfaceIndex,
|
|
DNSServiceErrorType errorCode,
|
|
const char * fullname,
|
|
uint16_t rrtype,
|
|
uint16_t rrclass,
|
|
uint16_t rdlen,
|
|
const void * rdata,
|
|
uint32_t ttl,
|
|
void * context)
|
|
{
|
|
// Called when we get a response to our query.
|
|
// It does some preliminary work, but the bulk of the interesting stuff
|
|
// is done in the processRecord:length: method.
|
|
|
|
XMPPSRVResolver *resolver = (__bridge XMPPSRVResolver *)context;
|
|
|
|
NSCAssert(dispatch_get_specific(resolver->resolverQueueTag), @"Invoked on incorrect queue");
|
|
|
|
XMPPLogCTrace();
|
|
|
|
if (!(flags & kDNSServiceFlagsAdd))
|
|
{
|
|
// If the kDNSServiceFlagsAdd flag is not set, the domain information is not valid.
|
|
return;
|
|
}
|
|
|
|
if (errorCode == kDNSServiceErr_NoError &&
|
|
rrtype == kDNSServiceType_SRV)
|
|
{
|
|
XMPPSRVRecord *record = [resolver processRecord:rdata length:rdlen];
|
|
if (record)
|
|
{
|
|
[resolver->results addObject:record];
|
|
}
|
|
|
|
if ( ! (flags & kDNSServiceFlagsMoreComing) )
|
|
{
|
|
[resolver succeed];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[resolver failWithDNSError:errorCode];
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Public Methods
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout
|
|
{
|
|
dispatch_block_t block = ^{ @autoreleasepool {
|
|
|
|
if (resolveInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
XMPPLogTrace2(@"%@: startWithSRVName:%@ timeout:%f", THIS_FILE, aSRVName, aTimeout);
|
|
|
|
// Save parameters
|
|
|
|
srvName = [aSRVName copy];
|
|
|
|
timeout = aTimeout;
|
|
|
|
// Check parameters
|
|
|
|
const char *srvNameCStr = [srvName cStringUsingEncoding:NSASCIIStringEncoding];
|
|
if (srvNameCStr == NULL)
|
|
{
|
|
[self failWithDNSError:kDNSServiceErr_BadParam];
|
|
return;
|
|
|
|
}
|
|
|
|
// Create DNS Service
|
|
|
|
DNSServiceErrorType sdErr;
|
|
sdErr = DNSServiceQueryRecord(&sdRef, // Pointer to unitialized DNSServiceRef
|
|
kDNSServiceFlagsReturnIntermediates, // Flags
|
|
kDNSServiceInterfaceIndexAny, // Interface index
|
|
srvNameCStr, // Full domain name
|
|
kDNSServiceType_SRV, // rrtype
|
|
kDNSServiceClass_IN, // rrclass
|
|
QueryRecordCallback, // Callback method
|
|
(__bridge void *)self); // Context pointer
|
|
|
|
if (sdErr != kDNSServiceErr_NoError)
|
|
{
|
|
[self failWithDNSError:sdErr];
|
|
return;
|
|
}
|
|
|
|
// Extract unix socket (so we can poll for events)
|
|
|
|
sdFd = DNSServiceRefSockFD(sdRef);
|
|
if (sdFd < 0)
|
|
{
|
|
// Todo...
|
|
}
|
|
|
|
// Create GCD read source for sd file descriptor
|
|
|
|
sdReadSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sdFd, 0, resolverQueue);
|
|
|
|
dispatch_source_set_event_handler(sdReadSource, ^{ @autoreleasepool {
|
|
|
|
XMPPLogVerbose(@"%@: sdReadSource_eventHandler", THIS_FILE);
|
|
|
|
// There is data to be read on the socket (or an error occurred).
|
|
//
|
|
// Invoking DNSServiceProcessResult will invoke our QueryRecordCallback,
|
|
// the callback we set when we created the sdRef.
|
|
|
|
DNSServiceErrorType dnsErr = DNSServiceProcessResult(sdRef);
|
|
if (dnsErr != kDNSServiceErr_NoError)
|
|
{
|
|
[self failWithDNSError:dnsErr];
|
|
}
|
|
|
|
}});
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_source_t theSdReadSource = sdReadSource;
|
|
#endif
|
|
DNSServiceRef theSdRef = sdRef;
|
|
|
|
dispatch_source_set_cancel_handler(sdReadSource, ^{ @autoreleasepool {
|
|
|
|
XMPPLogVerbose(@"%@: sdReadSource_cancelHandler", THIS_FILE);
|
|
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_release(theSdReadSource);
|
|
#endif
|
|
DNSServiceRefDeallocate(theSdRef);
|
|
|
|
}});
|
|
|
|
dispatch_resume(sdReadSource);
|
|
|
|
// Create timer (if requested timeout > 0)
|
|
|
|
if (timeout > 0.0)
|
|
{
|
|
timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, resolverQueue);
|
|
|
|
dispatch_source_set_event_handler(timeoutTimer, ^{ @autoreleasepool {
|
|
|
|
NSString *errMsg = @"Operation timed out";
|
|
NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg};
|
|
|
|
NSError *err = [NSError errorWithDomain:XMPPSRVResolverErrorDomain code:0 userInfo:userInfo];
|
|
|
|
[self failWithError:err];
|
|
|
|
}});
|
|
|
|
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
|
|
|
|
dispatch_source_set_timer(timeoutTimer, tt, DISPATCH_TIME_FOREVER, 0);
|
|
dispatch_resume(timeoutTimer);
|
|
}
|
|
|
|
resolveInProgress = YES;
|
|
}};
|
|
|
|
if (dispatch_get_specific(resolverQueueTag))
|
|
block();
|
|
else
|
|
dispatch_async(resolverQueue, block);
|
|
}
|
|
|
|
- (void)stop
|
|
{
|
|
dispatch_block_t block = ^{ @autoreleasepool {
|
|
|
|
XMPPLogTrace();
|
|
|
|
delegate = nil;
|
|
if (delegateQueue)
|
|
{
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_release(delegateQueue);
|
|
#endif
|
|
delegateQueue = NULL;
|
|
}
|
|
|
|
[results removeAllObjects];
|
|
|
|
if (sdReadSource)
|
|
{
|
|
// Cancel the readSource.
|
|
// It will be released from within the cancel handler.
|
|
dispatch_source_cancel(sdReadSource);
|
|
sdReadSource = NULL;
|
|
sdFd = -1;
|
|
|
|
// The sdRef will be deallocated from within the cancel handler too.
|
|
sdRef = NULL;
|
|
}
|
|
|
|
if (timeoutTimer)
|
|
{
|
|
dispatch_source_cancel(timeoutTimer);
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_release(timeoutTimer);
|
|
#endif
|
|
timeoutTimer = NULL;
|
|
}
|
|
|
|
resolveInProgress = NO;
|
|
}};
|
|
|
|
if (dispatch_get_specific(resolverQueueTag))
|
|
block();
|
|
else
|
|
dispatch_sync(resolverQueue, block);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Utility Methods
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
+ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain
|
|
{
|
|
if (xmppDomain == nil)
|
|
return nil;
|
|
else
|
|
return [NSString stringWithFormat:@"_xmpp-client._tcp.%@", xmppDomain];
|
|
}
|
|
|
|
@end
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@implementation XMPPSRVRecord
|
|
|
|
@synthesize priority;
|
|
@synthesize weight;
|
|
@synthesize port;
|
|
@synthesize target;
|
|
|
|
@synthesize sum;
|
|
@synthesize srvResultsIndex;
|
|
|
|
|
|
+ (XMPPSRVRecord *)recordWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
|
|
{
|
|
return [[XMPPSRVRecord alloc] initWithPriority:p1 weight:w port:p2 target:t];
|
|
}
|
|
|
|
- (id)initWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
|
|
{
|
|
if ((self = [super init]))
|
|
{
|
|
priority = p1;
|
|
weight = w;
|
|
port = p2;
|
|
target = [t copy];
|
|
|
|
sum = 0;
|
|
srvResultsIndex = 0;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@:%p target(%@) port(%hu) priority(%hu) weight(%hu)>",
|
|
NSStringFromClass([self class]), self, target, port, priority, weight];
|
|
}
|
|
|
|
- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord
|
|
{
|
|
UInt16 mPriority = self.priority;
|
|
UInt16 aPriority = aRecord.priority;
|
|
|
|
if (mPriority < aPriority)
|
|
return NSOrderedAscending;
|
|
|
|
if (mPriority > aPriority)
|
|
return NSOrderedDescending;
|
|
|
|
return NSOrderedSame;
|
|
}
|
|
|
|
@end
|