-
Notifications
You must be signed in to change notification settings - Fork 308
/
Copy pathOFNetStateRegistration.m
580 lines (469 loc) · 24.6 KB
/
OFNetStateRegistration.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
// Copyright 2008-2019 Omni Development, Inc. All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// <http://www.omnigroup.com/developer/sourcecode/sourcelicense/>.
#import <OmniFoundation/OFNetStateRegistration.h>
#import <OmniBase/NSError-OBUtilities.h>
#import <OmniFoundation/NSData-OFEncoding.h>
#import <OmniFoundation/NSData-OFSignature.h>
#import <OmniFoundation/NSSet-OFExtensions.h>
#import <OmniFoundation/NSString-OFReplacement.h>
#import <OmniFoundation/OFErrors.h>
#import <OmniFoundation/OFPreference.h>
#import <OmniFoundation/OFXMLIdentifier.h>
#import <Foundation/Foundation.h>
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
#import <UIKit/UIApplication.h>
#import <UIKit/UIDevice.h>
#import <sys/socket.h>
#import <netinet/in.h>
#else
#import <OmniFoundation/NSProcessInfo-OFExtensions.h>
#import <OmniFoundation/OFController.h>
#endif
#import <dns_sd.h>
#include <netdb.h>
#if !OF_ENABLE_NET_STATE
#error Should not be in the target
#endif
OB_REQUIRE_ARC
static OFDeclareDebugLogLevel(OFNetStateRegistrationDebug);
#define DEBUG_REGISTRATION(level, format, ...) do { \
if (OFNetStateRegistrationDebug >= (level)) \
NSLog(@"STATE REGISTRATION %@: " format, [self shortDescription], ## __VA_ARGS__); \
} while (0)
// <http://www.dns-sd.org/ServiceTypes.html> Short name of protocol, fourteen characters maximum, conforming to normal DNS host name rules: Only lower-case letters, digits, and hyphens; must begin and end with lower-case letter or digit.
#if 0 && defined(DEBUG)
NSString * const OFNetStateServiceType = @"_omnidebug._tcp."; // Different service type to make it easier to work on local builds w/o seeing chatter from a zillion other devices on the network.
#else
NSString * const OFNetStateServiceType = @"_omnistate._tcp."; // OBFinishPorting <bug:///147854> (Frameworks-iOS Engineering: Register _omnistate._tcp.)
#endif
#ifdef USE_WIDE
NSString * const OFNetStateServiceDomain = @""; // can go across Back to my Mac
#else
NSString * const OFNetStateServiceDomain = @"local.";
#endif
RCS_ID("$Id$")
/*
THREADING NOTES:
Calls to this class can be made from any thread/queue (just one calling thread/serial queue, not multiple), but the NSNetService should be used only on the main queue (since we have to schedule it in a runloop and we don't want to do so in a transient runloop for a worker thread/queue).
We use the _service ivar on the calling thread, store it in a strong local and then invoke blocks on the main queue.
The NSNetService is potentially initialized on a non-main queue, and depending on the vagaries of reference counting, it might be deallocated on a background queue. But, all the runloop registration/deregistration happens on the main queue.
Update coalescing:
If you update your TXT record too quickly, mDNSResponder will bleat wildly to the console log:
3/21/13 3:01:38.738 PM mDNSResponder[36]: Excessive update rate for jWOlAmmA4a1\032fyHotaIF7vq._omnistate._tcp.local.; delaying announcement by 2 seconds
So, we should coalesce updates ourselves. This could be tricky if we want to ensure that the "last" update has a chance to be seen. If -invalidate is called while we have a pending update, it could get dropped. Maybe we don't care.
*/
// When mDNSResponder starts complaining, it starts by delaying our announcement by one second. But, if we keep up a constant stream of updates, it will still complain at longer and longer coalescing intervals. Experimentally, we can do three second updates for a sustainted period and not get mDNSResponder complaints (obviously this could change in the future).
static const NSTimeInterval kCoalesceTimeInterval = 3;
@implementation OFNetStateRegistration
{
int _socket;
in_port_t _port;
NSNetService *_service;
NSString *_version;
// Main thread state for coalescing updates
NSTimeInterval _lastUpdateTimeInterval;
NSTimer *_delayedUpdateTimer;
NSData *_delayedUpdateTXTData;
}
+ (void)initialize;
{
OBINITIALIZE;
#ifdef OMNI_ASSERTIONS_ON
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
// No NSProcessInfo or readable entitlements on iOS, of course...
#else
if ([[NSProcessInfo processInfo] isSandboxed]) {
NSDictionary *entitlements = [[NSProcessInfo processInfo] effectiveCodeSigningEntitlements:NULL];
// Assert that we have network server entitlement, or this ain't gonna work
OBASSERT([[entitlements objectForKey:@"com.apple.security.network.server"] boolValue]);
}
#endif
#endif
}
static NSString * const OFNetStateRegistrationGroupTerminator = @" ";
- initWithGroupIdentifier:(NSString *)groupIdentifier memberIdentifier:(NSString *)memberIdentifier name:(NSString *)name state:(NSData *)state;
{
OBPRECONDITION(![NSString isEmptyString:groupIdentifier]);
OBPRECONDITION([groupIdentifier rangeOfString:OFNetStateRegistrationGroupTerminator].location == NSNotFound);
OBPRECONDITION([groupIdentifier length] < kDNSServiceMaxServiceName); // We put this in the NSNetService name to avoid resolving TXT records on things we don't care about.
OBPRECONDITION(![NSString isEmptyString:memberIdentifier]);
OBPRECONDITION(OFNOTEQUAL(memberIdentifier, groupIdentifier));
if (!(self = [super init]))
return nil;
_groupIdentifier = [groupIdentifier copy];
_memberIdentifier = [memberIdentifier copy];
_name = [name copy];
_localState = [state copy];
_version = OFXMLCreateID();
_registrationIdentifier = OFXMLCreateID();
_socket = -1;
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
BOOL inForeground = ([OFSharedApplication() applicationState] != UIApplicationStateBackground);
#else
BOOL inForeground = YES;
#endif
if (inForeground)
[self _publishService];
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
#endif
return self;
}
- (void)dealloc;
{
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
if (_service)
[self invalidate];
}
- (void)invalidate;
{
OBPRECONDITION(_service);
DEBUG_REGISTRATION(2, @"invalidating");
NSNetService *service = _service;
_service = nil;
NSTimer *timer = _delayedUpdateTimer;
_delayedUpdateTimer = nil;
// We could close() this here, but lets not do it until the NSNetService is stopped.
int serviceSocket = _socket;
_socket = -1;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[timer invalidate];
[service stop];
[service removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
if (serviceSocket > 0) {
if (close(serviceSocket) < 0)
NSLog(@"%s: close -> %d, %s", __PRETTY_FUNCTION__, OMNI_ERRNO(), strerror(OMNI_ERRNO()));
}
}];
}
NSString * const OFNetStateRegistrationGroupIdentifierKey = @"g";
NSString * const OFNetStateRegistrationMemberIdentifierKey = @"m";
NSString * const OFNetStateRegistrationStateKey = @"s";
NSString * const OFNetStateRegistrationVersionKey = @"v";
- (void)setLocalState:(NSData *)state;
{
// Might get backgrounded and have some background task that finishes up. If we ever get foregrounded again, we'll remember the state to publish.
//OBPRECONDITION(_service); // not invalidated
if (OFISEQUAL(_localState, state))
return;
DEBUG_REGISTRATION(1, @"Setting state");
DEBUG_REGISTRATION(2, @" ... new value is %@", state);
_localState = [state copy];
_version = OFXMLCreateID();
if (_localState && _service)
[self _queueTXTRecordUpdate];
}
#pragma mark - Debugging
- (NSString *)shortDescription;
{
return [NSString stringWithFormat:@"<%@:%p %@ %@ %@>", NSStringFromClass([self class]), self, _name, _memberIdentifier, _registrationIdentifier];
}
#pragma mark - Private
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
- (void)_applicationDidEnterBackground:(NSNotification *)notification;
{
OBPRECONDITION([NSThread isMainThread]);
// Radar 14075101: UIApplicationDidEnterBackgroundNotification sent twice if app with background activity is killed from Springboard
if (_service)
[self invalidate];
}
- (void)_applicationWillEnterForeground:(NSNotification *)notification;
{
OBPRECONDITION([NSThread isMainThread]);
OBPRECONDITION(_service == nil);
if (!_service)
[self _publishService];
}
#endif
- (void)_queueTXTRecordUpdate;
{
OBPRECONDITION(_service);
NSMutableDictionary *txtRecord = [NSMutableDictionary new];
txtRecord[OFNetStateRegistrationMemberIdentifierKey] = _memberIdentifier;
if (_groupIdentifier)
txtRecord[OFNetStateRegistrationGroupIdentifierKey] = _groupIdentifier;
if (_localState) {
DEBUG_REGISTRATION(2, @"_localState %@", _localState);
NSData *state = _localState;
if ([state length] > 20) { // SHA-1 digest length
state = [state sha1Signature];
DEBUG_REGISTRATION(1, @"state, SHA1 %@", state);
}
NSString *stateString = [state ascii85String];
DEBUG_REGISTRATION(1, @"state, ascii85 %@", stateString);
txtRecord[OFNetStateRegistrationStateKey] = stateString; // We need short strings for Bonjour, so no hex
/*
Say there are members A and B, both in state S1. If A goes to S2, B hears about this and quickly flaps from S1->S2->S1, then A may not see that B has changed state at all when it may need to. For example, if A creates a document and B deletes it (assuming the state is based on info about the document). A needs to be able to tell that B did this rather than thinking B didn't acknowledge the state.
Right now we don't publish the actual state value to the delegate in OFNetStateNotifier, so it is almost not worth including the state itself in the TXT record. We could probably get away with just sending a version token.
*/
OBASSERT(![NSString isEmptyString:_version]);
txtRecord[OFNetStateRegistrationVersionKey] = _version;
}
NSData *txtData = OFNetStateTXTRecordDataFromDictionary(txtRecord, YES);
DEBUG_REGISTRATION(1, @"txtRecord now %@", txtRecord);
NSNetService *service = _service;
__weak OFNetStateRegistration *weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[weakSelf _setUpDelayedTXTUpdate:txtData service:service];
}];
}
- (void)_setUpDelayedTXTUpdate:(NSData *)txtData service:(NSNetService *)service;
{
NSTimeInterval nextAllowedUpdateInterval = _lastUpdateTimeInterval + kCoalesceTimeInterval;
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval delayInterval = (nextAllowedUpdateInterval - now);
if (delayInterval > 0) {
_delayedUpdateTXTData = [txtData copy];
if (!_delayedUpdateTimer) {
// Pass the NSNetService we had at the time along rather than reading _service again when this fires.
DEBUG_REGISTRATION(1, @"Delaying TXT record update for %g seconds", delayInterval);
_delayedUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:delayInterval target:self selector:@selector(_performDelayedTXTDataUpdate:) userInfo:service repeats:NO];
}
} else
_updateTXTRecord(self, service, txtData);
}
- (void)_performDelayedTXTDataUpdate:(NSTimer *)timer;
{
OBPRECONDITION([NSThread isMainThread]);
DEBUG_REGISTRATION(1, @"Performing delayed TXT record update");
if (_delayedUpdateTimer == nil) {
// We don't cancel the timer immediately in -invalidate since we have to dispatch to the main queue. This might give it time to fire.
OBASSERT(_service == nil);
return;
}
NSNetService *service = timer.userInfo;
NSData *txtData = _delayedUpdateTXTData;
_delayedUpdateTXTData = nil;
[_delayedUpdateTimer invalidate];
_delayedUpdateTimer = nil;
_updateTXTRecord(self, service, txtData);
}
static void _updateTXTRecord(OFNetStateRegistration *self, NSNetService *service, NSData *txtData)
{
if (![service setTXTRecordData:txtData] && ![txtData isEqual:[service TXTRecordData]]) {
NSLog(@"%@: unable to set TXT record of %@ to %@", self, service, [txtData unadornedLowercaseHexString]);
OBASSERT_NOT_REACHED("What happened that prevented setting the TXT record?");
} else {
DEBUG_REGISTRATION(2, "Set TXT data on service %@ to %@", service, txtData);
}
self->_lastUpdateTimeInterval = [NSDate timeIntervalSinceReferenceDate];
}
+ (BOOL)netServiceName:(NSString *)serviceName matchesAnyGroup:(NSSet *)groupIdentifiers;
{
return [groupIdentifiers any:^BOOL(NSString *groupIdentifier) {
if (![serviceName hasPrefix:groupIdentifier])
return NO;
// Allow clients that don't append a registration identifier... bad form, but might as well.
if ([serviceName rangeOfString:OFNetStateRegistrationGroupTerminator].location == [groupIdentifier length])
return YES;
return NO;
}] != nil;
}
- (void)_publishService;
{
OBPRECONDITION(!_service);
OBPRECONDITION(_socket < 0);
__autoreleasing NSError *error;
_socket = [[self class] _createSocketBoundToLocalPort:&_port requestedPort:0 error:&error];
if (_socket < 0) {
// One possible failure is NSPOSIXErrorDomain+EPERM for sandboxed applications
NSLog(@"Error creating socket for network state registration: %@", [error toPropertyList]);
}
// We need the name to be unique, at least within our app, if we want to publish multiple registrations. Without this, if an app publishes several services under different ports and the same name, only one service will appear in dns-sd.
// If the name is too *long*, then nothing will be published (not even a truncated version).
// We also encode the group name in our service name so that OFNetStateNotifier can avoid resolving TXT records for un-interesting registrations.
// NOTE: we don't currently use this since OFNetStateNotifiers can change the groups the monitor, but maybe we should.
NSString *serviceName = [NSString stringWithFormat:@"%@%@%@", _groupIdentifier, OFNetStateRegistrationGroupTerminator, _registrationIdentifier];
// We need all of this to be present so that our name is unique
OBASSERT([serviceName length] < kDNSServiceMaxServiceName);
// Then add in as much debug info as we can.
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
NSBundle *controllingBundle = [NSBundle mainBundle];
#else
NSBundle *controllingBundle = [OFController controllingBundle];
#endif
NSString *appName = [[controllingBundle infoDictionary] objectForKey:@"CFBundleDisplayName"];
if (![NSString isEmptyString:appName])
serviceName = [serviceName stringByAppendingFormat:@" %@", appName];
if (![NSString isEmptyString:_name])
serviceName = [serviceName stringByAppendingFormat:@" %@", _name];
// Truncate the service name to make sure it gets registered instead of silently dropped on the floor(!)
if ([serviceName length] >= kDNSServiceMaxServiceName)
serviceName = [serviceName substringToIndex:kDNSServiceMaxServiceName-1];
// *Hopefully* this is OK; we're creating the service on whatever thread the instance's owner uses, but not registering it here. This is needed since we want to serialize the 'has a service' checks on the calling thread.
_service = [[NSNetService alloc] initWithDomain:OFNetStateServiceDomain type:OFNetStateServiceType name:serviceName port:_port];
OBASSERT(_service);
NSNetService *service = _service;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[service scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[service publish];
}];
[self _queueTXTRecordUpdate]; // Update our TXT data; doing this before -publish fails.
}
+ (int)_createSocketBoundToLocalPort:(in_port_t *)outPort requestedPort:(in_port_t)requestedPort error:(NSError **)outError;
{
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
OBErrorWithErrno(outError, OMNI_ERRNO(), "socket", @"", @"Unable to create socket descriptor");
OFError(outError, OFNetStateRegistrationCannotCreateSocket, NSLocalizedStringFromTableInBundle(@"Cannot create socket.", @"OmniFoundation", OMNI_BUNDLE, @"error description"), @"Unable to create socket descriptor.");
return -1;
}
struct sockaddr_in ipv4;
bzero(&ipv4, sizeof(ipv4));
socklen_t socketAddressLength = sizeof(ipv4);
ipv4.sin_len = sizeof(ipv4);
ipv4.sin_family = AF_INET;
ipv4.sin_addr.s_addr = htonl(INADDR_ANY);
ipv4.sin_port = htons(requestedPort); // 0 means for the system to pick an available port
if (bind(fd, (struct sockaddr *)&ipv4, socketAddressLength) < 0) {
close(fd);
OBErrorWithErrno(outError, OMNI_ERRNO(), "bind", @"", @"Unable to bind socket.");
OFError(outError, OFNetStateRegistrationCannotCreateSocket, NSLocalizedStringFromTableInBundle(@"Cannot create socket.", @"OmniFoundation", OMNI_BUNDLE, @"error description"), @"Unable to bind socket.");
return -1;
}
int shouldReuse = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &shouldReuse, sizeof(shouldReuse)) < 0) {
close(fd);
OBErrorWithErrno(outError, OMNI_ERRNO(), "setsockopt", @"SO_REUSEADDR", @"Unable to set socket option.");
OFError(outError, OFNetStateRegistrationCannotCreateSocket, NSLocalizedStringFromTableInBundle(@"Cannot create socket.", @"OmniFoundation", OMNI_BUNDLE, @"error description"), @"Unable to set socket option.");
return NO;
}
shouldReuse = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &shouldReuse, sizeof(shouldReuse)) < 0) {
close(fd);
OBErrorWithErrno(outError, OMNI_ERRNO(), "setsockopt", @"SO_REUSEPORT", @"Unable to set socket option.");
OFError(outError, OFNetStateRegistrationCannotCreateSocket, NSLocalizedStringFromTableInBundle(@"Cannot create socket.", @"OmniFoundation", OMNI_BUNDLE, @"error description"), @"Unable to set socket option.");
return NO;
}
memset(&ipv4, 0, sizeof(ipv4));
if (getsockname(fd, (struct sockaddr *)&ipv4, &socketAddressLength) < 0) {
close(fd);
OBErrorWithErrno(outError, OMNI_ERRNO(), "getsockname", @"", @"Unable to get socket address.");
OFError(outError, OFNetStateRegistrationCannotCreateSocket, NSLocalizedStringFromTableInBundle(@"Cannot create socket.", @"OmniFoundation", OMNI_BUNDLE, @"error description"), @"Unable to get socket address.");
return NO;
}
*outPort = ntohs(ipv4.sin_port);
return fd;
}
@end
NSData *OFNetStateTXTRecordDataFromDictionary(NSDictionary *dictionary, BOOL addTypePrefixes)
{
// The TXT records have some undocumented restrictions and some issues that are only documented at the CF level.
// The total length of the UTF-8 representation for each key and its data must be < 255.
// The values can be datas or strings, but if strings, they'll be flattened to data and when unarchiving from the TXT data, the resulting dictionary will have datas for values, no matter the input type.
// So, we need to record each pref on its own and not record anything of excessive length.
NSMutableDictionary *valueByKey = [NSMutableDictionary dictionary];
NSArray *keys = [dictionary allKeys];
NSUInteger keyIndex = [keys count];
while (keyIndex--) {
NSString *key = [keys objectAtIndex:keyIndex];
id value = [dictionary objectForKey:key];
NSString *string;
if ([value isKindOfClass:[NSString class]]) {
if (addTypePrefixes)
string = [NSString stringWithFormat:@"s:%@", value];
else
string = value;
} else if ([value isKindOfClass:[NSNumber class]]) {
CFNumberType type = CFNumberGetType((CFNumberRef)value);
switch (type) {
case kCFNumberSInt32Type:
if (addTypePrefixes)
string = [NSString stringWithFormat:@"i:%d", [value intValue]];
else
string = [NSString stringWithFormat:@"%d", [value intValue]];
break;
case kCFNumberCharType:
if (addTypePrefixes)
string = [NSString stringWithFormat:@"c:%d", [value intValue]];
else
string = [NSString stringWithFormat:@"%d", [value intValue]];
break;
default:
NSLog(@"Unable to archive key '%@' in TXT record with number of type %ld", key, type);
OBASSERT_NOT_REACHED("Add archiving for this type");
continue;
}
} else {
NSLog(@"Unable to archive key '%@' in TXT record with value of class %@", key, [value class]);
OBASSERT_NOT_REACHED("Add archiving for this type");
continue;
}
// We could check the key+string length vs 255 and drop individual settings if they would break the whole batch. But it seems like that might be confusing. Better to have the client complain that it couldn't get the settings.
[valueByKey setObject:string forKey:key];
}
return [NSNetService dataFromTXTRecordDictionary:valueByKey];
}
NSDictionary *OFNetStateTXTRecordDictionaryFromData(NSData *txtRecord, BOOL expectTypePrefixes, __autoreleasing NSString **outErrorString)
{
OBPRECONDITION(outErrorString != NULL);
if ([txtRecord length] == 0) {
*outErrorString = @"No TXT record data returned.";
return nil;
}
NSDictionary *dict = [NSNetService dictionaryFromTXTRecordData:txtRecord];
if (!dict) {
*outErrorString = @"Unable to interpret TXT record data.";
return nil;
}
NSMutableDictionary *validatedValues = [NSMutableDictionary dictionary];
NSArray *keys = [dict allKeys];
NSUInteger keyIndex = [keys count];
while (keyIndex--) {
NSString *key = [keys objectAtIndex:keyIndex];
id value = [dict objectForKey:key];
// If the TXT record is just "foozle", it will be mapped to "foozle"=<null/>
if (OFNOTNULL(value)) {
NSString *string;
if ([value isKindOfClass:[NSString class]])
string = value;
else if ([value isKindOfClass:[NSData class]])
string = [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding];
//NSLog(@"%@: %@", key, string);
if (!string) {
*outErrorString = [NSString stringWithFormat: @"Unable to interpret data for TXT record entry \"%@\".", key];
return nil;
}
if (expectTypePrefixes) {
NSUInteger length = [string length];
if (length < 2) {
*outErrorString = [NSString stringWithFormat:@"TXT record entry for \"%@\" is too short.", key];
return nil;
}
NSString *payload = [string substringFromIndex:2];
unichar type = [string characterAtIndex:0];
switch (type) {
case 's': {
value = payload;
break;
}
case 'i': {
value = [NSNumber numberWithInt:[payload intValue]];
break;
}
case 'c': {
// Probably a bool...
value = [NSNumber numberWithChar:(char)[payload intValue]];
break;
}
default:
OBASSERT_NOT_REACHED("Implement this type");
*outErrorString = [NSString stringWithFormat:@"Unable to handle TXT record entry with type '%c' for key \"%@\".", type, key];
return nil;
}
} else {
value = string;
}
}
if (value)
[validatedValues setObject:value forKey:key];
}
return validatedValues;
}