Handling varargs before passing them to

I would like to create a function that has the same signature as NSString

stringWithFormat:

(with varargs), but I would like the url to encode each argument before passing it to stringWithFormat:

. I already have a method + (NSString *)urlEncode:(NSString *)s

that does the encoding. How do I create a va_list with encoded arguments?

+3


source to share


2 answers


There is no portable way to create va_lists in C, C ++ or Objective-C. Perhaps you could write or find some functions that use inline assembly to directly modify the stack and make variation calls, but this is really not a good way. You have three practical options that I can think of.

Here's the first option. Use only mutable strings and pass arguments using NSString initWithFormat: arguments: method.

- (NSString*)forwardMessage:(NSString*)format, ... {
    va_list args;
    va_start(args, format);

    BOOL escape = NO;
    char* ptr = (char*)[format UTF8String];
    while (*ptr) {
        if (*ptr == '%') {
            escape = !escape;
        } else if (escape) {
            // argument
            id obj = va_arg(args, id);
            if (*ptr == '@') {
                // object
                if ([obj isKindOfClass:[NSString class]]) {
                    // string
                    id copy = [obj copy];
                    if (copy != obj) {
                        // mutable
                        [obj replaceCharactersInRange:NSMakeRange(0, [obj length]) withString:@"replaced!"];
                    }
                }
            }
            escape = NO;
        }
        ++ptr;
    }

    va_end(args);

    va_list args2;
    va_start(args2, format);

    NSString* ret = [[NSString alloc] initWithFormat:format arguments:args2];

    va_end(args2);
    return ret;
}

      

This method will accept variadic arguments and replace the content of any mutable string with "replace!" Since we can only read the arguments before passing them, we cannot send different objects to the arguments to initWithFormat :: we just need to modify the objects. Keep in mind that making a copy of the object to check if it has changed, as in this method, is not a big practice.

Here's your second option. Use NSInvocation to create new arguments in stringWithFormat :.



- (NSString*)forwardMessage:(NSString*)format, ... {
    BOOL escape = NO;
    NSUInteger count = 0;

    char* ptr = (char*)[format UTF8String];
    while (*ptr) {
        if (*ptr == '%') {
            escape = !escape;
        } else if (escape) {
            if (*ptr == '@') {
                // this is an object
            }
            ++count;
            escape = NO;
        }
        ++ptr;
    }

    char* sig = malloc(3 + count + 2);
    memset(sig, '@', 3 + count);
    sig[3 + count] = ':';
    sig[3 + count + 1] = '\0';

    NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:sig]];

    free(sig);

    [invocation setTarget:[NSString class]];
    [invocation setSelector:@selector(stringWithFormat:)];

    [invocation setArgument:&format atIndex:2];

    va_list args;
    va_start(args, format);

    for (NSUInteger i = 0; i < count; ++i) {
        void* arg = va_arg(args, void*);
        // arg is an object, you can change it here
        [invocation setArgument:&arg atIndex:i + 3];
    }

    [invocation invoke];

    va_end(args);

    id ret;

    [invocation getReturnValue:&ret];

    return ret;
}

      

This method has one drawback: it won't work if you pass in types with sizes that are not the same as void * or id. For example, integers work, but floats don't come out correctly. [self forwardMessage:@"test %d asd %s %f %@ %d", 2, "asd", 2.897, @"test" 5]

returns test 2 asd asd 0.000000 test 5

. It wouldn't be too hard to add a little more logic to get certain types to work correctly, though.

I'm not sure I can recommend any of these solutions, but they might work for your situation.

Third option: if your arguments are only NSString objects, I would have to recommend dropping stringWithFormat: and parse / format the string yourself.

+1


source


I think it's pretty hard to do it nicely. The point is, you cannot create va_list

; "functions" va_start

, va_end

etc. are defined as macros hiding ugly inner things that probably don't want to get your hands dirty.

If you really want to do this, perhaps you can parse the contents of the string format

(shouldn't be terribly difficult, especially if you assume that %@

are only possible format specifiers), so you know how many arguments there are in va_list

, then encode

each one and returns the final rebuild line. You are of course sorting out re-embedding stringWithFormat: then though.

The Internet tells me that there is no portable way to build va_list

, but if you could, you could use the following idea:

s = [[[NSString alloc] initWithFormat:format arguments:argp] autorelease];

      



I personally think your best bet is to just loop through the line and create the result using va_arg

whenever you come across a token %@

.

[edit] This follows from what @daxnitro says here below, the approach I suggested and he calls option 3: This is a quick hack, but only for object tokens, it works.

typedef enum {
NORMAL,
TOKEN
} STATE;

+ (NSString *) encodeWithFormat: (id) format, ... {

STATE s = NORMAL;
va_list argp;
va_start(argp, format);
unichar c;
NSString * tmp;
NSMutableString * out = [[NSMutableString alloc] init];

for(int i = 0; i < [format length]; i ++) {

    c = [format characterAtIndex: i];

    // simple state-based recognising
    switch(c) {
    case '%':
        if(s == NORMAL)
            s = TOKEN; // switch to token-mode
        else { // we were accepting tokens, so this is an escaped '%'
            [out appendFormat: @"%c", c];
            s = NORMAL;
        }
        break;
    default:
        if(s == NORMAL) // default case
            [out appendFormat: @"%c", c];
        else // accepting tokens, so check type
            switch(c) {
            case '@': // this is a string placeholder
                tmp = va_arg(argp, NSString*);
                [out appendFormat: @"%@", [Test encode:tmp]]; // your magic here
                s = NORMAL;
                break;
            // you could add cases for %d etc here, if necessary
            default: // some unrecognised placeholder. ignore.
                s = NORMAL;
            break;
            }
        break;
    }
}
va_end(argp);
return [out autorelease];
}

      

0


source







All Articles