cclparser.m   [plain text]


/*
 * Copyright (c) 2007 Apple Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

// ****************************************************************************
//  cclparser.m
//  libccl
//
//  Created by kevine on 3/1/06.
//  Copyright 2006-7 Apple, Inc. All rights reserved.
// *****************************************************************************
 
// #define SC_SCHEMA_DECLARATION(k,q)     extern NSString * k;      // ??
#import "cclparser.h"


// names that would otherwise cause ambiguity are expanded
#define kPersonalityExpanded @"CCLNameExpanded"
#define kPathExpanded @"CCLPathExpanded"
// localized by BTSA and network preferences


@implementation CCLParser

+ (CCLParser*) createCCLParser
{
    CCLParser* retVal= [[CCLParser alloc] init];

    return retVal;
}

/******************************************************************************
* see cclparser.h for a description of the class variables
******************************************************************************/
- (id) init
{
    self = [super init];
    mBundleData = [[NSMutableDictionary alloc] init];
    mBundlesProcessed = [[NSMutableDictionary alloc] init];
	mFlatOverrides = [[NSMutableDictionary alloc] init];
	mTypeFilter = nil;

    return self;
}

// ****************************************************************************************************
- (void) dealloc
{
	[self setTypeFilter:nil];
	[mFlatOverrides release];
	[mBundlesProcessed release];
    [mBundleData release];
    [super dealloc];

    return;
}

- (void)finalize
{
    [super finalize];
}

- (void)setTypeFilter:(NSSet*)desiredConnectTypes
{
	[desiredConnectTypes retain];
	[mTypeFilter release];

	mTypeFilter = desiredConnectTypes;
}


// ****************************************************************************************************
- (NSMutableDictionary*)buildBaseModelDict:(NSDictionary*)matchEntry
        path:(NSString*)cclPath personality:(NSString*)personality
{
    NSMutableDictionary* rval = NULL;
    NSMutableDictionary* baseDict = [NSMutableDictionary dictionaryWithCapacity:5];
    id connectType= [matchEntry objectForKey: (id)kCCLConnectTypeKey];
    id cclVars= [matchEntry objectForKey: (id)kCCLParametersKey];
    id gprsCaps = [matchEntry objectForKey: (id)kCCLGPRSCapabilitiesKey];

    if (!baseDict ||
            ![connectType isKindOfClass: [NSString class]] ||
            ![cclVars isKindOfClass: [NSDictionary class]])
        goto finish;

    [baseDict setValue:cclPath forKey:(id)kSCPropNetModemConnectionScript];
    [baseDict setValue:personality forKey:(id)kSCPropNetModemConnectionPersonality];
    [baseDict setValue:connectType forKey:(id)kCCLConnectTypeKey];
    [baseDict setValue:cclVars forKey:(id)kCCLParametersKey];

    if ([connectType isEqualTo:(id)kCCLConnectGPRS]) {
        if (![gprsCaps isKindOfClass:[NSDictionary class]])
            goto finish;
        [baseDict setValue:gprsCaps forKey:(id)kCCLGPRSCapabilitiesKey];
    }

    rval = baseDict;

finish:
    return rval;
}
   
// ****************************************************************************************************
- (BOOL) parseMatchEntry:(NSDictionary*)matchEntry path:(NSString*)cclPath personality:(NSString*)personality mergeDict:(NSMutableDictionary*)mergeDict
{
    BOOL retVal= NO;
    NSMutableDictionary *firstModelDict = nil;
	NSArray *supersedesList;

	NSArray* deviceNameList= [matchEntry objectForKey: (id)kCCLDeviceNamesKey];
	if(deviceNameList!= NULL && [deviceNameList isKindOfClass: [NSArray class]]) {
		NSEnumerator* deviceEnum= [deviceNameList objectEnumerator];
		NSDictionary* curEntry= [deviceEnum nextObject];
		
		retVal= YES;
		while(curEntry!= NULL) {
			BOOL success = NO;

			if([curEntry isKindOfClass: [NSDictionary class]]) {
				NSString* venName= [curEntry objectForKey: (id)kCCLVendorKey];
				NSString* modName= [curEntry objectForKey: (id)kCCLModelKey];

				if([venName isKindOfClass: [NSString class]] &&
						[modName isKindOfClass: [NSString class]]) {
					NSMutableDictionary *modelDict;

					// Since each UI dict has its own model/vendor keys,
					// UIs need a separate dictionary for each pair
					modelDict= [self buildBaseModelDict:matchEntry
							path:cclPath personality:personality];
					[modelDict setObject: modName forKey:(id)kCCLModelKey];
					[modelDict setObject: venName forKey:(id)kCCLVendorKey];
						
					NSMutableArray* venList= [mergeDict objectForKey: venName];
					if(venList!= NULL) {
						[venList addObject: modelDict];
					} else {
						venList= [NSMutableArray arrayWithObject: modelDict];
						[mergeDict setObject: venList forKey: venName];
					}
					success = YES;

					// stash for overrides below
					if (!firstModelDict)
						firstModelDict = modelDict;
				}
			}
			retVal&= success;
			curEntry= [deviceEnum nextObject];
		}
	}

	// for the overrides, we use firstModelDict captured above
	if (firstModelDict) {
		// .ccl bundles implicitly supersede flat scripts of the same basename
		NSString *oldName = [cclPath lastPathComponent]; 
		if ([[oldName pathExtension] isEqual:(id)kCCLFileExtension])
			oldName = [oldName stringByDeletingPathExtension];
		// if a bundle has multiple personalities, implicit override -> first
		if (![mFlatOverrides objectForKey:oldName])
			[mFlatOverrides setObject:firstModelDict forKey:oldName];
		/* all personalities one foo.ccl will all implicitly supersede foo
		else
			NSLog(@"WARNING: %@/%@ also claims to supersede %@",
					cclPath, personality, oldName);
		*/

		// if this personality overrides anything else,
		// add one description to mFlatOverrides
		supersedesList = [matchEntry objectForKey:(id)kCCLSupersedesKey];
		if (supersedesList && [supersedesList isKindOfClass:[NSArray class]]) {
			NSEnumerator *e = [supersedesList objectEnumerator];
			id flatscript;

			while ((flatscript = [e nextObject])) {
				if ([flatscript isKindOfClass:[NSString class]])
					[mFlatOverrides setObject:firstModelDict forKey:flatscript];
				else
					NSLog(@"%@'s Supersedes list: unintelligible object %@",
							cclPath, flatscript);
			}
		}
	}

    return retVal;
} 

// ****************************************************************************************************
- (BOOL) mergePersonalityDict: (NSDictionary*) mergeDict
{
    NSEnumerator* mergeVenEnum = [mergeDict keyEnumerator];
    NSString* venName;
    
    // walk list of vendors in the merge dictionary,
    // adding to existing lists in mBundleData
    while((venName = [mergeVenEnum nextObject])) {
        NSArray* mergeModels = [mergeDict objectForKey:venName];
        NSMutableArray* existingModels = [mBundleData objectForKey:venName];

        if(existingModels)
            [existingModels addObjectsFromArray:mergeModels];
        else
            [mBundleData setObject:mergeModels forKey:venName];
    }
    return YES;
}

// ****************************************************************************
- (BOOL)processFlatCCL:(NSString*)path named:(NSString*)name
{
    NSMutableDictionary *modelDict;
    NSMutableArray *otherVendor;

    // someday we could validate or auto-name, but flat CCLs are fading
    // (mutable for "cleanupMatchingModels")
    modelDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kCCLConnectDialup, (id)kCCLConnectTypeKey,
            name, (id)kCCLModelKey,
            (id)kCCLOtherVendorName, (id)kCCLVendorKey,
            path, (id)kSCPropNetModemConnectionScript,
        NULL];
    if (!modelDict)     return NO;

    // add to existing 'Other' vendor if it exists; else create
    otherVendor = [mBundleData objectForKey:(id)kCCLOtherVendorName];
    if (otherVendor) {
        [otherVendor addObject:modelDict];
    } else {
        if(!(otherVendor = [NSMutableArray arrayWithObject:modelDict]))
            return NO;
        [mBundleData setObject:otherVendor forKey:(id)kCCLOtherVendorName];
    }

    return YES;
}

// ****************************************************************************************************
- (BOOL) processCCLBundle:(NSString*)cclPath
{
    BOOL retVal= NO;
    NSDictionary* infoDict= [[NSBundle bundleWithPath: cclPath] infoDictionary];
	NSNumber *verNum;

	// check for verNum since broken plist doesn't lead to nULL infoDict
    if([infoDict isKindOfClass:[NSDictionary class]] &&
			(verNum = [infoDict objectForKey:(id)kCCLVersionKey]))
    {
		NSString *cfbundleID = [infoDict objectForKey:(id)kCFBundleIdentifierKey];
		NSString *opath;

		// ignore duplicate bundles
		// log if the duplication doesn't involve /S/L/Modem Scripts
		if ((opath = [mBundlesProcessed objectForKey:cfbundleID])) {
			if (!([opath hasPrefix:@"/System/Library"] ||
					[cclPath hasPrefix:@"/System/Library"]))
				NSLog(@"%@ appears to be a duplicate of %@ (id = %@); ignoring",
						cclPath, opath, cfbundleID);
			return YES;
		} else {
			// add the bundle ID
			[mBundlesProcessed setObject:cclPath forKey:(id)cfbundleID];
		}

        if([verNum isKindOfClass: [NSNumber class]] && [verNum isEqualToNumber:
                [NSNumber numberWithInt: kCCLBundleVersion]])
        { //Versions match, let the fun continue...
            NSDictionary* personalityList= [infoDict objectForKey: (id)kCCLPersonalitiesKey];
            if(personalityList!= NULL && [personalityList isKindOfClass: [NSDictionary class]])
            {
                NSMutableDictionary* mergeDict= [NSMutableDictionary dictionaryWithCapacity: [personalityList count]];
                NSEnumerator* personalityKeyEnum= [personalityList keyEnumerator];
                NSString* personalityKey= [personalityKeyEnum nextObject];
            
                retVal= YES;    
                while((personalityKey!= NULL) && retVal)
                {
                    NSDictionary* personalityEntry= [personalityList objectForKey: personalityKey];
                    if([personalityEntry isKindOfClass: [NSDictionary class]]) {
						BOOL interested;

						if (!mTypeFilter) {
							interested = YES;
						} else {
							NSString *connectType = [personalityEntry objectForKey:(id)kCCLConnectTypeKey];
							// see if this personality's type matches
							interested = [mTypeFilter containsObject:connectType];
						}
							
						if (interested)
							retVal= [self parseMatchEntry: personalityEntry path: cclPath personality: personalityKey mergeDict: mergeDict];
					}
                    personalityKey= [personalityKeyEnum nextObject];
                }

                if(retVal)
                    retVal= [self mergePersonalityDict: mergeDict];
            } else
                NSLog(@"skipping %@: trouble extracting personalities",cclPath);
        } else
            NSLog(@"skipping %@: incompatible CCL version number: %@",
                    cclPath, verNum);
    } else
        NSLog(@"skipping %@: malformed bundle dictionary", cclPath);

    return retVal;
}

// ****************************************************************************************************
- (BOOL) processFolder:(NSString*)folderPath
{
    BOOL retVal= YES;
    NSFileManager* fileMan=  [NSFileManager defaultManager];
    NSDirectoryEnumerator* folderEnum= [fileMan enumeratorAtPath:folderPath];
    NSString *curFileName, *displayName;

    while((curFileName = [folderEnum nextObject])) {
        NSString* filePath;
        BOOL isDir = NO, exists;

		filePath = [folderPath stringByAppendingPathComponent:curFileName];
		if (![fileMan fileExistsAtPath:filePath isDirectory:&isDir]) {
			NSLog(@"Warning: %@ doesn't seem to exist", filePath);
			continue;
		}

		// if it's a new-fangled .ccl bundle, process appropriately
		if (isDir) {
			if ([[curFileName pathExtension] isEqualToString:(id)kCCLFileExtension]){
				[folderEnum skipDescendents];   // don't descend into bundle
				retVal&= [self processCCLBundle: filePath];
			} else {
				// ignore .directories
				if ([[filePath lastPathComponent] hasPrefix:@"."])
					[folderEnum skipDescendents];
			}
        } else {
			// (note: we always process flat CCLs b/c we don't know their type)
			// handle as a flat file
			CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:filePath];
			LSItemInfoRecord info;
			OSStatus errn;

			// 4152940 requests invisibility info in NSFileManager
			errn = LSCopyItemInfoForURL(url, kLSRequestBasicFlagsOnly, &info);
			if (errn) {
				NSLog(@"skipping %@: error %d getting item info",filePath,errn);
				continue;
			}

			// must be visible and flat
			if ((info.flags & (kLSItemInfoIsPlainFile | kLSItemInfoIsSymlink))
					&& !(info.flags & kLSItemInfoIsInvisible)) {
				// use display name of file
				displayName = [fileMan displayNameAtPath:filePath];
				retVal&= [self processFlatCCL:filePath named:displayName];
            }
        }
    }

    return retVal;
}


// ****************************************************************************************************
- (void) cleanupNameWithPersonality: (NSMutableDictionary*) modelDict
{
    if([modelDict objectForKey: kPersonalityExpanded]== NULL)
    {
        NSString* modelName= [modelDict objectForKey: (id)kCCLModelKey];
        NSString* modelPersonality= [modelDict objectForKey: (id)kSCPropNetModemConnectionPersonality];

	if (modelName && modelPersonality) {
	    NSString* newModelName= [NSString stringWithFormat: @"%@, %@", modelName, modelPersonality];
	    [modelDict setObject: newModelName forKey: (id)kCCLModelKey];
	    [modelDict setObject: [NSNull null] forKey: kPersonalityExpanded];
	}
    }
}

// ****************************************************************************************************
- (void) cleanupNameWithCCLName: (NSMutableDictionary*) modelDict
{
    if([modelDict objectForKey: kPathExpanded]== NULL)
    {
        NSString *modelName = [modelDict objectForKey: (id)kCCLModelKey];
	NSString  *cScript = [modelDict objectForKey:(id)kSCPropNetModemConnectionScript];
        NSString *cclName = [cScript lastPathComponent];
	if ([modelName isEqual:cclName]) {
	    NSLog(@"a second copy of %@ is installed at %@?", cclName, cScript);
	    cclName = cScript;
	}

	if (![modelName isEqual:cclName]) {
	    NSString* newModelName= [NSString stringWithFormat: @"%@, %@", modelName, cclName];
	    [modelDict setObject: newModelName forKey: (id)kCCLModelKey];
	    [modelDict setObject: [NSNull null] forKey: kPathExpanded];
	}
    }
}

// ****************************************************************************************************
- (bool) cleanupMatchingModels: (NSDictionary*) curModelDict modelArray: (NSArray*) modelArray changeSEL: (SEL) selector
{
    bool retVal= NO;
    NSMutableArray* matchList= [NSMutableArray array];
    NSString* matchModelName= [curModelDict objectForKey: (id)kCCLModelKey];
    NSEnumerator* modelEnum= [modelArray objectEnumerator];
    NSDictionary* curDict= [modelEnum nextObject];
    while(curDict!= NULL)
    {
        if(curDict!= curModelDict)
        {
            NSString* curModelName= [curDict objectForKey: (id)kCCLModelKey];
            if([matchModelName isEqualToString: curModelName])
            {
                [matchList addObject: curDict];
            }
            
        }
        curDict= [modelEnum nextObject];
    }
    if([matchList count]>0)
    {
        [matchList addObject: curModelDict];
        NSEnumerator* modelEnum= [matchList objectEnumerator];
        NSMutableDictionary* curDict= [modelEnum nextObject];
        while(curDict!= NULL)
        {
            [self performSelector: selector withObject: curDict];
            curDict= [modelEnum nextObject];
        }
        retVal= YES;
    }
    return retVal;
}

// ****************************************************************************************************
- (void)cleanupDuplicates
{
    NSEnumerator	*keyEnum;
    NSString		*curVenName, *modName;
	NSMutableArray	*persList;
	unsigned		persCount, i;

	// first remove overridden flat CCLs (message -> nil == no-op)
	// remove from mBundleData/Other any names in mFlatOverrides
	persList = [mBundleData objectForKey:kCCLOtherVendorName];
	persCount = [persList count];
	for (i = 0; i < persCount; i++) {
		modName = [[persList objectAtIndex:i] objectForKey:(id)kCCLModelKey];
		if ([mFlatOverrides objectForKey:modName]) {
			[persList removeObjectAtIndex:i];
			i--; persCount--;		// since everything slid
		}
	}
	// and if nothing's left, remove the Other vendor entirely
	if (persList && [persList count] == 0)
		[mBundleData removeObjectForKey:kCCLOtherVendorName];

	// check each model list against itself (XX use NSOrderedSet?)
	keyEnum = [mBundleData keyEnumerator];
    while(curVenName= [keyEnum nextObject]) {
        NSArray* modelArray= [mBundleData objectForKey: curVenName];
        NSEnumerator* modelEnum= [modelArray objectEnumerator];
        NSMutableDictionary* curModelDict;
        while((curModelDict = [modelEnum nextObject]))
            if([self cleanupMatchingModels: curModelDict modelArray: modelArray changeSEL: @selector(cleanupNameWithCCLName:)])
                [self cleanupMatchingModels: curModelDict modelArray: modelArray changeSEL: @selector(cleanupNameWithPersonality:)];
    }
}

// ****************************************************************************************************
- (NSArray*)copyVendorList
{
    NSMutableArray *vendors;

    // get mutable copy of all vendors
    vendors = [[mBundleData allKeys] mutableCopy];

    // remove 'Other'; sort; and append
    [vendors removeObject:(id)kCCLOtherVendorName];
    [vendors sortUsingSelector:@selector(caseInsensitiveCompare:)];
	if ([mBundleData objectForKey:(id)kCCLOtherVendorName])
		[vendors addObject:(id)kCCLOtherVendorName];

    return vendors;
}

// ****************************************************************************************************
- (NSArray*)getModelListForVendor: (NSString*) vendor
{
	NSMutableArray *models = [mBundleData objectForKey:vendor];
	NSSortDescriptor* sortd = [[NSSortDescriptor alloc]
			initWithKey:(id)kCCLModelKey ascending:TRUE 
			selector:@selector(caseInsensitiveCompare:)];

	// might have sorted it before, but this doesn't happen often and
	// the sort should be fast if already sorted
	[models sortUsingDescriptors:[NSArray arrayWithObject:sortd]];
	[sortd release];

    return models;
}

// ****************************************************************************************************
- (void) clearParser
{
    [mBundleData removeAllObjects];
	[mBundlesProcessed removeAllObjects];
	[mFlatOverrides removeAllObjects];
}

/*******************************************************************************
* mergeCCLPersonality:withDeviceConfiguration: takes a "modem" dict that would
* have come from SystemConfiguration (e.g. containing phone number and such)
* and sets various keys in a new dictionary based on data in the personality.
* Some key/value pairs are just copied (connection script and personality
* within the script bundle) or removed as appropriate, but GPRS APN and CID
* are expressed as "preferred" (aka "default") in the personality dict while
* they are hard values in the device dictionary.  If the user has already
* chosen APN or CID for this personality, we don't copy over the preferred
* values.  But if the user is choosing the personality for the first time,
* we do copy the values over.  The UI can then use them to populate the UI.
*
* We return a new NSMutableDictionary so that the UI can call us repeatedly
* with different personalities and the the same deviceConfiguration
* dictionary (from SysConfig) without any defaults from one personality
* polluting the UI when the user chooses another personality that perhaps
* doesn't have any defaults.  Only when the user saves the configuration
* should we ever see any of the things we inserted feed back to us.
*******************************************************************************/

- (NSMutableDictionary*)mergeCCLPersonality:(NSDictionary*)personality withDeviceConfiguration:(NSDictionary*)deviceConfiguration;
{
    NSMutableDictionary *rval = [deviceConfiguration mutableCopy];
    NSString *mScript, *pScript, *mPers;
    NSString *pName, *pType, *pVendor;
    BOOL newPersonality;

    // see whether the user changed personalities
    mScript = [rval objectForKey:(id)kSCPropNetModemConnectionScript];
    pScript = [personality objectForKey:(id)kSCPropNetModemConnectionScript];
    mPers = [rval objectForKey:(id)kSCPropNetModemConnectionPersonality];
    pName = [personality objectForKey:(id)kSCPropNetModemConnectionPersonality];
    newPersonality = (![pScript isEqual:mScript] || ![pName isEqual:mPers]);
    // XX any other checking of current modem dictionary needed?

    // copy vendor, model, script, personality (only model+script for flat CCL)
    pVendor = [personality objectForKey:(id)kCCLVendorKey];
    if (pVendor)
        [rval setObject:pVendor forKey:(id)kCCLVendorKey];
    else
        [rval removeObjectForKey:(id)kCCLVendorKey];
    [rval setObject:[personality objectForKey:(id)kCCLModelKey]
            forKey:(id)kCCLModelKey];
    [rval setObject:pScript forKey:(id)kSCPropNetModemConnectionScript];
    if (pName)
        [rval setObject:pName forKey:(id)kSCPropNetModemConnectionPersonality];
    else
        [rval removeObjectForKey:(id)kSCPropNetModemConnectionPersonality];

    // check to see if APN or CID need to be populated / updated
    // - if GPRS and not set; set to preferred
    // - see below for Preferred CID logic
    pType = [personality objectForKey:(id)kCCLConnectTypeKey];
    if ([pType isEqual:(id)kCCLConnectGPRS]) {
        NSDictionary *cclParms =
                [personality objectForKey:(id)kCCLParametersKey];
        NSString *savedAPN =
                [rval objectForKey:(id)kSCPropNetModemAccessPointName];
        NSString *preferredAPN =
                [cclParms objectForKey:(id)kCCLPreferredAPNKey];
        NSString *savedCID =
                [rval objectForKey:(id)kSCPropNetModemDeviceContextID];
        NSNumber *preferredCID =
                [cclParms objectForKey:(id)kCCLPreferredCIDKey];
        BOOL safeCIDs = [[[personality objectForKey:(id)kCCLGPRSCapabilitiesKey]
                            objectForKey:(id)kCCLIndependentCIDs] boolValue];
        // if !safeCIDs, perhaps we could set the preferred to max[- 1?]?
        // (we already insert values)

        // A saved CID should be overwritten if changing personalities and
        // the new personality isn't known to be safe for the old CID
        // or if there is a new preferred CID for the new personality.
        // If there is no preferred CID, the old CID should be removed if
        // CIDs are unsafe.  In other words, leave a CID alone only if
        // - it was already selected for this personality
        // - it is known to be safe for this personality && no preferred
        // unset, no preferred -> leave alone
        // unset, preferred -> set to preferred
        // set, !new personality -> leave alone
        // set, new personality, preferred -> set to preferred
        // set, new, !preferred, safe -> leave alone
        // set, new, !preferred, unsafe -> clear
        if (!savedCID || newPersonality) {
            if (preferredCID) {
                [rval setObject:[preferredCID stringValue]
                        forKey:(id)kSCPropNetModemDeviceContextID];
            } else if (savedCID && !safeCIDs) {
                [rval removeObjectForKey:(id)kSCPropNetModemDeviceContextID];
            }
        }
        if (preferredAPN && (!savedAPN || newPersonality))
            [rval setObject:preferredAPN forKey:(id)kSCPropNetModemAccessPointName];
    } else {
        // clear any existing APN/CID
        [rval removeObjectForKey:(id)kSCPropNetModemAccessPointName];
        [rval removeObjectForKey:(id)kSCPropNetModemDeviceContextID];
    }

    return rval;
}

/*******************************************************************************
* -upgradeDeviceConfiguration takes a system configuration modem dictionary
* that doesn't have a device/vendor pair and adds one.  It may or may not
* do smart things like use keys in the CCL bundles to tell it which
* personalities are equivalent to old flat scripts.  :)
*******************************************************************************/
- (NSMutableDictionary*)upgradeDeviceConfiguration:(NSDictionary*)deviceConf
{
    NSMutableDictionary *rval = nil;
    NSString *vendor, *model, *cScript, *csName, *persName = nil;
    NSArray *persList;
	NSEnumerator *e;
	NSDictionary *pers;
	BOOL knownModel = NO;


    // use the kSCProp constants since these are SC dicts
    csName = [[deviceConf objectForKey:(id)kSCPropNetModemConnectionScript] lastPathComponent];

    // let's see if there's anything here (we'll validate below)
    vendor = [deviceConf objectForKey:(id)kSCPropNetModemDeviceVendor];
    model = [deviceConf objectForKey:(id)kSCPropNetModemDeviceModel];

	if (model && vendor) {
		persList = [mBundleData objectForKey:vendor];
		e = [persList objectEnumerator];
		while ((pers = [e nextObject])) {
			if ([[pers objectForKey:(id)kCCLModelKey] isEqual:model]) {
				knownModel = YES;
				break;
			}
		}
	} else {
        // maybe overridden (implicitly or otherwise)
        if ((pers = [mFlatOverrides objectForKey:csName])) {
            vendor = [pers objectForKey:(id)kCCLVendorKey];
            model = [pers objectForKey:(id)kCCLModelKey];
            persName = [pers objectForKey:(id)kSCPropNetModemConnectionPersonality];
			knownModel = YES;
        } else {
            // fall back to trying "other"/<connectionscript>
            vendor = kCCLOtherVendorName;
			if ([[csName pathExtension] isEqual:(id)kCCLFileExtension])
				csName = [csName stringByDeletingPathExtension];
            model = csName;

			// validate with 'other' vendor list
			persList = [mBundleData objectForKey:vendor];
			e = [persList objectEnumerator];
			while ((pers = [e nextObject])) {
				if ([[pers objectForKey:(id)kCCLModelKey] isEqual:model]) {
					knownModel = YES;
					break;
				}
			}
        }
    }

	if (knownModel) {
        // yay; go ahead and create result dictionary
        rval = [deviceConf mutableCopy];
        [rval setObject:vendor forKey:(id)kSCPropNetModemDeviceVendor];
        [rval setObject:model forKey:(id)kSCPropNetModemDeviceModel];
		cScript = [pers objectForKey:(id)kSCPropNetModemConnectionScript];
		if (cScript)
			[rval setObject:cScript forKey:(id)kSCPropNetModemConnectionScript];
        if (persName)
            [rval setObject:persName forKey:(id)kSCPropNetModemConnectionPersonality];
    }

    return rval;        // still nil if the vendor/model didn't work out
}

@end