5.2 Read GET and POST variables from forms and URLs

by Marko Riedel

5.2.1 Idea

We add a category to NSMutableDictionary that contains a method that initializes the dictionary with the variables that were passed to the CGI script.

The procedure is simple.

  1. Obtain the data from environment variables: if REQUEST_METHOD is POST read CONTENT_LENGTH bytes from the standard input; if it is GET, use the contents of QUERY_STRING.
  2. Split the data into key-value pairs. This is done by splitting on the ampersand character &. The data consists of a sequence of such pairs, where the equal sign = denotes assignment, as in

    key1=value1&key2=value2.

  3. Replace the plus character + by a space and hexadecimal escape sequences of the form %XX by the corresponding ASCII character in both the key and the value.

5.2.2 Implementation

We start by adding a method to NSString that will carry out the last of the steps that we described above. It will scan the respective C string and and write characters into a temporary buffer. We allocate the same number of bytes as in the original string since it can only get shorter.

 
@implementation NSString (CGI) 
 
- parseCGI 
{ 
    int pos, length = [self length]; 
    const char *cstr = [self cString]; 
 
    NSZone *zone = NSDefaultMallocZone(); 
 
    char *rbuf, *rcur; 
    NSString *result; 
 
    rbuf = rcur = 
        NSZoneMalloc(zone, length*sizeof(char));

We use a loop to do the actual processing. A plus character is recorded as a space character. A complete hexadecimal escape sequence is passed to sscanf for conversion into an unsigned integer, the lowest byte of which is recorded in the buffer.

 
    for(pos=0; pos<length; pos++){ 
        if(cstr[pos]==’+’){ 
            *rcur++ = ’_’; 
        } 
        else if(cstr[pos]==’%’ && pos+2<length && 
                isxdigit(cstr[pos+1]) && isxdigit(cstr[pos+2])){ 
            char hex[3] = { cstr[pos+1], cstr[pos+2], 0 }; 
            unsigned int dec; 
            sscanf(hex, ”%x”, &dec); 
            pos += 2; 
 
            *rcur++ = dec; 
        } 
        else{ 
            *rcur++ = cstr[pos]; 
        } 
    } 
    result = [NSString stringWithCString:rbuf length:rcur-rbuf]; 
    NSZoneFree(zone, rbuf); 
 
    return result; 
}

Finally the method converts the data into the result string and frees the temporary buffer.

The method initWithCGI (added to NSMutableString) actutally creates the dictionary. We prepare for this by reading the process environment and immediately consulting REQUEST_METHOD to determine how to obtain the raw data. We will scan the data from start to finish and declare three variables to maintain state during the scan: prev, the first character of the current key-value pair, eqpos, the position of the equal sign in the current pair, and pos, which gives the first position of the next pair or the end of the data.

 
@implementation NSMutableDictionary (CGI) 
 
- (id)initWithCGI 
{ 
    NSProcessInfo *procInfo = [NSProcessInfo processInfo]; 
    NSDictionary *env = [procInfo environment]; 
 
    NSString *method = [env objectForKey:@”REQUEST_METHOD”]; 
 
    int length, prev, pos, eqpos; 
    const char *bytes;

We obtain the data first. REQUEST_METHOD tells us how. We raise an exception if the data available on the standard input do not contain the number of bytes specified in CONTENT_LENGTH (this applies to POST submissions). We initialize the dictionary by preparing it to hold eight pairs (we could have chosen a different capacity).

 
    if([method isEqualToString:@”GET”]){ 
        NSString *str = [env objectForKey:@”QUERY_STRING”]; 
 
        length = [str length]; 
        bytes = [str cString]; 
    } 
    else if([method isEqualToString:@”POST”]){ 
        NSData *data; 
 
        length = [[env objectForKey:@”CONTENT_LENGTH”] 
                      intValue]; 
        data = [[NSFileHandle fileHandleWithStandardInput] 
                    readDataOfLength:length]; 
        if([data length]!=length){ 
            NSString *fmt = 
                @”expected_%d_characters_on_STDIN,_got_%d”; 
            [NSException raise:NSRangeException format:fmt, 
                          length, [data length]]; 
        } 
        bytes = [data bytes]; 
    } 
    else{ 
        [NSException raise:NSInvalidArgumentException 
                      format:@”request_method_not_supported:_%@”, 
                      method]; 
    } 
 
    [self initWithCapacity:8];

Now start the loop. We record the most recent occurence of the equal sign for use in the key-value split.

 
    for(prev=0, pos=0; pos<length; pos++){ 
        if(bytes[pos]==’=’){ 
            eqpos = pos; 
        }

An ampersand signals that a complete pair has been scanned, as does having scanned up to the last character of the string. We compute the length of the key string and the value string and make sure they contain meaningful values, e.g. the key should not be empty.

 
        if(bytes[pos]==’&’ ∣∣ (pos>0 && pos==length-1)){ 
            int klen, vlen; 
            NSString *key, *value; 
            id lookup; 
 
            klen = eqpos-prev; 
            vlen = pos-eqpos-1+(bytes[pos]==’&’ ? 0 : 1); 
 
            if(!(klen>0 && vlen>=0)){ 
                [NSException raise:NSInvalidArgumentException 
                              format:@”malformed_query/content”]; 
            }

It remains to convert the source bytes into Objective C strings and parse them with parseCGI as discussed previously.

 
            key = 
                [[NSString 
                      stringWithCString:bytes+prev 
                      length:klen] parseCGI]; 
            value = 
                [[NSString 
                      stringWithCString:bytes+eqpos+1 
                      length:vlen] parseCGI];

Now put the pair into the dictionary. There are three cases.

  1. The key is not in the dictionary. We add the key-value pair.
  2. The key is already in the dictionary but it is not an array. We have found a second value for the key, and replace the object that it refers to by an array containing the two values.
  3. The key is in the dictionary and refers to an array. We add the new value to the array.

Multiple values for the same key arise when the state of a set of checkboxes is submitted. We store the start position of the next pair in prev at the end of the body of the loop.

 
            lookup = [self objectForKey:key]; 
 
            if(lookup==nil){ 
                [self setObject:value forKey:key]; 
            } 
            else if([lookup isKindOfClass: 
                                 [NSMutableArray class]]==NO){ 
                [self setObject:[NSMutableArray 
                                      arrayWithObjects:value, 
                                      lookup, nil] 
                     forKey:key]; 
            } 
            else{ 
                [lookup addObject:value]; 
            } 
 
            prev = pos+1; 
        } 
    } 
 
    return self; 
}

The routine returns self, which now contains all pairs that were submitted.

This code was inspired by cgi-lib.pl by Steven E. Brenner.