MoreOSL/MoreOSLStringCompare.c

/*
    File:       MoreOSLStringCompare.c
 
    Contains:   String comparison utilities for MoreOSL.
 
    Written by: Quinn
 
    Copyright:  Copyright © 2000 by Apple Computer, Inc., all rights reserved.
 
                You may incorporate this Apple sample source code into your program(s) without
                restriction. This Apple sample source code has been provided "AS IS" and the
                responsibility for its operation is yours. You are not permitted to redistribute
                this Apple sample source code as "Apple sample source code" after having made
                changes. If you're going to re-distribute the source, we require that you make
                it clear in the source that the code was descended from Apple sample source
                code, but that you've made changes.
 
    Change History (most recent first):
 
         <3>     27/3/00    Quinn   Change "Pre-Carbon" -> "Non-Carbon" to be consisent with rest of
                                    MOSL. Renumber 'scpt' resource ID.
         <2>     20/3/00    Quinn   Comments, asserts, use MoreAppleEvents where appropriate, remove
                                    dependency on <string.h>.
         <1>      9/3/00    Quinn   First checked in.
*/
 
/////////////////////////////////////////////////////////////////
 
// MoreIsBetter Setup
 
#include "MoreSetup.h"
 
// Our Prototypes
 
#include "MoreOSLStringCompare.h"
 
// Mac OS Interfaces
 
#include <AERegistry.h>
#include <AppleEvents.h>
#include <ASRegistry.h>
#include <OSA.h>
#include <Resources.h>
 
// ANSI Interfaces
 
// #include <string.h>
 
// MIB Prototypes
 
#include "MoreMemory.h"
#include "MoreResources.h"
#include "MoreTextUtils.h"
#include "MoreAppleEvents.h"
#include "MoreOSL.h"                // for error codes
 
/////////////////////////////////////////////////////////////////
 
#if TARGET_API_MAC_CARBON
 
    static OSStatus MoreCFError(const void *p)
        // CF doesnÕt return real error codes (grumble grumble grumble)
        // so we follow the HI ToolboxÕs lead and return
        // coreFoundationUnknownErr if we get an error from CF.
    {
        OSStatus err;
        
        if (p == nil) {
            err = coreFoundationUnknownErr;
        } else {
            err = noErr;
        }
        return err;
    }
 
    // The following are the CFString compare options we use for 
    // all CFString operations.
        
    enum {
        kMyCFCompareOptions = 
                    kCFCompareCaseInsensitive 
                  | kCFCompareNonliteral
                  | kCFCompareLocalized
    };
 
    static OSStatus MOSLStringCompareCFStringCFString(DescType oper, CFStringRef str1, CFStringRef str2, Boolean *result)
        // The guts of the CFString comparison engine.  This compares two
        // CFStrings, str1 and str2, using the supplied operator, returning
        // the result in *result.  For Carbon code, all string comparisons
        // come through here.
    {
        OSStatus err;
        CFRange  range;
        CFRange  resultRange;
        
        MoreAssertQ(str1   != nil);
        MoreAssertQ(str2   != nil);
        MoreAssertQ(result != nil);
 
        err = noErr;        
        switch (oper) {
            case kAEEquals:
                *result = (CFStringCompare(str1, str2, kMyCFCompareOptions) == 0);
                break;
            case kAEGreaterThanEquals:
                *result = (CFStringCompare(str1, str2, kMyCFCompareOptions) >= 0);
                break;
            case kAEGreaterThan:
                *result = (CFStringCompare(str1, str2, kMyCFCompareOptions) > 0);
                break;
            case kAELessThan:
                *result = (CFStringCompare(str1, str2, kMyCFCompareOptions) < 0);
                break;
            case kAELessThanEquals:
                *result = (CFStringCompare(str1, str2, kMyCFCompareOptions) <= 0);
                break;
            case kAEBeginsWith:
                range.location = 0;
                range.length   = CFStringGetLength(str1);
                *result = CFStringFindWithOptions(str1, str2, range, kMyCFCompareOptions, &resultRange)
                                && (resultRange.location == 0);
                break;
            case kAEEndsWith:
                range.location = 0;
                range.length   = CFStringGetLength(str1);
                *result = CFStringFindWithOptions(str1, str2, range, kMyCFCompareOptions | kCFCompareBackwards, &resultRange)
                                && ((resultRange.location + resultRange.length) == range.length);
                break;
            case kAEContains:
                range.location = 0;
                range.length   = CFStringGetLength(str1);
                *result = CFStringFindWithOptions(str1, str2, range, kMyCFCompareOptions, nil);
                break;
            default:
                MoreAssertQ(false);
                err = kMOSLUnrecognisedOperatorErr;
                break;
        }
        return err;
    }
 
    static OSStatus DescToCFString(const AEDesc *desc, CFStringRef *str)
        // This routine converts an AEDesc to a CFString.  For text
        // with multiple script runs, it relies on AppleScriptÕs
        // built-in coercions to typeUnicodeText.  These are only
        // present in recent AppleScript implementation.
        // ¥¥¥ need to figure out if they go back to 8.1 and what to do otherwise
    {
        OSStatus    err;
        CFStringRef newStr;
        DescType    typeToCoerceTo;
        AEDesc      coercedDesc;
        Size        coercedDescDataSize;
        Handle      coercedDescData;
        IntlText   *intlTextPtr;
        TextEncoding encoding;
        
        MoreAssertQ(desc != nil);
        MoreAssertQ(str  != nil);
        MoreAssertQ(*str == nil);
        
        MoreAENullDesc(&coercedDesc);
        coercedDescData = nil;
        
        newStr = nil;
        switch (desc->descriptorType) {
        
            // Any descriptor type that can contain multiple style runs must
            // be coerced to UniCode because each style run could be in a
            // different script.
            
            case typeUnicodeText:
            case typeStyledUnicodeText:
            case typeStyledText:
            case typeAEText:
            case typeEncodedString:
                typeToCoerceTo = typeUnicodeText;
                break;
                
            // Descriptor types made up of a single style run are coerced to
            // typeIntlText, which gives me a script and language value.  Note
            // that we send default through this branch as well because itÕs
            // much more likely that a non-standard type has a coercion to
            // typeIntlText than to UniCode because typeIntlText is much older
            // than typeUnicodeText.
            
            case typeText:
            case cText:
            case typeIntlText:
            case typeCString:
            case typePString:
            default:
                typeToCoerceTo = typeIntlText;
                break;
        }
        
        // Coerce the descriptor to the required type and extract the
        // descriptorÕs data into a handle.
        
        err = AECoerceDesc(desc, typeToCoerceTo, &coercedDesc);
        if (err == noErr) {
            coercedDescDataSize = AEGetDescDataSize(&coercedDesc);
            if (coercedDesc.descriptorType == typeIntlText) {
                // WeÕre going to add a null terminator to the text in the handle 
                // later in this routine, so letÕs make space for it now.
                coercedDescDataSize += 1;
            }
            coercedDescData = NewHandle(coercedDescDataSize);
            err = MoreMemError(coercedDescData);
        }
        if (err == noErr) {
            HLock(coercedDescData);
            MoreAssertQ(MemError() == noErr);
            err = AEGetDescData(&coercedDesc, *coercedDescData, AEGetDescDataSize(&coercedDesc));
        }
        
        // Now create a CFString based on the data in the coercedDescData handle.
        
        if (err == noErr) {
            if (coercedDesc.descriptorType == typeIntlText) {
            
                // Creating a CFString from typeIntlText is tricky, but not
                // that difficult.
                
                // First we put a null terminator (see, I promised thereÕd be one)
                // at the end of the text.
                
                (*coercedDescData)[coercedDescDataSize - 1] = 0;
                intlTextPtr = (IntlText *) *coercedDescData;
 
                // Then we combine the old style script and language codes into a new
                // style text encoding.
                
                err = UpgradeScriptInfoToTextEncoding(intlTextPtr->theScriptCode,
                                                      intlTextPtr->theLangCode,
                                                      kTextRegionDontCare,
                                                      nil,
                                                      &encoding);
 
                // Finally we create a CFString based on that encoding.
                
                if (err == noErr) {
                    newStr = CFStringCreateWithCString(nil, intlTextPtr->theText, encoding);
                    err = MoreCFError(newStr);
                }
            } else {
                MoreAssertQ(coercedDesc.descriptorType == typeUnicodeText);
                MoreAssertQ((coercedDescDataSize % 2) == 0);
                
                // Creating a CFString from UniCode is easy.
                
                newStr = CFStringCreateWithCharacters(nil, (const UniChar *) *coercedDescData, coercedDescDataSize / 2);
                err = MoreCFError(newStr);
            }
        }
        
        // Clean up.
        
        if (err == noErr) {
            MoreAssertQ(newStr != nil);
        } else {
            if (newStr != nil) {
                CFRelease(newStr);
                newStr = nil;
            }
        }
        *str = newStr;
        
        MoreAEDisposeDesc(&coercedDesc);
        if (coercedDescData != nil) {
            DisposeHandle(coercedDescData);
            MoreAssertQ(MemError() == noErr);
        }
        
        return err;
    }
    
    extern pascal OSStatus MOSLStringCompare(DescType oper, const AEDesc *data1, const AEDesc *data2, Boolean *result)
        // See comment in header.
    {
        OSStatus err;
        CFStringRef str1;
        CFStringRef str2;
 
        MoreAssertQ(data1  != nil);
        MoreAssertQ(data2  != nil);
        MoreAssertQ(result != nil);
                
        str1 = nil;
        str2 = nil;
        
        err = DescToCFString(data1, &str1);
        if (err == noErr) {
            err = DescToCFString(data2, &str2);
        }
        if (err == noErr) {
            err = MOSLStringCompareCFStringCFString(oper, str1, str2, result);
        }
        if (str1 != nil) {
            CFRelease(str1);
        }
        if (str2 != nil) {
            CFRelease(str2);
        }
 
        return err;
    }
 
    extern pascal OSStatus MOSLStringEqualsCFStringDesc(CFStringRef myObjectName, const AEDesc *comparisonName, Boolean *result)
        // See comment in header.
    {
        OSStatus err;
        CFStringRef str2;
        
        str2 = nil;
        err = DescToCFString(comparisonName, &str2);
        if (err == noErr) {
            err = MOSLStringCompareCFStringCFString(kAEEquals, myObjectName, str2, result);
        }
        if (str2 != nil) {
            CFRelease(str2);
        }
        return err;
    }
 
#else   // not TARGET_API_MAC_CARBON
 
    // These global variables allow us to hold the compiled AppleScript
    // in memory, permanently ready to execute, which should speed up string
    // comparison (but I didnÕt profile it).
    
    static ComponentInstance gOSAInstance = nil;
    static OSAID             gOSAContext  = kOSANullScript;
 
    static OSStatus PrepareCompareScript(void)
        // This routine is called the first time that string comparison
        // is done to load and prepare the AppleScript that we use
        // to do the comparison.
    {
        OSStatus err;
        OSStatus junk;
        Handle   scriptData;
        SInt8    s;
        AEDesc   scriptDataDesc;
        
        MoreAENullDesc(&scriptDataDesc);
 
        // Load the AppleScript resource and then create a descriptor
        // from its contents.
                
        scriptData = Get1Resource('scpt', 5300);
        err = MoreResError(scriptData);
        if (err == noErr) {
            s = HGetState(scriptData);
            MoreAssertQ(MemError() == noErr);
            HLock(scriptData);
            MoreAssertQ(MemError() == noErr);
            
            err = AECreateDesc(typeScript, *scriptData, GetHandleSize(scriptData), &scriptDataDesc);
            
            HSetState(scriptData, s);
        }
        
        // Open a connection to the generic scripting component and prepare
        // our script for execution.
        
        if (err == noErr) {
            err = OpenADefaultComponent(kOSAComponentType, kOSAGenericScriptingComponentSubtype, &gOSAInstance);
        }
        if (err == noErr) {
            err = OSALoad(gOSAInstance, &scriptDataDesc, 0, &gOSAContext);
        }
 
        // Clean up.
        
        MoreAEDisposeDesc(&scriptDataDesc);
        if (err != noErr) {
            if ( gOSAInstance != nil ) {
                if ( gOSAContext != kOSANullScript ) {
                    junk = OSADispose(gOSAInstance, gOSAContext);
                    MoreAssertQ(junk == noErr);
                    gOSAContext = kOSANullScript;
                }
                junk = CloseComponent(gOSAInstance);
                MoreAssertQ(junk == noErr);
                gOSAInstance = nil;
            }
        }
        return err;
    }
 
    extern pascal OSStatus MOSLStringCompare(DescType oper, const AEDesc *data1, const AEDesc *data2, Boolean *result)
        // See comment in header.
    {
        OSStatus            err;
        OSStatus            errNum;
        AppleEvent          event;
        AppleEvent          reply;
        DescType            junkType;
        Size                junkSize;
 
        MoreAssertQ(data1  != nil);
        MoreAssertQ(data2  != nil);
        MoreAssertQ(result != nil);
 
        MoreAENullDesc(&event);
        MoreAENullDesc(&reply);
        
        // If this is the first time weÕve run, load and prepare our script.
        
        err = noErr;
        if ( gOSAContext == kOSANullScript ) {
            err = PrepareCompareScript();
        }
        
        // Create the self-send AppleEvent that weÕre going to send to the script.
        // Note that the 'MOSL' creator code is hard wired here (rather than using
        // kMyCreator) because I expect it to remain the same if you move this
        // code to another application.
        
        if (err == noErr) {
            err = MoreAECreateAppleEventSelfTarget('MOSL', oper, &event);
        }
        
        // Put data1 and data2 into the direct object and argument parameters.
        
        if (err == noErr) {
            err = AEPutParamDesc(&event, keyDirectObject, data1);
        }
        if (err == noErr) {
            err = AEPutParamDesc(&event, keyASArg, data2);
        }
        
        // Get the script to execute the event, then extract the response.
        
        if (err == noErr) {
            err = OSADoEvent(gOSAInstance, &event, gOSAContext, kOSAModeNeverInteract | kOSAModeCantSwitchLayer, &reply);
        }
        if (err == noErr) {
            err = AEGetParamPtr(&reply, keyErrorNumber, typeLongInteger, &junkType, &errNum, sizeof(errNum), &junkSize);
            if (err == noErr) {
                err = errNum;
            } else if (err == errAEDescNotFound) {
                err = noErr;
            }
        }
        if (err == noErr) {
            err = AEGetParamPtr(&reply, keyAEResult, typeBoolean, &junkType, result, sizeof(*result), &junkSize);
        }
 
        // Clean up.
        
        MoreAEDisposeDesc(&event);
        MoreAEDisposeDesc(&reply);
 
        return err;     
    }
 
#endif  // not TARGET_API_MAC_CARBON
 
// The code for the wrapper routines is layered on top of MOSLStringCompare,
// so common to both Carbon and non-Carbon.
 
extern pascal OSStatus MOSLStringEqualsPStringDesc(ConstStr255Param myObjectName, const AEDesc *comparisonName, Boolean *result)
    // See comment in header.
{
    OSStatus err;
    AEDesc   desc1;
    
    MoreAENullDesc(&desc1);
    
    err = AECreateDesc(typeText, &myObjectName[1], myObjectName[0], &desc1);
    if (err == noErr) {
        err = MOSLStringCompare(kAEEquals, &desc1, comparisonName, result);
    }
    
    MoreAEDisposeDesc(&desc1);
 
    return err;
}
 
extern pascal OSStatus MOSLStringEqualsCStringDesc(const char *myObjectName, const AEDesc *comparisonName, Boolean *result)
    // See comment in header.
{
    OSStatus err;
    AEDesc   desc1;
    
    MoreAENullDesc(&desc1);
    
    err = AECreateDesc(typeText, myObjectName, MoreStrLen(myObjectName), &desc1);
    if (err == noErr) {
        err = MOSLStringCompare(kAEEquals, &desc1, comparisonName, result);
    }
    
    MoreAEDisposeDesc(&desc1);
 
    return err;
}
 
extern pascal OSStatus MOSLStringEqualsIntlTextDesc(ScriptCode script, LangCode lang, const void *textBuf, Size textBufLen, const AEDesc *comparisonName, Boolean *result)
    // See comment in header.
{
    OSStatus err;
    AEDesc   desc1;
    Handle   tmpH;
    
    tmpH = nil;
    MoreAENullDesc(&desc1);
    
    err = PtrToHand(&script, &tmpH, sizeof(script));
    if (err == noErr) {
        err = PtrAndHand(&lang, tmpH, sizeof(lang));
    }
    if (err == noErr) {
        err = PtrAndHand(textBuf, tmpH, textBufLen);
    }
    if (err == noErr) {
        HLock(tmpH);
        err = AECreateDesc(typeIntlText, *tmpH, GetHandleSize(tmpH), &desc1);
    }
 
    if (tmpH != nil) {
        DisposeHandle(tmpH);
        MoreAssertQ(MemError() == noErr);
    }
 
    if (err == noErr) {
        err = MOSLStringCompare(kAEEquals, &desc1, comparisonName, result);
    }
    
    MoreAEDisposeDesc(&desc1);
 
    return err;
}