Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C: "format string is not a string literal (potentially insecure)" warning with macro

I'm using a macro to simplify returning localised strings, like so:

#define GetLocalStr(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]

Basically, if you have an entry in a localisation Strings file like "name" = "My name is %@";, calling

GetLocalStr( @"name", @"Foo" );

will return the NSString @"My name is Foo"

When I run it however, like:

NSString * str = GetLocalStr( @"name", @"Foo" );

I get the "format string is not a string literal" warning. Even following the advice of the other answers on SO about this warning and replacing it with:

NSString * str = [NSString stringWithFormat:@"%@", GetLocalStr( @"name", @"Foo" )];

I still get the warning, and besides, it kind of defeats the point of the macro making life easier.

How can I get rid of the warning short of wrapping all the GetLocalStr calls in #pragma suppressors?

Edit 27/08

After running through CRD's answer and doing some more tests, it seems like I made a bad assumption on the error. To clarify:

Localisation Strings file:

"TestNoArgs" = "Hello world";
"TestArgs" = "Hello world %@";

Code:

NSString * str1 = GetLocalStr( @"TestNoArgs" ); // gives warning
NSString * str2 = GetLocalStr( @"TestArgs", @"Foo" ); // doesn't give warning

The majority of my translations take no arguments, and those were the ones giving the warning, but I didn't make the connection until I read through CRD's answer.

I changed my single macro to two, like so:

#define GetLocalStrNoArgs(key) \
    [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil]

#define GetLocalStrArgs(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]

And if I call each one separately, there's no warnings.

I'd like GetLocalStr to expand to either GetLocalStrNoArgs or GetLocalStrArgs depending on if any arguments were passed or not, but so far I've been having no luck (macros are not my strong suit :D).

I'm using sizeof(#__VA_ARGS__) to determine if there's any arguments passed - it stringifys the arguments, and if the size is 1, it's empty (i.e. `\0'). Perhaps it's not the most ideal method, but it seems to work.

If I rewrite my GetLocalStr macro to:

#define GetLocalStr(key,...) (sizeof(#__VA_ARGS__) == 1) ? GetLocalStrNoArgs(key) : GetLocalStrArgs(key,##__VA_ARGS__)

I can use it, but I still get warnings everywhere it's used and there's no arguments passed, while something like

#define GetLocalStr( key,...)               \
    #if ( sizeof(#__VA_ARGS__) == 1 )       \
        GetLocalStrNoArgs(key)              \
    #else                                   \
        GetLocalStrArgs(key,##__VA_ARGS__)

won't compile. How can I get my GetLocalStr macro to expand properly?

like image 721
divillysausages Avatar asked Aug 26 '13 16:08

divillysausages


1 Answers

The Clang & GCC compilers check that format strings and the supplied arguments conform, they cannot do this if the format string is not a literal - hence the error message you see as you are obtaining the format string from the bundle.

To address this issue there is an attribute, format_arg(n) (docs), to mark functions which take a format string; alter it in some way without changing the actual format specifiers, e.g translate it; and then return it. Cocoa provides the convenient macro NS_FORMAT_ARG(n) for this attribute.

To fix your problem you need to do two things:

  1. Wrap up the call to NSBundle in a function with this attribute specified; and

  2. Change your "key" to include the format specifiers.

Second first, your strings file should contain:

"name %@" = "My name is %@"

so the key has the same format specifiers as the result (if you need to reorder the specifiers for a particular language you use positional format specifiers).

Now define a simple function to do the lookup, attributing it as a format translation function. Note we mark it as static inline, using the macro NS_INLINE as a hint to the compiler to both inline it into your macro expansion; the static allows you to include it in multiple files without symbol clashes:

NS_INLINE NSString *localize(NSString *string) NS_FORMAT_ARGUMENT(1);
NSString *localize(NSString *string)
{
   return [[NSBundle mainBundle] localizedStringForKey:string value:@"" table:nil];
}

And your macro becomes:

#define GetLocalStr(key, ...) [NSString stringWithFormat:localize(key), ##__VA_ARGS__]

Now when you:

GetLocalStr(@"name %@", @"Foo")

You will get both the localised format string and format checking.

Update

After Greg's comment I went back and checked - I had reproduced your error and so assumed it was down to a missing attribute. However as Greg points out localizedStringForKey:value:table: already has the attribute, so why the error? What I had absentmindedly done in reproducing your error was:

NSLog( GetLocalStr( @"name %@", @"Foo" ) );

and the compiler pointed at the macro definition and not that line - I should have spotted the compiler was misleading me.

So where does that leave you? Maybe you've done something similar? The key is that a format string must either be a literal or the result of a function/method attributed as a format translating function. And don't forget, you must also had the format specifier to your key as above.

Update 2

After your additional comments what you need to use is function, rather than a macro, along with the format attribute, for which Cocoa provides the convenient NS_FORMAT_FUNCTION(f,a) macro. This attribute informs the compiler that the function is a formatting one, the value of f is the number of the format string and a is the number of the first argument to the format. This gives the function declaration:

NSString *GetLocalStr(NSString *key, ...) NS_FORMAT_FUNCTION(1,2);

and the definition (assuming ARC):

NSString *GetLocalStr(NSString *key, ...)
{
   va_list args;
   va_start(args, key);
   NSString *format = [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil];
   NSString *result = [[NSString alloc] initWithFormat:format arguments:args];
   va_end (args);
   return result;
}

(which is essentially the same as @A-Live's).

Uses of this will be checked appropriately, for example:

int x;
...
NSString *s1 = GetLocalStr(@"name = %d", x); // OK
NSString *s2 = GetLocalStr(@"name = %d");    // compile warning - More '%" conversions than data arguments
NSString *s3 = GetLocalStr(@"name", x);      // compile warning - Data argument not used by format string
NSString *s4 = GetLocalStr(@"name");         // OK
like image 173
CRD Avatar answered Oct 29 '22 07:10

CRD