I need simple client-server communication with iPhone app, and XML property lists seem like quite easy and simple solution that saves me trouble of dealing with XML parser delegates and building these structures myself.
I just wonder is it wise to use NSPropertyListSerialization class with external data? Are there any obscure plist features that could be exploited?
Yes, it's safe to use NSPropertyListSerialization with untrusted data, but after you turn the bag of bytes into a hiearchy of plist types, you have to validate those types to makes sure they match your expected data format.
For example, if you expect a dictionary with string keys, and NSNumbers as values, you have to validate that with something like:
NSString *errorDescription = nil;
NSPropertyListFormat format = 0;
id topObject = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&errorDescription];
NSDictionary *validDictionary = nil;
if ([topObject isKindOfClass:[NSDictionary class]]) {
BOOL allNumbers = YES;
for(id value in [topObject allValues]) {
allNumbers = allNumbers && [value isKindOfClass:[NSNumber class]];
}
if (allNumbers) {
validDictionary = topObject;
}
}
return validDictionary;
If you don't do that, the source of the data could have placed plist values into the archive with mis matched types, or illegal values that could cause your client to misbehave and wind up being a security vulnerability.
#Jon Hess: thanks. I've created NSDictionary category to cut down on required number of isKindOfClasses.
#interface NSDictionary (TypedDictionary)
-(NSArray *)arrayForKey:(NSString*)key;
-(NSString *)stringForKey:(NSString*)key;
-(NSDictionary *)dictionaryForKey:(NSString*)key;
#end
#implementation NSDictionary (TypedDictionary)
-(NSArray *)arrayForKey:(NSString*)key {
NSArray *obj = [self objectForKey:key];
return [obj isKindOfClass:[NSArray class]] ? obj : nil;
}
-(NSString *)stringForKey:(NSString*)key {
NSString *obj = [self objectForKey:key];
return [obj isKindOfClass:[NSString class]] ? obj : nil;
}
-(NSDictionary *)dictionaryForKey:(NSString*)key {
NSDictionary *obj = [self objectForKey:key];
return [obj isKindOfClass:[NSDictionary class]] ? obj : nil;
}
#end
Related
I use setObject:forKey: to add an object of type Rresource to a NSMutableDictionary named: resourceLib.
Then I immediately look at what's actually in the dictionary and it's OK.
When I try to look at it again in another object's method, the proper key is present but a reference to a string property "url" cases a list of error messages including:
2016-09-28 11:32:42.636 testa[760:16697] -[__NSCFString url]: unrecognized selector sent to instance 0x600000456350
Rresource object is defined as:
#interface Rresource : NSObject
#property (nonatomic,strong) NSString* url;
#property (nonatomic,strong)NSMutableArray* resourceNotesArray;
#property(nonatomic,strong)NSString* name;
#property(nonatomic,strong)NSString* resourceUniqueID;
#property(nonatomic)BOOL isResourceDirty;
This method in a ViewController adds the Rresource to the NSMutableDictionary
-(void)saveResource
{
Rresource* resource = self.currentResource;
Rresource* temp;
if (resource)
{
if ( resource.isResourceDirty)
{
[self.model.resourceLib setObject:resource forKey:resource.resourceUniqueID];
temp = [self.model.resourceLib objectForKey:resource.resourceUniqueID];
}
}
}
Resource and temp contain identical info showing the info was added correctly.
In model's method the following causes the error message described above.
for (Rresource* resource in self.resourceLib)
{
NSString* string = resource.url;
}
where model contains:
#property(nonatomic,strong)NSMutableDictionary* resourceLib;
and :
#implementation Model
- (instancetype)init
{
self = [super init];
if (self)
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
self.path = [[paths objectAtIndex:0] stringByAppendingString:#"/Application Support/E2"];
BOOL exists = [[NSFileManager defaultManager] createDirectoryAtPath:self.path withIntermediateDirectories:NO attributes:nil error:nil];
if (!exists)
{
[[NSFileManager defaultManager] createDirectoryAtPath:self.path withIntermediateDirectories:NO attributes:nil error:nil];
}
self.resourceLibPath = [NSString pathWithComponents:#[self.path,#"resources"]];
self.resourceLib = [[NSMutableDictionary alloc]init];
self.noteLibPath = [NSString pathWithComponents:#[self.path, #"notes"]];
self.noteLib = [[NSMutableDictionary alloc]init];
}
return self;
I have found this question difficult to ask clearly even after spending several hours formulating it. I apologize.
I've tried pretty much everything for about a week. I'm stumped.
Any ideas?
Thanks
According to this entry on Enumeration, when you iterate over a dictionary using the fast enumeration syntax, you're iterating over its keys. In the above code sample you're assuming the enumeration happens over its values. What you're effectively doing is casting an NSString object as an Rresource, and sending it a selector only an actual Rresource object can respond to.
This should fix the loop:
for (NSString* key in self.resourceLib)
{
NSString* string = [self.resourceLib objectForKey:key].url;
}
I have an object that belongs to this class that contains the following declarations:
HEADER
#interface MyClassObject : NSObject <NSCopying, NSPasteboardWriting, NSPasteboardReading>
#property (nonatomic, strong) NSArray *children;
#property (nonatomic, assign) NSInteger type;
#property (nonatomic, strong) NSString *name;
#property (nonatomic, strong) id node;
children holds other objects of this class, node holds objects that can be NSView or NSImageView.
I would like to be able to copy and paste objects of this type to/from NSPasteboard.
I have google around but the explanations are vague.
What should I do to make this class copiable/readable to NSPasteboard or in other words, make it conform to NSPasteboardWriting and NSPasteboardReading protocols?
I don't have the slightest clue on how this is done and as usual, Apple documetation and nothing is the same thing.
This is the complete solution.
First thing is to make the class conforming to NSCoding too. In that case the class declaration will be
#interface MyClassObject : NSObject <NSCopying, NSPasteboardWriting, NSPasteboardReading, NSCoding>
To make it compatible with NSCoding you have to implement encodeWithCoder: and initWithCoder:... in my case:
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:#(self.type) forKey:#"type"];
[coder encodeObject:self.name forKey:#"name"];
// converting the node to NSData...
// the object contained in node must be compatible with NSCoding
NSData *nodeData = [NSKeyedArchiver archivedDataWithRootObject:self.node];
[coder encodeObject:nodeData forKey:#"node"];
NSData *childrenData = [NSKeyedArchiver archivedDataWithRootObject:self.children];
[coder encodeObject:childrenData forKey:#"children"];
}
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_type = [[coder decodeObjectForKey:#"type"] integerValue];
_name = [coder decodeObjectForKey:#"name"];
NSData *nodeData = [coder decodeObjectForKey:#"node"];
_efeito = [NSKeyedUnarchiver unarchiveObjectWithData:nodeData];
NSData *childrenData = [coder decodeObjectForKey:#"children"];
_children = [NSKeyedUnarchiver unarchiveObjectWithData:childrenData];
_parent = nil; // I want this to be nil when the object is recreated
}
To make the class work with NSPasteboard you have to add these 4 methods:
-(id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type {
return [NSKeyedUnarchiver unarchiveObjectWithData:propertyList];
}
+(NSArray *)readableTypesForPasteboard:(NSPasteboard *)pasteboard {
// I am using the bundleID as a type
return #[[[NSBundle mainBundle] bundleIdentifier]];
}
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard {
// I am using the bundleID as a type
return #[[[NSBundle mainBundle] bundleIdentifier]];
}
- (id)pasteboardPropertyListForType:(NSString *)type {
// I am using the bundleID as a type
if(![type isEqualToString:[[NSBundle mainBundle] bundleIdentifier]]) {
return nil;
}
return [NSKeyedArchiver archivedDataWithRootObject:self];
}
Then, on the class you implement the copy and paste you add:
- (void)copy:(id)sender {
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
NSArray *copiedObjects = #[theObjectYouWantToCopyToThePasteboard];
[pasteBoard writeObjects:copiedObjects];
}
- (void)paste:sender {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSArray *classArray = #[[LayerObject class]];
NSDictionary *options = [NSDictionary dictionary];
BOOL ok = [pasteboard canReadItemWithDataConformingToTypes:#[[[NSBundle mainBundle] bundleIdentifier]]];
if (ok) {
NSArray *objectsToPaste = [pasteboard readObjectsForClasses:classArray options:options];
MyObjectClass *object = [objectsToPaste objectAtIndex:0];
// object is the one you have copied to the pasteboard
// now you can do whatever you want with it.
// add the code here.
}
}
Ill only discuss writing. it is basically the same for reading (but in reverse of course ;))
write
declare the type you write
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard {
return #[#"com.mycompany.mytype"];
}
write the data
- (id)pasteboardPropertyListForType:(NSString *)type {
//check the type we are asked to write
if(![type isEqualToString:#"com.mycompany.mytype"]) return nil;
//create a _plist_ object
// :: ONLY PLIST TYPES
NSMutableDictionary *plist = [[NSMutableDictionary alloc] init];
...
return plist;
}
I'm using the function below to set new rtfd data to an attribute of type binary in a core data entity.
If its not equal, then I set the data to the managed object.
The problem here is that the equality only works when the rtfd data is only text. When there are pictures attached this method is returning false.
-(BOOL)setRTFData:(NSData*)data forKey:(NSString*)key
{
NSData *actualData = [self getDataValueForKey:key];
NSDictionary *dict = nil;
NSAttributedString *actualAttribString = [[NSAttributedString alloc] initWithRTFD:actualData documentAttributes:&dict];
NSAttributedString *newAttribString = [[NSAttributedString alloc] initWithRTFD:data documentAttributes:&dict];
BOOL isEqual = [actualAttribString isEqualToAttributedString:newAttribString];
[actualAttribString release];
[newAttribString release];
if (!isEqual)
{
[self willChangeValueForKey:key];
[self setPrimitiveValue:data forKey:key];
[self didChangeValueForKey:key];
return TRUE;
}
return FALSE;
}
this is how I get my *rtfd data*from a NSTextView:
NSData* data = [notesTextField RTFDFromRange:NSMakeRange(0,[[notesTextField textStorage] length])];
I'm using sdk 10.7
Help me please with the following problem:
- (NSDictionary *)getGamesList
{
NSMutableDictionary *gamesDictionary = [[NSMutableDictionary dictionary] retain];
// I was trying to change this on the commented code below, but did have no effect
// NSMutableDictionary *gamesDictionary = [[NSMutableDictionary alloc] init];
// [gamesDictionary retain];
while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *key = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
NSArray *gameDate = [key componentsSeparatedByString:#" "];
NSNumber *_id = [[NSNumber alloc] initWithInt:sqlite3_column_int(statement, 0)];
NSString *date_time = [NSString stringWithFormat:#"%#, %#",[gameDate objectAtIndex:0],[gameDate objectAtIndex:2]];
if (![gamesDictionary valueForKey:date_time]) [gamesDictionary setValue:[NSMutableArray array] forKey:date_time];
[[gamesDictionary valueForKey:date_time] addObject:[[_id copy] autorelease]];
[_id release];
}
sqlite3_reset(statement);
return gamesDictionary;
}
The leak starts in another method of another class, there the getGamesList method is called, like this:
NSMutableDictionary *gamesDictionary;
gamesDictionary = [[NSMutableDictionary dictionaryWithDictionary:[appDelegate getGamesList]] retain];
After that there are a lot of leaks that points to NSCFArray in the string:
NSArray *keys = [[NSArray arrayWithArray:[gamesDictionary allKeys]] retain];
in this method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSArray *keys = [[NSArray arrayWithArray:[gamesDictionary allKeys]] retain];
if ([keys count] != 0) return [[keys objectAtIndex:section] uppercaseString];
return #"";
}
I assume these things are connected to each other, but I still can not understand all of the memory management tips.
Thanks a lot!
Didn't use Cocoa for years (that's why I can't tell you an exact answer :/). But I guess your problem is that you systematically use retain on your objects.
Since the object reference count never get to 0, all dictionaries are keep in memory and not freed.
Try to remove the retain on [NSArray arrayWithArray] and [NSMutableDictionary dictionaryWithDictionary
http://en.wikibooks.org/wiki/Programming_Mac_OS_X_with_Cocoa_for_beginners/Some_Cocoa_essential_principles#Retain_and_Release
It does look like you are over-retaining your array.
When you create the gamesDictionary it is created with an retain count of +1. You then retain it (count becomes +2). When you get the value outside this function you retain again (count becomes +3).
You are correct that if you create an object you are responsible for it's memory management. Also, when you get an object from a method, you should retain it if you want to keep it around for longer than the span of the function. In your case, you just want to get at some of the properties of the object, so you don't need to retain it.
Here is a suggestion:
- (NSDictionary *)getGamesList
{
NSMutableDictionary *gamesDictionary = [NSMutableDictionary dictionary]; // Remove the retain.
while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *key = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
NSArray *gameDate = [key componentsSeparatedByString:#" "];
NSNumber *_id = [[NSNumber alloc] initWithInt:sqlite3_column_int(statement, 0)];
NSString *date_time = [NSString stringWithFormat:#"%#, %#",[gameDate objectAtIndex:0],[gameDate objectAtIndex:2]];
if (![gamesDictionary valueForKey:date_time]) [gamesDictionary setValue:[NSMutableArray array] forKey:date_time];
[[gamesDictionary valueForKey:date_time] addObject:[[_id copy] autorelease]];
[_id release];
}
sqlite3_reset(statement);
return gamesDictionary;
}
This next bit is messy. you create a new dictionary and retain it. The original dictionary is not autoreleased, so the count isn't decremented and it always hangs around. Just assign the dictionary rather than create a new one.
NSMutableDictionary *gamesDictionary = [[appDelegate getGamesList] retain];
// Retaining it, becuase it looks like it's used elsewhere.
Now, in this method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString *returnString;
// Don't need to retain the keys because you are only using it within the function
// and since you didn't alloc, copy or retain the array it contains, you aren't responsible for it's memory management.
NSArray *keys = [NSArray arrayWithArray:[gamesDictionary allKeys]];
if ([keys count] != 0) {
returnString = [[NSString alloc] initWithString:[[keys objectAtIndex:section] uppercaseString]];
return [returnString autorelease];
}
return #"";
}
I am currently trying to put together an URL where I specify some GET parameters. But I want to use japanese or other characters too in this URL.
Is there a way to convert a NSString to a string containing the HTML entities for the 'special' characters in my NSString?
I am currently using the following code, which seems to work, except for 'special characters' like chinese and japanese:
NSString* url = #"/translate_a/t?client=t&sl=auto&tl=";
url = [url stringByAppendingString:destinationLanguage];
url = [url stringByAppendingString:#"&text="];
url = [url stringByAppendingString:text];
NSURL* nsurl = [[NSURL alloc] initWithScheme:#"http" host:#"translate.google.com" path:url];
NSError* error;
NSString* returnValue = [[NSString alloc] initWithContentsOfURL:nsurl encoding:NSUTF8StringEncoding error:&error];
To properly URL encode your parameters, you need to convert each name and value to UTF-8, then URL encode each name and value separately, then join names with values using '=' and name-value pairs using '&'.
I generally find it easier to put all the parameters in an NSDictionary, then build the query string from the dictionary. Here's a category that I use for doing that:
// file NSDictionary+UrlEncoding.h
#import <Cocoa/Cocoa.h>
#interface NSDictionary (UrlEncoding)
-(NSString*) urlEncodedString;
#end
// file NSDictionary+UrlEncoding.m
#import "NSDictionary+UrlEncoding.h"
// private helper function to convert any object to its string representation
static NSString *toString(id object) {
return [NSString stringWithFormat: #"%#", object];
}
// private helper function to convert string to UTF-8 and URL encode it
static NSString *urlEncode(id object) {
NSString *string = toString(object);
return [string stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
}
#implementation NSDictionary (UrlEncoding)
-(NSString*) urlEncodedString {
NSMutableArray *parts = [NSMutableArray array];
for (id key in self) {
id value = [self objectForKey: key];
NSString *part = [NSString stringWithFormat: #"%#=%#",
urlEncode(key), urlEncode(value)];
[parts addObject: part];
}
return [parts componentsJoinedByString: #"&"];
}
#end
The method build an array of name-value pairs called parts by URL encoding each key and value, then joining them together with '='. Then the parts in the parts array are joined together with '&' characters.
So for your example:
#import "NSDictionary+UrlEncoding.h"
// ...
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
[parameters setValue: #"t" forKey: #"client"];
[parameters setValue: #"auto" forKey: #"sl"];
[parameters setValue: destinationLanguage forKey: #"tl"];
[parameters setValue: text forKey: #"text"];
NSString *urlString = [#"/translate_a/t?" stringByAppendingString: [parameters urlEncodedString]];
NSString has a method -stringByAddingPercentEscapesUsingEncoding:
Here's NSString extension you can find over internet
http://code.google.com/p/wimframework/source/browse/trunk/WimFramework/Classes/Helpers/WimAdditions.m
The decode part has some error in mapping array index to actual entity number. But since you only need encoding, it's fine to use it.
For simple URL encoding of strings, many of the solutions I have seen, while technically correct, look a lot less easy to use than I would like. So I came up with the following NSString category:
#interface NSString (MLExtensions)
- (NSString *)urlencode;
#end
NSString *_mlfilterChars = #";/?:#&=+$,";
#implementation NSString (MLExtensions)
- (NSString *)urlencode
{
return [[NSString stringWithString: (NSString *)
CFURLCreateStringByAddingPercentEscapes(
NULL,
(CFStringRef)self,
NULL,
(CFStringRef)_mlfilterChars,
kCFStringEncodingUTF8)]
stringByReplacingOccurrencesOfString: #"%20" withString: #"+"];
}
#end
I'm kind of in a hurry with some other stuff I'm working on, so I kind of cheated with the %20 => + conversion step, but it all seems to work great and I've been using it for a while now with a good number of URLs in my app.
Usage is blessfully easy:
- (NSString *)URLForSearch: (NSString *)searchFor
{
return [#"http://example.org/search?query="
stringByAppendingString: [searchFor urlencode]];
}