How to encode + into %2B with NSURLComponents - swift

I'm using NSURLComponents and I can't seem to get the query values to encode correctly. I need the final URL to represent a + as %2B.
let baseUrl = NSURL(string: "http://www.example.com")
let components = NSURLComponents(URL: baseUrl, resolvingAgainstBaseURL: true)
components.queryItems = [ NSURLQueryItem(name: "name", value: "abc+def") ]
XCTAssertEqual(components!.string!, "http://www.example.com?connectionToken=abc%2Bdef")
Failed!
Output equals:
http://www.example.com?connectionToken=abc+def
NOT
http://www.example.com?connectionToken=abc%2Bdef
I've tried several variations and I just can't seem to get it to output %2B at all.

My answer from Radar 24076063 with an explanation of why it works the way it does (with a little cleanup of the text):
The '+' character is legal in the query component so it does not need to be percent-encoded.
Some systems use the '+' as a space and require '+' the plus character to be percent-encoded. However, that kind of two stage encoding (converting plus sign to %2B and then converting space to plus sign) is prone to errors because it easily leads to encoding problems. It also breaks if the URL is normalized (syntax normalization of URLs includes the removal of all unnecessary percent-encoding — see rfc3986 section 6.2.2.2).
So, if you need that behavior because of the server your code is talking to, you'll handle the extra transformation(s) yourself. Here's a snippet of code that shows what you need to do both ways:
NSURLComponents *components = [[NSURLComponents alloc] init];
NSArray *items = [NSArray arrayWithObjects:[NSURLQueryItem queryItemWithName:#"name" value:#"Value +"], nil];
components.queryItems = items;
NSLog(#"URL queryItems: %#", [components queryItems]);
NSLog(#"URL string before: %#", [components string]);
// Replace all "+" in the percentEncodedQuery with "%2B" (a percent-encoded +) and then replace all "%20" (a percent-encoded space) with "+"
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:#"+" withString:#"%2B"] stringByReplacingOccurrencesOfString:#"%20" withString:#"+"];
NSLog(#"URL string after: %#", [components string]);
// This is the reverse if you receive a URL with a query in that form and want to parse it with queryItems
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:#"+" withString:#"%20"] stringByReplacingOccurrencesOfString:#"%2B" withString:#"+"];
NSLog(#"URL string back: %#", [components string]);
NSLog(#"URL queryItems: %#", [components queryItems]);
The output is:
URL queryItems: (
"<NSURLQueryItem 0x100502460> {name = name, value = Value +}"
)
URL string before: ?name=Value%20+
URL string after: ?name=Value+%2B
URL string back: ?name=Value%20+
URL queryItems: (
"<NSURLQueryItem 0x1002073e0> {name = name, value = Value +}"
)

As the other answers mention, "+" isn't encoded on iOS by default. But if your server requires that to be encoded, here's how to do it:
var comps = URLComponents(url: self, resolvingAgainstBaseURL: true)
// a local var is needed to fix a swift warning about "overlapping accesses" caused by writing to the same property that's being read.
var compsCopy = comps
compsCopy?.queryItems = [URLQueryItem(name: "name", value: "abc+def")]
comps?.percentEncodedQuery = compsCopy?.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")

+ may be a valid character when the content-type is application/x-www-form-urlencoded, see the link, so NSURLComponents doesn't encode it.
Apple also mention this:
RFC 3986 specifies which characters must be percent-encoded in the
query component of a URL, but not how those characters should be
interpreted. The use of delimited key-value pairs is a common
convention, but isn't standardized by a specification. Therefore, you
may encounter interoperability problems with other implementations
that follow this convention.
One notable example of potential interoperability problems is how the
plus sign (+) character is handled:
According to RFC 3986, the plus sign is a valid character within a
query, and doesn't need to be percent-encoded. However, according to
the W3C recommendations for URI addressing, the plus sign is reserved
as shorthand notation for a space within a query string (for example,
?greeting=hello+world).
If a URL query component contains a date formatted according to RFC
3339 with a plus sign in the timezone offset (for example,
2013-12-31T14:00:00+00:00), interpreting the plus sign as a space
results in an invalid time format. RFC 3339 specifies how dates should
be formatted, but doesn't advise whether the plus sign must be
percent-encoded in a URL. Depending on the implementation receiving
this URL, you may need to preemptively percent-encode the plus sign
character.
As an alternative, consider encoding complex and/or potentially
problematic data in a more robust data-interchange format, such as
JSON or XML.
The conclude is you may or may not encode '+'.
In my opinion, NSURLComponents only encode character which it make sure that should be encoded, such as '&', '=' or Chinese characters like '你' '好', it doesn't encode the character may be encode or not according to the content-type, like '+' I mentioned above. So if you find you have to encode '+' or your server can't parse correctly, you can use the code below.
I don't know swift, so I just provide objective-c code, sorry for that.
- (NSString *)URLEncodingValue:(NSString *)value
{
NSCharacterSet *set = [NSCharacterSet URLQueryAllowedCharacterSet];
NSMutableCharacterSet *mutableQueryAllowedCharacterSet = [set mutableCopy];
[mutableQueryAllowedCharacterSet removeCharactersInString:#"!*'();:#&=+$,/?%#[]"];
return [value stringByAddingPercentEncodingWithAllowedCharacters:mutableQueryAllowedCharacterSet];
}
!*'();:#&=+$,/?%#[] are reserved characters defined in RFC 3986, the code will encode all of them appear in the value parameter, if you just want to encode '+', just replace !*'();:#&=+$,/?%#[] with +.

This is an Apple bug. Instead use
NSString -stringByAddingPercentEncodingWithAllowedCharacters:
with
NSCharacterSet +URLQueryAllowedCharacterSet

Related

What is the relevance of this String format specifier?

I'm trying to get an understanding of some code I came across recently.
In an answer to a question here https://stackoverflow.com/a/51173170/1162328, the author made use of a String with a format specifier when looping over files in the documentDirectory. Can anyone shed some light on what %#/%# is actually doing?
for fileName in fileNames {
let tempPath = String(format: "%#/%#", path, fileName)
// Check for specific file which you don't want to delete. For me .sqlite files
if !tempPath.contains(".sql") {
try fileManager.removeItem(atPath: tempPath)
}
}
Reading the Apple documentation archive for Formatting Basics I came across this:
In format strings, a ‘%’ character announces a placeholder for a value, with the characters that follow determining the kind of value expected and how to format it. For example, a format string of "%d houses" expects an integer value to be substituted for the format expression '%d'. NSString supports the format characters defined for the ANSI C functionprintf(), plus ‘#’ for any object.
What exactly then, is %#/%# doing?
Each format specifier is replaced by one of the following arguments (usually in the same order, although that can be controlled with positional arguments). So in your case, the first %# is replaced by path and the second %# is replaced by fileName. Example:
let path = "/path/to/dir"
let fileName = "foo.txt"
let tempPath = String(format: "%#/%#", path, fileName)
print(tempPath) // /path/to/dir/foo.txt
The preferred way to build file names and paths is to use the corresponding URL methods instead of string manipulation. Example:
let pathURL = URL(fileURLWithPath: path)
let tempURL = pathURL.appendingPathComponent(fileName)
if tempURL.pathExtension != "sql" {
try FileManager.default.removeItem(at: tempURL)
}
%# is something similar to %d or anything like that. This is the way of string interpolation in Swift.
To be exact %# is placeholder for object - used in Objective-C A LOT. Since NSString * was object (now it is only String), it was used to insert NSString * into another NSString *.
Also given code is just rewritten objective-c code which was something like
NSString *tempPath = [NSString stringWithFormat:#"%#/%#", path, filename];
which can be rewritten in swift:
let tempPath = path + "/" + fileName
Also, given path = "Test" and fileName = "great" will give output Test/great.
One more note: %# is as good as dangerous. You can put UITableView as well as String in it. It will use description property for inserting into string.

Replace all Special Characters in String with valid URL characters

I cannot figure out how to replace all special characters in a string and convert it to a string I can use in a URL.
What I am using it for:
I am uploading an image, converting it to base64, and then passing it to the Laravel framework, however the base64 string can contain +, /, \, etc. which changes the meaning of the URL.
I can replace the + sign with the following code:
let withoutPlus = image.stringByReplacingCharactersInRange("+", withString: "%2B")
however then I cannot use that as a NSString to try and change the other characters.
Surely there is a way to just target every single special character and convert it something usable in a URL?
You can use stringByAddingPercentEncodingWithAllowedCharacters to escape characters as needed. You pass it an NSCharacterSet containing the characters that are valid for that string (i.e. the ones you don't want replaced). There's a built-in NSCharacterSet for characters allowed in URL query strings that will get you most of the way there, but it includes + and / so you'll need to remove those from the set. You can do that by making a mutable copy of the set and then calling removeCharactersInString:
let allowedCharacters = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as NSMutableCharacterSet
allowedCharacters.removeCharactersInString("+/=")
Then you can call stringByAddingPercentEncodingWithAllowedCharacters on your string, passing in allowedCharacters:
let encodedImage = image.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters)
Note that it will return an optional String (String?) so you'll probably want to use optional binding:
if let encodedImage = image.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters) {
/* use encodedImage here */
} else {
/* stringByAddingPercentEncodingWithAllowedCharacters failed for some reason */
}
Example:
let unencodedString = "abcdef/+\\/ghi"
let allowedCharacters = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as NSMutableCharacterSet
allowedCharacters.removeCharactersInString("+/=")
if let encodedString = unencodedString.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters) {
println(encodedString)
}
Prints:
abcdef%2F%2B%5C%2Fghi
Use
let withoutPlus = image.stringByReplacingOccurrencesOfString("+", withString: "%2B")
rather than image.stringByReplacingCharactersInRange. Note that your call as posted doesn't work, as that method is declared as
func stringByReplacingCharactersInRange(range: Range<String.Index>, withString replacement: String) -> String
and you are not supplying the correct parameters.
You might do better to use POST to send a file, rather than encode it into your URL

regular expression to match " but not \"

How can I construct a regular expression which matches an literal " but only if it is not preceded by the escape slash namely \
I have a NSMutableString str which prints the following on NSLog. The String is received from a server online.
"Hi, check out \"this book \". Its cool"
I want to change it such that it prints the following on NSLog.
Hi, check out "this book ". Its cool
I was originally using replaceOccurencesOfString ""\" with "". But then it will do the following:
Hi, check out \this book \. Its cool
So, I concluded I need the above regular expression to match only " but not \" and then replace only those double quotes.
thanks
mbh
[^\\]\"
[^m] means does not match m
Not sure how this might translate to whatever is supported in the iOS apis, but, if they support anchoring (which I think all regex engines should), you're describing something like
(^|[^\])"
That is, match :
either the beginning of the string ^ or any character that's not
\ followed by:
the " character
If you want to do any sort of replacement, you'll have to grab the first (and only) group in the regex (that is the parenthetically grouped part of the expression) and use it in the replacement. Often this value labeled as $1 or \1 or something like that in your replacement string.
If the regex engine is PCRE based, of course you could put the grouped expression in a lookbehind so you wouldn't need to capture and save the capture in the replacement.
Not sure about regex, a simpler solution is,
NSString *str = #"\"Hi, check out \\\"this book \\\". Its cool\"";
NSLog(#"string before modification = %#", str);
str = [str stringByReplacingOccurrencesOfString:#"\\\"" withString:#"#$%$#"];
str = [str stringByReplacingOccurrencesOfString:#"\"" withString:#""];
str = [str stringByReplacingOccurrencesOfString:#"#$%$#" withString:#"\\\""];//assuming that the chances of having '#$%$#' in your string is zero, or else use more complicated word
NSLog(#"string after modification = %#", str);
Output:
string before modification = "Hi, check out \"this book \". Its cool"
string after modification = Hi, check out \"this book \". Its cool
Regex: [^\"].*[^\"]. which gives, Hi, check out \"this book \". Its cool
It looks like it's a JSON string? Perhaps created using json_encode() in PHP on the server? You should use the proper JSON parser in iOS. Don't use regex as you will run into bugs.
// fetch the data, eg this might return "Hi, check out \"this book \". Its cool"
NSData *data = [NSData dataWithContentsOfURL:#"http://example.com/foobar/"];
// decode the JSON string
NSError *error;
NSString *responseString = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
// check if it worked or not
if (!responseString || ![responseString isKindOfClass:[NSString class]]) {
NSLog(#"failed to decode server response. error: %#", error);
return;
}
// print it out
NSLog(#"decoded response: %#", responseString);
The output will be:
Hi, check out "this book ". Its cool
Note: the JSON decoding API accepts an NSData object, not an NSString object. I'm assuming you also have a data object and are converting it to a string at some point... but if you're not, you can convert NSString to NSData using:
NSString *responseString = [NSJSONSerialization JSONObjectWithData:[myString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:&error];
More details about JSON can be found at:
http://www.json.org
http://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html

NSScanner scanUpToCharactersFromSet gives BAD ACCESS

NSString*test=[NSString stringWithString:inputString];
NSScanner*scan;
BOOL pass= [scan scanUpToCharactersFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet] intoString:&test];
The last line crashes the app with bad access. Does it have something to do with the address symbol, &? I'm not clear on why it needs this type of syntax anyway.
I am trying to simply check with my BOOL if the inputString contains any non-alphanumeric characters. If it does, pass becomes YES.
UPDATE: I see I do not understand scanner entirely. I understand this output:
NSString*test=#"Hello"; // this should cause my BOOL to be NO since no characters are scanned
NSScanner*scanix=[NSScanner scannerWithString:test];
BOOL pass= [scanix scanUpToCharactersFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet] intoString:nil];
NSLog(#"pass is %i for %#",pass, test);
Result in log: pass is 1 for Hello
What I want is to know if in fact the test string contains non-alphanumeric characters. How would I factor such a test into this?
You are supposed to initialise your scanner with the string to parse,
NSScanner *scan =[NSScanner scannerWithString:mystring];
What I want is to know if in fact the test string contains
non-alphanumeric characters. How would I factor such a test into this?
You don't need to use NSScanner if this is your only goal. You can simply use NSString's -rangeOfCharacterFromSet: method:
NSCharacterSet *nonAlphanumeric = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
pass = ([test rangeOfCharacterFromSet:nonAlphanumeric].location != NSNotFound);
You are mixing up the destination string and the source string. Your source is test. Also, your scanner is not initialized.
NSScanner *scan = [NSScanner scannerWithString:test];
BOOL didScanCharacters = [scan scanUpToCharactersFromSet:
[[NSCharacterSet alphanumericCharacterSet] invertedSet]
intoString:nil];
The EXC_BAD_ACCESS error occurs because you are sending a message to a nonexistent object.

Get NSString From NSURL

I try to get NSString from NSURL with this method:
NSString *tmp2 = [item.path absoluteString];
Unfortunately I get instead of the NSURL:
<CFURL 0x173c50 [0x3f1359f8]>{type = 0, string = /var/mobile/Applications/A30FD2E4-A273-4522-AFD5-A981EFD3C2AA/Documents/*** *** - *** ***.***, encoding = 134217984, base = (null)}
I get :
file://localhost/var/mobile/Applications/A30FD2E4-A273-4522-AFD5-A981EFD3C2AA/Documents/***%20***%20-%20***%20***.***
any idea why?
The NSURL documentation clearly states that absoluteString returns an NSString, just like your code above. This is the string representation of the absolute path, so what you are getting is what you should be getting.
However, looking at the documentation you could also use path, relativePath or relativeString to get a string representation of the url in other formats (absolute or relative paths that either do or do not conform to RFC 1808 (a now obsolete percent encoding).